【Netty】TCP粘包/拆包解决方案

【Netty】TCP粘包/拆包解决方案TCP 作为传输层(4 层)协议,并不知道你的应用层(7 层)数据报文的含义,因此使用 TCP 传输数据时,可能会出现「粘包/拆包」,需要引用层通过特定的协议去解决。

1. 什么是 TCP 粘包/拆包?

「粘包/拆包」在 Socket 编程中经常会出现,使用 TCP 协议传输数据时,如果对端连续发送多个小的数据包,TCP 会将这些小的数据包打包,合并成一个 TCP 报文发送出去,这就是「粘包」。如果对端发送一个超大的数据包,TCP 会根据缓冲区的情况,将这个超大数据包拆分成多个小的 TCP 报文发送出去,这就是「拆包」。

拿 HTTP 服务举例,如果一次 HTTP 请求的报文过大,TCP 会进行拆包。因为 TCP 作为传输层协议,并不知道上层应用层 HTTP 的报文含义,它不知道单个报文的数据边界在哪里,而且一旦报文长度超过了双方约定的「可传输最大 TCP 报文长度」,就不得不拆包了。此时,服务端会先收到一个不完整的 HTTP 请求,如果服务端不做处理,那它要如何处理这个请求呢?服务端应该根据 HTTP 协议对请求报文和响应报文的规定,判断此次接收到的请求是否完整,如果是一个不完整的请求报文,就应该等待对端继续发送请求数据,等待读取到一个完整的请求时,再进行处理。

下图所示为粘包/拆包的场景:【Netty】TCP粘包/拆包解决方案

2. 为什么会导致粘包/拆包?

清楚了「粘包/拆包」的概念,顺带了解下可能会导致 TCP「粘包/拆包」的原因。

2.1 Nagle 算法

TCP 是面向连接的、可靠的、基于字节流的传输层协议。应用层交给 TCP 的数据,并不会以应用层的报文消息为单位进行传输,这些消息可能会被组合成一个数据段发送给目标主机。

Nagle 是一种通过减少数据包的方式来提高 TCP 传输效率的算法,因为网络带宽有限,如果频繁的发送小的数据包,对带宽的压力会比较大。Nagle 算法会在本地缓冲区先缓冲待发送的数据,待数据总量达到最大数据段(MSS)时,再一次性批量发送。这种方式虽然可能会使消息的发送存在延迟,但是对带宽的压力小,降低了网络拥堵的可能性并减少了额外的开销。

现在的网络资源不像几十年前那样紧张了,Linux 默认是关闭 Nagle 算法的,即 SO_NODELAY=1。

2.2 TCP_CORK

TCP 有一个选项TCP_CORK也可能会导致「粘包/拆包」。如果开启TCP_CORK,当发送的数据小于最大数据段(MSS)时,TCP 会延迟 20ms 发送,或者等待发送缓冲区的数据达到最大数据段(MSS)才真正发送。

2.1 MTU

最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。

指通信协议的最大传输单元,普遍使用的网卡 MTU 为 1500,即最大只能传输 1500 字节的数据帧。可以通过ifconfig命令查看各网卡的数据帧:【Netty】TCP粘包/拆包解决方案

2.2 MSS

最大报文段长度(MSS)是 TCP 协议的一个选项,用于在 TCP 连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度(不包括文段头)。

TCP 双方建立连接后会约定可传输的最大报文长度,是 TCP 用来限制应用层可发送的最大字节数。如果应用层单次发送的报文长度超过了 MSS,那么也会面临「拆包」。

3. 粘包/拆包的解决方案

常用的解决方案大致可分为三种:

  1. 数据报文定长,不足时自动填充。这种方式实现简单,但会浪费一定的带宽。
  2. 使用特定的分隔符(如换行符)将不同的报文进行分割。
  3. 在请求头中写入报文的长度。


针对这三种解决方案,Netty 都提供了开箱即用的解码器,使用非常的方便。【Netty】TCP粘包/拆包解决方案DelimiterBasedFrameDecoder 是一个可以自定义分隔符的解码器,Netty 只有当读到指定的分隔符时才会认为是一个完整的数据报文。

LineBasedFrameDecoder 是一个以「换行符」为分隔符的解码器。

FixedLengthFrameDecoder 是一个定长帧的解码器,你需要指定定长的帧大小,Netty 只有读到一个完整的桢时才会调用后续的 ChannelRead()。

LengthFieldBasedFrameDecoder 是一个将报文长度写入到请求头的解码器,Netty 会根据长度字段的偏移量和长度字段占用的字节数,读取到本次报文的长度,当读到一个完整的帧时才调用后续的 ChannelRead()。它的用法如下:

/**
 * @param maxFrameLength 最大帧大小
 * @param lengthFieldOffset 长度字段的偏移量
 * @param lengthFieldLength 长度字段占用的字节数,一般用int,4字节
 */

public LengthFieldBasedFrameDecoder(
        int maxFrameLength,
        int lengthFieldOffset, int lengthFieldLength)
 
{
    this(maxFrameLength, lengthFieldOffset, lengthFieldLength, 00);
}

4. 案例实战

由于 LengthFieldBasedFrameDecoder 比较常用,这里只演示这一个,其他解码器大家自行探索。

因为只是单纯的测试,我这里图方便,只编写一个 EmbeddedChannel 来测试,没有启 Netty 服务,案例如下:

// 读写半包 Demo
public class HalfDemo {
 public static void main(String[] args) {
  EmbeddedChannel channel = new EmbeddedChannel();
  // 最大帧1MB,0~4字节记录报文的长度
  channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024 * 102404));
  channel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf buf = (ByteBuf) msg;
    int length = buf.readInt();
    System.out.println("报文长度:" + length);
    System.out.println("收到数据:" + buf.toString(Charset.defaultCharset()));
   }
  });

  ByteBuf buf = Unpooled.buffer();
  // 写入报文的长度
  buf.writeInt(5);
  // 如果不写满5字节,控制台将不会有输出
  buf.writeBytes("hello".getBytes());
  channel.writeInbound(buf);
 }
}


原文始发于微信公众号(程序员小潘):【Netty】TCP粘包/拆包解决方案

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/29402.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!