前言

记得前段时间我们生产上的一个网关出现了故障。

这个网关逻辑非常简单,就是接收客户端的请求然后解析报文最后发送短信。

但这个请求并不是常见的 HTTP ,而是利用 Netty 自定义的协议。

有个前提是:网关是需要读取一段完整的报文才能进行后面的逻辑。

问题是有天突然发现网关解析报文出错,查看了客户端的发送日志也没发现问题,最后通过日志发现收到了许多不完整的报文,有些还多了。

于是想会不会是 TCP 拆、粘包带来的问题,最后利用 Netty 自带的拆包工具解决了该问题。

这便有了此文。

TCP 协议

问题虽然解决了,但还是得想想原因,为啥会这样?打破砂锅问到底才是一个靠谱的程序员。

这就得从 TCP 这个协议说起了。

TCP 是一个面向字节流的协议,它是性质是流式的,所以它并没有分段。就像水流一样,你没法知道什么时候开始,什么时候结束。

所以他会根据当前的套接字缓冲区的情况进行拆包或是粘包。

下图展示了一个 TCP 协议传输的过程:

发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取。

当我们发送两个完整包到接收端的时候:

正常情况会接收到两个完整的报文。


但也有以下的情况:

接收到的是一个报文,它是由发送的两个报文组成的,这样对于应用程序来说就很难处理了(这样称为粘包)。


还有可能出现上面这样的虽然收到了两个包,但是里面的内容却是互相包含,对于应用来说依然无法解析(拆包)。

对于这样的问题只能通过上层的应用来解决,常见的方式有:

  • 在报文末尾增加换行符表明一条完整的消息,这样在接收端可以根据这个换行符来判断消息是否完整。
  • 将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。
  • 规定好报文长度,不足的空位补齐,取的时候按照长度截取即可。

以上的这些方式我们在 Netty 的 pipline 中里加入对应的解码器都可以手动实现。

但其实 Netty 已经帮我们做好了,完全可以开箱即用。

比如:

  • LineBasedFrameDecoder 可以基于换行符解决。
  • DelimiterBasedFrameDecoder可基于分隔符解决。
  • FixedLengthFrameDecoder可指定长度解决。

字符串拆、粘包

下面来模拟一下最简单的字符串传输。

还是在之前的

https://github.com/crossoverJie/netty-action

进行演示。

在 Netty 客户端中加了一个入口可以循环发送 100 条字符串报文到接收端:

    /*** 向服务端发消息 字符串* @param stringReqVO* @return*/@ApiOperation("客户端发送消息,字符串")@RequestMapping(value = "sendStringMsg", method = RequestMethod.POST)@ResponseBodypublic BaseResponse<NULLBody> sendStringMsg(@RequestBody StringReqVO stringReqVO){BaseResponse<NULLBody> res = new BaseResponse();for (int i = 0; i < 100; i++) {heartbeatClient.sendStringMsg(stringReqVO.getMsg()) ;}// 利用 actuator 来自增counterService.increment(Constants.COUNTER_CLIENT_PUSH_COUNT);SendMsgResVO sendMsgResVO = new SendMsgResVO() ;sendMsgResVO.setMsg("OK") ;res.setCode(StatusEnum.SUCCESS.getCode()) ;res.setMessage(StatusEnum.SUCCESS.getMessage()) ;return res ;}/*** 发送消息字符串** @param msg*/public void sendStringMsg(String msg) {ByteBuf message = Unpooled.buffer(msg.getBytes().length) ;message.writeBytes(msg.getBytes()) ;ChannelFuture future = channel.writeAndFlush(message);future.addListener((ChannelFutureListener) channelFuture ->LOGGER.info("客户端手动发消息成功={}", msg));}

服务端直接打印即可:

    @Overrideprotected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {LOGGER.info("收到msg={}", msg);}

顺便提一下,这里加的有一个字符串的解码器:.addLast(new StringDecoder()) 其实就是把消息解析为字符串。

    @Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {out.add(msg.toString(charset));}

在 Swagger 中调用了客户端的接口用于给服务端发送了 100 次消息:

正常情况下接收端应该打印 100 次 hello 才对,但是查看日志会发现:

收到的内容有完整的、多的、少的、拼接的;这也就对应了上面提到的拆包、粘包。

该怎么解决呢?这便可采用之前提到的 LineBasedFrameDecoder 利用换行符解决。

利用 LineBasedFrameDecoder 解决问题

LineBasedFrameDecoder 解码器使用非常简单,只需要在 pipline 链条上添加即可。

//字符串解析,换行防拆包
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())

构造函数中传入了 1024 是指报的长度最大不超过这个值,具体可以看下文的源码分析。

然后我们再进行一次测试看看结果:

注意,由于 LineBasedFrameDecoder 解码器是通过换行符来判断的,所以在发送时,一条完整的消息需要加上 \n

最终的结果:

仔细观察日志,发现确实没有一条被拆、粘包。

LineBasedFrameDecoder 的原理

目的达到了,来看看它的实现原理:

  1. 第一步主要就是 findEndOfLine 方法去找到当前报文中是否存在分隔符,存在就会返回分隔符所在的位置。
  2. 判断是否需要丢弃,默认为 false ,第一次走这个逻辑(下文会判断是否需要改为 true)。
  3. 如果报文中存在换行符,就会将数据截取到那个位置。
  4. 如果不存在换行符(有可能是拆包、粘包),就看当前报文的长度是否大于预设的长度。大于则需要缓存这个报文长度,并将 discarding 设为 true。
  5. 如果是需要丢弃时,判断是否找到了换行符,存在则需要丢弃掉之前记录的长度然后截取数据。
  6. 如果没有找到换行符,则将之前缓存的报文长度进行累加,用于下次抛弃。

从这个逻辑中可以看出就是寻找报文中是否包含换行符,并进行相应的截取。

由于是通过缓冲区读取的,所以即使这次没有换行符的数据,只要下一次的报文存在换行符,上一轮的数据也不会丢。

高效的编码方式 Google Protocol

上面提到的其实就是在解码中进行操作,我们也可以自定义自己的拆、粘包工具。

编解码的主要目的就是为了可以编码成字节流用于在网络中传输、持久化存储。

Java 中也可以实现 Serializable 接口来实现序列化,但由于它性能等原因在一些 RPC 调用中用的很少。

Google Protocol 则是一个高效的序列化框架,下面来演示在 Netty 中如何使用。

安装

首先第一步自然是安装:

在官网下载对应的包。

本地配置环境变量:

当执行 protoc --version 出现以下结果表明安装成功:

定义自己的协议格式

接着是需要按照官方要求的语法定义自己的协议格式。

比如我这里需要定义一个输入输出的报文格式:

BaseRequestProto.proto:

syntax = "proto2";package protocol;option java_package = "com.crossoverjie.netty.action.protocol";
option java_outer_classname = "BaseRequestProto";message RequestProtocol {required int32 requestId = 2;required string reqMsg = 1;}

BaseResponseProto.proto:

syntax = "proto2";package protocol;option java_package = "com.crossoverjie.netty.action.protocol";
option java_outer_classname = "BaseResponseProto";message ResponseProtocol {required int32 responseId = 2;required string resMsg = 1;}

再通过

protoc --java_out=/dev BaseRequestProto.proto BaseResponseProto.proto

protoc 命令将刚才定义的协议格式转换为 Java 代码,并生成在 /dev 目录。

只需要将生成的代码拷贝到我们的项目中,同时引入依赖:

<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.4.0</version>
</dependency>

利用 Protocol 的编解码也非常简单:

public class ProtocolUtil {public static void main(String[] args) throws InvalidProtocolBufferException {BaseRequestProto.RequestProtocol protocol = BaseRequestProto.RequestProtocol.newBuilder().setRequestId(123).setReqMsg("你好啊").build();byte[] encode = encode(protocol);BaseRequestProto.RequestProtocol parseFrom = decode(encode);System.out.println(protocol.toString());System.out.println(protocol.toString().equals(parseFrom.toString()));}/*** 编码* @param protocol* @return*/public static byte[] encode(BaseRequestProto.RequestProtocol protocol){return protocol.toByteArray() ;}/*** 解码* @param bytes* @return* @throws InvalidProtocolBufferException*/public static BaseRequestProto.RequestProtocol decode(byte[] bytes) throws InvalidProtocolBufferException {return BaseRequestProto.RequestProtocol.parseFrom(bytes);}
}

利用 BaseRequestProto 来做一个演示,先编码再解码最后比较最终的结果是否相同。答案肯定是一致的。

利用 protoc 命令生成的 Java 文件里已经帮我们把编解码全部都封装好了,只需要简单调用就行了。

可以看出 Protocol 创建对象使用的是构建者模式,对使用者来说清晰易读,更多关于构建器的内容可以参考这里。

更多关于 Google Protocol 内容请查看官方开发文档。

结合 Netty

Netty 已经自带了对 Google protobuf 的编解码器,也是只需要在 pipline 中添加即可。

server 端:

// google Protobuf 编解码
.addLast(new ProtobufDecoder(BaseRequestProto.RequestProtocol.getDefaultInstance()))
.addLast(new ProtobufEncoder())

客户端:

// google Protobuf 编解码.addLast(new ProtobufDecoder(BaseResponseProto.ResponseProtocol.getDefaultInstance())).addLast(new ProtobufEncoder())

稍微注意的是,在构建 ProtobufDecoder 时需要显式指定解码器需要解码成什么类型。

我这里服务端接收的是 BaseRequestProto,客户端收到的是服务端响应的 BaseResponseProto 所以就设置了对应的实例。

同样的提供了一个接口向服务端发送消息,当服务端收到了一个特殊指令时也会向客户端返回内容:

    @Overrideprotected void channelRead0(ChannelHandlerContext ctx, BaseRequestProto.RequestProtocol msg) throws Exception {LOGGER.info("收到msg={}", msg.getReqMsg());if (999 == msg.getRequestId()){BaseResponseProto.ResponseProtocol responseProtocol = BaseResponseProto.ResponseProtocol.newBuilder().setResponseId(1000).setResMsg("服务端响应").build();ctx.writeAndFlush(responseProtocol) ;}}

在 swagger 中调用相关接口:

在日志可以看到服务端收到了消息,同时客户端也收到了返回:

虽说 Netty 封装了 Google Protobuf 相关的编解码工具,其实查看它的编码工具就会发现也是利用上文提到的 api 实现的。

Protocol 拆、粘包

Google Protocol 的使用确实非常简单,但还是有值的注意的地方,比如它依然会有拆、粘包问题。

不妨模拟一下:

连续发送 100 次消息看服务端收到的怎么样:

会发现服务端在解码的时候报错,其实就是被拆、粘包了。

这点 Netty 自然也考虑到了,所以已经提供了相关的工具。

//拆包解码
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufVarint32LengthFieldPrepender())

只需要在服务端和客户端加上这两个编解码工具即可,再来发送一百次试试。

查看日志发现没有出现一次异常,100 条信息全部都接收到了。

这个编解码工具可以简单理解为是在消息体中加了一个 32 位长度的整形字段,用于表明当前消息长度。

总结

网络这块同样是计算机的基础,由于近期在做相关的工作所以接触的比较多,也算是给大学补课了。

后面会接着更新 Netty 相关的内容,最后会产出一个高性能的 HTTP 以及 RPC 框架,敬请期待。

上文相关的代码:

https://github.com/crossoverJie/netty-action

号外

最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。

地址: https://github.com/crossoverJie/Java-Interview

欢迎关注公众号一起交流:

转载于:https://www.cnblogs.com/crossoverJie/p/9446410.html

Netty(三) 什么是 TCP 拆、粘包?如何解决?相关推荐

  1. Netty解决TCP的粘包和分包(二)

    2019独角兽企业重金招聘Python工程师标准>>> Netty解决TCP的粘包和分包(二) 使用LengthFieldBasedFrameDecoder解码器分包 先看一下这个类 ...

  2. 「 计算机网络 」TCP的粘包拆包问题

    「 计算机网络 」TCP的粘包/拆包问题 参考&鸣谢 大病初愈,一分钟看懂TCP粘包拆包 雷小帅 TCP 的粘包拆包以及解决方案 一乐说 文章目录 「 计算机网络 」TCP的粘包/拆包问题 一 ...

  3. TCP通信粘包问题分析和解决

    在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的 ...

  4. 面试题:聊聊TCP的粘包、拆包以及解决方案

    TCP的粘包和拆包问题往往出现在基于TCP协议的通讯中,比如RPC框架.Netty等.如果你的简历中写了类似的技术或者你所面试的公司使用了相关的技术,被问到该面试的几率会非常高. 今天这篇文章就带大家 ...

  5. netty 粘包的解决策略

    粘包问题的解决策略 由于底层的 TCP 无法理解上层业务数据,所以在底层是无法保证数据包不被拆分和重组的 , 这个问题只能通过上层的应用协议栈设计来解决,根据业界主流的协议的解决方案, 可以归纳如下: ...

  6. TCp传输粘包问题

    解决TCP网络传输"粘包"问题   当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API).TCP/IP传输层有两个并列的协 议:T ...

  7. socket Php 粘包,python3 tcp的粘包现象和解决办法解析

    这篇文章主要介绍了python3 tcp的粘包现象和解决办法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 服务器端 import socket ...

  8. 关于TCP的粘包问题

    简单的说就是通过TCP协议发送了多条独立的数据,但接收的时候,有些数据不幸的合并成了一个.比如客户端向服务器发送两个命令:"Start"."Parameter[x.x.x ...

  9. tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法

    tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法 参考文章: (1)tcp的粘包和拆包示例以及使用LengthFieldFrameDecoder来解决的方法 ( ...

最新文章

  1. 安装oracle到create inventory时卡住了怎么办_「推荐」wacom数位板怎么用?教你如何正确的安装数位板驱动...
  2. 044_Properties工具类
  3. python字符串类型_python字符串类型介绍
  4. spo2数据集_Arduino 血氧心率模块传感器数据采集
  5. Java中使用Jedis连接Redis对Set进行操作的常用命令
  6. 【CSON原创】HTML5游戏框架cnGameJS开发实录
  7. 稀疏数组与原始数组之间的转换
  8. windows系统登陆就注销如何解决,系统登陆就注销的解决办法
  9. 卡方分布分位数_卡方检验和精确概率法及两两比较
  10. 数据科学学习心得_学习数据科学
  11. java计算器 运算符优先级_跪求大神帮忙,怎样在java 计算器中实现,四则运算优先级;...
  12. Spring Boot文档阅读笔记-FileHandling解析及抓包分析
  13. html如何取单元格内容,JS获取表格内指定单元格html内容的方法
  14. spring配置定时器的时间设置
  15. C++设计模式10--命令模式(二)(Command)--降低请求发送者与接收者耦合
  16. Unity游戏开发——新发教你做游戏(二):60个Unity免费资源获取网站
  17. Cisco(61)——双出口策略路由PBR+NAT
  18. python123随机密码生成器_python密码生成器的3种方法
  19. c++语言设计五子棋游戏,C++实现五子棋游戏
  20. 浦东街镇医保卡制卡网点地址电话

热门文章

  1. 【转】 linux的网络接口之扫盲
  2. 第二节:Maven的运行机制
  3. hdu 2034 - 集合操作
  4. 【病毒】开机弹出“tlntsvi_6635.exe程序”解决方案
  5. Flutter RotatedBox 旋转组件
  6. 谈谈Javascript中那些For循环的事
  7. 01 安装ansible
  8. 使用 Pandas 的 to_excel() 方法来将多个 csv 文件合并到一个 xlsx 的不同 sheets 内
  9. C语言学习之插入排序
  10. Learn CMake's Scripting Language in 15 Minutes (ZZ)