网络通信时,如何解决粘包/半包、丢包或者包乱序的问题?

  • 如果是 TCP 协议,面向连接(经历三次握手和四次挥手)、传输可靠((保证数据正确性,保证数据顺序)),在大多数场景下,是不存在丢包和包乱序问题的,因为 TCP 通信是可靠通信方式,TCP 协议栈通过序列号和包重传应答确认机制保证数据包的有序和一定被正确发到目的地;
  • 如果是 UDP 协议,面向非连接、传输不可靠(丢包[数据丢失])。如果不能接受少量丢包,那就要自己在 UDP 的基础上实现类似 TCP 这种有序和可靠传输机制了(例如 RTP协议、RUDP 协议)。

所以,对于 TCP 协议来说,我们需要关注的是如何粘包/半包问题。

说明一下,半包:半包不是说只收到了全包的一半,而是说收到了全包的一部分,有时我们叫拆包。

一、TCP粘包/半包

1、什么是 TCP 粘包/半包问题?

应用A 通过网络发送数据向应用B 发送消息,大概会经过如下阶段:

  • 阶段一:应用A 把流数据发送到 TCP发送缓冲区。
  • 阶段二:TCP发送缓冲区把数据发送到达 B服务器 TCP接收缓冲区。
  • 阶段三:应用B 从 TCP接收缓冲区读取流数据。

假设应用A 分别发送了两个数据包 D1 和 D2 给应用B,由于应用B一次读取到的字节数是不确定的,故可能存在以下 4 种情况。

(1)应用B 分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包;

(2)应用B 一次接收到了两个数据包,D1 和 D2 粘合在一起,被称为 TCP 粘包;

(3)应用B 分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包;

(4)应用B 分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包,这被称为 TCP 拆包;

由于 TCP 协议本身的机制,它会存在 TCP 粘包/半包问题。

TCP发送数据原由:

  • 因为TCP本身传输的数据包大小就有限制,所以应用发出的消息包过大,TCP会把应用消息包拆分为多个TCP数据包发送出去。
  • Negal算法的优化,当应用发送数据包太小,TCP为了减少网络请求次数的开销,它会等待多个消息包一起,打成一个TCP数据包一次发送出去。

TCP接收方的原由:

  • 因为TCP缓冲区里的数据都是字符流的形式,没有明确的边界,因为数据没边界,所以应用从TCP缓冲区中读取数据时就没办法指定一个或几个消息一起读,而只能选择一次读取多大的数据流,而这个数据流中就可能包含着某个消息包的一部分数据。

1.1 发生的原因

粘包的主要原因:

  • 发送方每次写入数据 < 套接字(Socket)缓冲区大小
  • 接收方读取套接字(Socket)缓冲区数据不够及时

半包的主要原因:

  • 发送方每次写入数据 > 套接字(Socket)缓冲区大小
  • 发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),因此必须拆包。

其实我们可以换个角度看待问题:

  • 从收发的角度看,便是一个发送可能被多次接收,多个发送可能被一次接收。
  • 从传输的角度看,便是一个发送可能占用多个传输包,多个发送可能共用一个传输包。

根本原因,其实是:

  • TCP 是流式协议,消息无边界。
    (PS : UDP 虽然也可以一次传输多个包或者多次传输一个包,但每个消息都是有边界的,因此不会有粘包和半包问题。)

2、如何解决 TCP粘包半包问题

就像上面说的,UDP 之所以不会产生粘包和半包问题,主要是因为消息有边界,因此,我们也可以采取类似的思路。

解决问题的根本手段:找出消息的边界。

2.1 改成短连接

将 TCP 连接改成短连接,一个请求一个短连接。这样的话,建立连接到释放连接之间的消息即为传输的信息,消息也就产生了边界。

这样的方法就是十分简单,不需要在我们的应用中做过多修改。但缺点也就很明显了,效率低下,TCP 连接和断开都会涉及三次握手以及四次握手,每个消息都会涉及这些过程,十分浪费性能。

因此,并不推荐这种方式。

2.2 封装成帧

封装成帧(Framing),也就是原本发送消息的单位是缓冲大小,现在换成了帧,这样我们就可以自定义边界了。

一般有4种方式:

2.2.1 固定长度

这种方式下,消息边界也就是固定长度即可。

优点就是实现很简单,缺点就是空间有极大的浪费,如果传递的消息中大部分都比较短,这样就会有很多空间是浪费的。

因此,这种方式一般也是不推荐的。

2.2.2 分隔符

这种方式下,消息边界也就是分隔符本身。

优点是空间不再浪费,实现也比较简单。缺点是当内容本身出现分割符时需要转义,所以无论是发送还是接受,都需要进行整个内容的扫描。

因此,这种方式效率也不是很高,但可以尝试使用。

2.2.3 专门的 length 字段

这种方式,就有点类似 Http 请求中的 Content-Length,有一个专门的字段存储消息的长度。作为服务端,接受消息时,先解析固定长度的字段(length字段)获取消息总长度,然后读取后续内容。

优点是精确定位用户数据,内容也不用转义。缺点是长度理论上有限制,需要提前限制可能的最大长度从而定义长度占用字节数。

因此,十分推荐用这种方式。

2.2.4 其他方式

其他方式就各不相同了,比如 JSON 可以看成是使用{}是否成对。这些优缺点就需要大家在各自的场景中进行衡量了。

二、Netty 中的实现

Netty 中解决粘包半包

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

Netty 支持上面所讲的封装成帧(Framing)中的前三种方式,简单介绍下:

1、固定长度

FixedLengthFrameDecoder采用的是定长协议:即把固定的长度的字节数当做一个完整的消息。

FixedLengthFrameDecodert提供了以下构造方法:

    public FixedLengthFrameDecoder(int frameLength) {ObjectUtil.checkPositive(frameLength, "frameLength");this.frameLength = frameLength;}
  • frameLength参数:我们指定的消息长度。

注意:FixedLengthFrameDecoder并没有提供一个对应的编码器,因为接收方只需要根据字节数进行判断即可,发送方无需编码。

例如:我们规定每个报文的大小为固定长度 5个字节,表示一个有效报文,如果不够,空位补空格;

1)服务端

            bootstrap.group(bossGroup, workerGroup)....childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5)) //固定长度 5个字节.addLast(new FixedLengthServerHandler());}});
 @Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 获取客户端发送过来的消息ByteBuf in = (ByteBuf) msg;String request = in.toString(CharsetUtil.UTF_8);System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());ctx.writeAndFlush(Unpooled.copiedBuffer("Welcome to Netty!".getBytes()));}

2)客户端

            bootstrap.group(eventExecutors)/*将线程组传入*/....handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(5)) //固定长度 5个字节.addLast(new FixedLengthClientHandler());}});
 @Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {// 接收服务端发送过来的消息System.out.println("client Accept[" + msg.toString(CharsetUtil.UTF_8) + "] and the counter is:" + counter.incrementAndGet());}/*** 客户端被通知channel活跃后 channel活跃后,做业务处理*/@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 发送消息到服务端ByteBuf in1 = Unpooled.buffer().writeBytes("CHARGE".getBytes());ByteBuf in2 = Unpooled.buffer().writeBytes(" and ".getBytes());ByteBuf in3 = Unpooled.buffer().writeBytes("ABCDEFGH".getBytes());ctx.writeAndFlush(in1);ctx.writeAndFlush(in2);ctx.writeAndFlush(in3);}

3)先启动服务端,再启动客户端,结果如下:

2、分隔符

在包尾增加分割符,比如回车换行符进行分割,例如 FTP 协议;

2.1 回车换行符进行分割

LineBasedFrameDecoder采用的通信协议格式非常简单:使用换行符\n或者\r\n作为依据,遇到\n或者\r\n都认为是一条完整的消息。

LineBasedFrameDecoder提供了2个构造方法,如下:

    public LineBasedFrameDecoder(int maxLength) {this(maxLength, true, false);}public LineBasedFrameDecoder(int maxLength, boolean stripDelimiter, boolean failFast) {this.maxLength = maxLength;this.failFast = failFast;this.stripDelimiter = stripDelimiter;}

其中:

  • maxLength:
    表示一行最大的长度,如果超过这个长度依然没有检测到\n或者\r\n,将会抛出TooLongFrameException
  • failFast:
    与maxLength联合使用,表示超过maxLength后,抛出TooLongFrameException的时机。如果为true,则超出maxLength后立即抛出TooLongFrameException,不继续进行解码;如果为false,则等到完整的消息被解码后,再抛出TooLongFrameException异常。
  • stripDelimiter:
    解码后的消息是否去除\n,\r\n分隔符。

1)服务端

            bootstrap.group(bossGroup, workerGroup)/*将线程组传入*/....childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {//回车换行符socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024)).addLast(new LineBaseServerHandler());}});
 @Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 获取客户端发送过来的消息ByteBuf in = (ByteBuf) msg;String request = in.toString(CharsetUtil.UTF_8);System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());String resp = "Hello," + request + ". Welcome to Netty World!" + System.getProperty("line.separator");ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));}

2)客户端

            bootstrap.group(eventExecutors)/*将线程组传入*/....handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {//回车换行符socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024)).addLast(new LineBaseClientHandler());}});
 @Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 发送消息到服务端ByteBuf msg = null;String request = "charge LineBasedFrameDecoder(1024),回车换行符" + System.getProperty("line.separator");for (int i = 0; i < 5; i++) {msg = Unpooled.buffer(request.length());msg.writeBytes(request.getBytes());ctx.writeAndFlush(msg);}}

3)先启动服务端,再启动客户端,结果如下:

2.2 自定义分割符

DelimiterBasedFrameDecoder是一个分隔符解码器。我们可以自定义消息分隔符。

DelimiterBasedFrameDecoder提供了多个构造方法,比如下面两个

    public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {this(maxFrameLength, true, delimiter);}public DelimiterBasedFrameDecoder(int maxFrameLength, boolean stripDelimiter, ByteBuf delimiter) {this(maxFrameLength, stripDelimiter, true, delimiter);}

其中:

  • maxFrameLength:
    表示一行最大的长度,可以同时接受多个分隔符,
    如果长度超过1024(可以指定),并且没有找到分隔符,则会抛异常。
    如果长度小于1024,并且没有找到分隔符,会缓存收到的消息,直到接收到分隔符,或者超出1024抛异常。
    同时存在多个分隔符时,优先匹配长度最短的分隔符,如果一样长,则哪个先出现,匹配哪个。
  • stripDelimiter:
    解码后的消息是否去除分隔符。
  • delimiter
    我们自定义分隔符

1)服务端

public class DelimiterEchoServer {public static final String DELIMITER_SYMBOL = "@~@";public static final int PORT = 19997;public static void main(String[] args) throws InterruptedException {DelimiterEchoServer delimiterEchoServer = new DelimiterEchoServer();System.out.println("服务器即将启动");delimiterEchoServer.start();}public void start() throws InterruptedException {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap bootstrap = new ServerBootstrap();/*服务端启动必须*/bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(PORT)).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {// 自定义分割符ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER_SYMBOL.getBytes());socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter)).addLast(new DelimiterServerHandler());}});System.out.println("MyServer 服务端已经准备就绪...");ChannelFuture channelFuture = bootstrap.bind().sync();System.out.println("服务器启动完成,等待客户端的连接和数据...");channelFuture.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}}
/*** 服务端读取到网络数据后,做业务处理** @param ctx* @param msg* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {// 获取客户端发送过来的消息ByteBuf in = (ByteBuf) msg;String request = in.toString(CharsetUtil.UTF_8);System.out.println("Server Accept[" + request + "] and the counter is:" + counter.incrementAndGet());String resp = "Hello," + request + ". Welcome to Netty World!" + DelimiterEchoServer.DELIMITER_SYMBOL;ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));// ctx.close();}/*** 服务端读取完成网络数据后,做业务处理* @param ctx* @throws Exception*/@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {// 发送消息给客户端System.out.println("channelReadComplete------");// ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);}

2)客户端

            bootstrap.group(eventExecutors)/*将线程组传入*/....handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel socketChannel) throws Exception {//自定义分割符ByteBuf delimiter = Unpooled.copiedBuffer(DelimiterEchoServer.DELIMITER_SYMBOL.getBytes());socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter)).addLast(new DelimiterClientHandler());}});
 @Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 发送消息到服务端ByteBuf msg = null;String request = "charge 自定义分割符 \\@\\~\\@ , 发送数据。" + DelimiterEchoServer.DELIMITER_SYMBOL;for (int i = 0; i < 10; i++) {msg = Unpooled.buffer(request.length());msg.writeBytes(request.getBytes());ctx.writeAndFlush(msg);System.out.println("发送数据到服务器");}}

3)先启动服务端,再启动客户端,结果如下:

3、专门的 length 字段

专门的 length 字段:将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)
的字段,通常设计思路为消息头的第一个字段使用 int32 来表示消息的总长度,使用
LengthFieldBasedFrameDecoder,后面再了解使用它。

上面几个都有一个共同的父类 ByteToMessageDecoder 解码器

到此,Netty 解决TCP粘包/半包使用有所了解,解码的更多逻辑可以查看源码。

参考文章:

  • TCP 粘包和半包 介绍及解决(上):https://network.51cto.com/article/604760.html
  • TCP拆包、半包、粘包:https://zhuanlan.zhihu.com/p/126279630

– 求知若饥,虚心若愚。

Netty 解决TCP粘包/半包使用相关推荐

  1. Netty解决TCP粘包/拆包导致的半包读写问题

    一.TCP粘包/拆包问题说明 TCP是个"流"协议,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包拆分,所以在业务上认为,一 ...

  2. netty解决TCP粘包/拆包导致的半包读写问题的三种方案

    解决方案一:LineBasedFrameDecoder+StringDecoder来解决TCP的粘包/拆包问题 只需要在客户端和服务端加上45.46两行代码并且在发送消息的时候加上换行符即可解决TCP ...

  3. TCP 粘包半包 netty 编解码 三者关系

    1 何为粘包 / 半包? 对方一次性接收了多条消息这种现象,我们就称之为 粘包现象. 对方多次接收了不完整消息这种现象,我们就称之为 半包现象. 粘包的原因: 发送方发送的消息 < 缓冲区大小 ...

  4. Netty学习总结(5)——Netty之TCP粘包/拆包问题的解决之道

    无论是服务端还是客户端,读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议. 流:没有界限的一串数据.如同河里的流水,它们是连成 ...

  5. Netty如何解决粘包半包问题

    何为粘包 / 半包? 比如,我们发送两条消息:ABC 和 DEF,那么对方收到的就一定是 ABC 和 DEF 吗? 不一定,对方可能一次就把两条消息接收完了,即 ABCDEF:也可能分成了好多次,比如 ...

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

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

  7. Netty框架之TCP粘包/半包解决方案

    Netty框架之TCP粘包/半包解决方案 一.TCP粘包 二.TCP半包 三.TCP粘包/半包解决方案 1.FixedLengthFrameDecoder定长解析器 2.LineBasedFrameD ...

  8. 网络:什么是TCP粘包/半包?怎么解决这个问题

    在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接.在TCP的socket编程中,发送端和接收端都有成对的socke ...

  9. 三、Netty的粘包半包问题解决

    一.定义 TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生.客户端给服务端发送了两条消息ABC和DEF,服务端这边的接收会有多少种情况呢?有可能是一次 ...

  10. Netty粘包/半包问题解析

    目录 一.什么是粘包/半包问题 二.TCP粘包/半包发生的原因 三.粘包/半包解决办法 四.Netty中粘包/半包解决示例 1. 采用固定长度数据包编解码方式 2. 采用特殊字符作为边界字符编解码方式 ...

最新文章

  1. 为什么像王者荣耀这样的游戏 Server 不愿意使用微服务?
  2. 运维管理工具+chef+puppet+ansible+SaltStack
  3. 怎么查看ubuntu是多少位和常用信息
  4. KingPaper初探ThinkPHP3.1.2之扩展函数库和类库的使用(四)
  5. 开源硬件_扩展对开源硬件的访问
  6. iOS开发应用结构化资源储备
  7. 想要把日志在两个blog同步太过麻烦了
  8. org manual翻译--3.6 Org-Plot
  9. python 抓取页面内容_用Python程序抓取网页的HTML信息的一个小实例
  10. 并发编程学习之CopyOnWriteArraySet
  11. 联想笔记本电脑键盘灯怎么开启_win10系统笔记本电脑开启/关闭键盘背光灯的方法...
  12. 2022年最新iOS面试题(附答案)
  13. Ubuntu14.04/16.04安装Dukto
  14. c语言:输入三角形的边长求面积
  15. Windows Vista 和 Windows Server 2008 在内存管理方面的功能增强(翻译)
  16. 重度办公用户,哪款平板更合适?
  17. JAVA_反射机制(照镜子)
  18. TP5做工资条群发系统
  19. 2022-2028年中国艾灸市场研究及前瞻分析报告
  20. 职场PUA,管理者的五宗罪

热门文章

  1. 当前流行的J2EE WEB应用架构分析(一)
  2. 许可证加密的WMV文件破解
  3. U盘 量产记录(俩盘符合并为一个盘符)
  4. 由于应用程序配置不正确,未能启动此应用程序
  5. CSAPP 大作业 程序人生
  6. 悬崖帝国中文版下载|悬崖帝国中文破解版下载 v1.0绿色免安装版
  7. 无线传感器网络MAC协议(下)
  8. echarts 柱状图设置边框_echarts柱状图
  9. SVN忽略文件和文件夹
  10. MTPA仿真实现和一些比较