深入理解拆包/粘包
编解码技术是实现网络通信的根本,从这篇文章开始,大明哥将用三篇文章来彻底说清楚 Netty 的编解码技术。其实在 死磕 Java NIO 一文 【死磕 Java NIO】— 消息边界的处理 中大明哥就已深入分析了在 Java NIO 消息边界的问题以及相对应的解决方案,下面我们来看 Netty 的吧。
现象演示
首先我们需要知道什么是拆包/粘包现象。
假设客户端向服务端发送两个数据包,分别为 package1 和 package2,这个时候服务端接收到客户端的数据有可能有如下四种情况。
- 情况 1 :服务端正常接收 package1 和 package 2,这种属于正常情况。
- 情况 2:服务端只接收到了一个 package,由于 TCP 保证送达的特性,所以这个 package 包含了客户端发送的两个 package ,这种现象属于粘包现象。如果客户端和服务端没有对应的协议来明确 package1 和 package2 的界限,那么服务端是无法区分 package1 和 package2 的。
- 情况 3 :服务端可能会接收到 3 个 package,package1 可能会被拆分为 package1.1 和 package1.2 ,这种现象属于拆包现象。
- 情况 4 :服务端接收到 2 个 package,但是这两个 package 都不为完整的,比如 package1 拆分成了 package1.1 和 package1.2 ,但是服务端接收的两个包为 package1.1 和 package1.2 + package2,这种情况是拆包和粘包的综合体。
下面大明哥通过两个例子来分别阐述 Netty 中的拆包/粘包现象。
粘包现象
从上面图中可以看出,拆包其实就是多个数据包合并成一个。所以我们只需要在客户端发送多个消息给服务端,看服务端是否是接收多次就可以了。
- 服务端代码
public class StickyServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
})
.bind(8081);
}
}
服务端没有多余的代码,只有一个 LoggingHandler 的 ChannelHandler,该 Handler 主要是用于打印服务端的日志情况。
- 客户端代码
public class StickyClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端连接成功,开始发送数据");
for (int i = 0 ; i < 10 ; i++) {
ByteBuf byteBuf = ctx.alloc().buffer(16);
byteBuf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.channel().writeAndFlush(byteBuf);
}
log.info("数据已发送完成");
}
});
}
}).connect("127.0.0.1",8081);
}
}
客户端与服务端建立连接后,就向服务端发送 10 次消息,每次 16 byte。
- 服务端日志
从服务端的运行日志中我们可以看出,客户端虽然发了 10 次,但是服务端只接收了一次,一次 160 byte,充分展示了粘包情况。
拆包现象
拆包就和粘包相反,它是将一个数据包拆分为多个数据包。
- 服务端
public class UnpackServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务端接收的内容:" + msg.toString());
}
});
}
})
.bind(8081);
}
}
服务端就单纯地将客户端发送过来的消息打印即可。
- 客户端
public class UnpackClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0 ; i < 500 ; i++) {
ctx.channel().writeAndFlush("大家好,我是大明哥,一个专注[死磕 Java] 的男人!!!\r\n");
}
}
});
}
}).connect("127.0.0.1",8081);
}
}
客户端建立连接后,向服务端发送 500 次消息“大家好,我是大明哥,一个专注[死磕 Java] 的男人!!!\r\n”
- 运行结果
运行结果中有一段是乱码,消息也不是完整的,所以这里一定是不完整的,发生了拆包现象。
为什么会有拆包/粘包
客户端消息明明是一条一条地发,为什么会有拆包/粘包情况呢?TCP 是传输层协议,它并不了解我们应用层业务数据的含义,它会根据实际情况对数据包进行划分。所以在业务上我们认为是一个完整的数据包,可能会被 TCP 拆分为多个数据库报进行发送,也有可能会将多个数据包合并成一个数据包发送,这就会出现拆包/粘包的问题。
在网络通信的过程中,影响可以发送的数据包大小受很多因素限制,比如 MTU 传输单元大小、MSS 最大报文长度、滑动窗口。同时 TCP 也采用了 Nagle 算法对网络数据包进行了优化,所以要了解 TCP 为什么会有拆包/粘包问题,就需要了解这些概念。
MTU 最大传输单元
MTU(Maximum Transmission Unit),最大传输单元,是指网络能够传输的最大数据包大小,它决定了发送端一次能够发送报文的最大字节数。它是链路层协议,其最大值默认为 1500 byte。
MTU 是数据链路层对网络层的限制,最小为 46 byte,最大为 1500 byte,意思就是说网络层必须将发给网卡 API 的数据包大小控制在 1500 byte 以下,否则失败。
那为什么要有一个这样的限制呢?我们都知道网络中通常是以数据包为单位进行信息传输的,那么一次传输多大的数据包就决定了整个传输过程中的效率了,理论上数据包的大小设置为尽可能大,因为着有效的数据量就越大,传输的效率也就越高,但是传输一个数据包的延迟就会很大,而且数据包中 bite 位发送错误的概率也就越大,如果这个数据包丢失了,那么重传的代价就会很大。但是如果我们将数据包大小设置较小,那么我们传输的有效数据就会很小,传输效率就会比较低。所以我们就需要 MTU 来控制网络上传输数据包的大小,如果数据包大,我们就将其拆分,如果小,我们就把几个数据包进行合并,从而提供传输效率。
MSS 最大报文长度
MSS(Maximum Segment Size),最大报文长度,它表示 TCP payload 的最大值,它是 TCP 用来限制应用层发送的最大字节数。
我们知道了 MTU 限定了网络层往数据链路层发送数据包的大小,如果网络层发现一个数据包大于 MTU,那么它需要将其进行分片,切割成小于 MTU 的数据包,再将其发送到数据链路层。
一台主机上的所有应用都将数据包发往网络层,如果这些数据包太大了,则需要对其进行分片,但是这么多数据包都交给网络层来分片,是不是降低了效率?作为网络层,它的理想状态是,让 TCP 来的每一个数据包,大小都小于 MTU,这样它就不需要分片了。
MSS 是 TCP 协议定义的一个选项,是 TCP 用来限定应用层最大的发送字节数。它是在 TCP 连接建立时,双方进行约定的。当一个 TCP 连接建立时,连接的双方都需要通告各自的 MSS,以避免分片。
TCP 建立连接时,双方都需要根据 MTU 来计算各自的 MSS,计算规则如下:
MTU = IP Header(20) + TCP Header(20) + Data,MTU 默认最大值为 1500,所以 TCP 的有效数据 Data 的最大值为 1500 - 20 - 20 = 1460 ,这个值就是 MSS 的值。
MSS 的值是通过三次握手的方式告知对方的,互相确认对方的 MSS 值大小,取较小的那个作为 MSS。
- 客户端在发送的 SYN 报文中携带自己的 MSS(1300)。
- 服务端接收该报文后,取客户端的 MSS(1300) 和自己本地的 MSS (1200)中较小的那个值作为自己的 MSS(1200)。在回复的 SYN-ACK 中也携带自己的 MSS(1200)。
- 客户端收到该 SYN-ACK 后,取服务端的 MSS(1200)和自己本地的 MSS(1200)中较小的那个值作为客户端的 MSS(1200)。
Nagle 算法
Nagle 算法于 1984 年被福特航空和通信公司定义为 TCP/IP 拥塞控制方法。它主要用于解决频繁发送小数据包而带来的网络拥塞问题。
为了尽可能地利用网络带宽,TCP 总是希望能够发送足够大的数据包,由于有 MSS 的控制,所以它总是希望每次都能够以 MSS 的尺寸来发送数据。但是我们需要发送的数据并不会每次都有那么多字节,怎么办?攒着。Nagle 算法会在数据为得到确认之前会先将其写入到缓冲区中,等待数据确认或者缓冲区积攒到一定大小再把数据包发送出去。
Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle 能够有效地降低网络开销,但是它会有一定的延时性,如果我们的业务系统对时延要求比较高的话,希望发出去的消息都能够尽快地响应,这个时候我们就需要关闭 Nagle 算法了。Netty 为了使数据传输延迟最小化,所以就默认禁用了 Nagle 算法。
在 Netty 中可以通过参数 ChannelOption.TCP_NODELAY
来开启和关闭 Nagle 算法。
总结
本篇大明哥详细分析了拆包/粘包问题,并分析了产生拆包/粘包的核心原因,我相信小伙伴一定有所收获。当然对于一些计算机网络方面的知识点,大明哥都是浅尝遏止,并没有深入,因为本系列的重点并不是计算机网络,而是 Netty,所以小伙伴如果对 MTU、MSS以及 Nagle 算法有兴趣的话,可以更加深入下,下面是大明哥在网上找了一些不错的文章,就分析给各位小伙伴了。
- 什么是MTU?为什么MTU值普遍都是1500?:https://developer.aliyun.com/article/222535
- 什么是MTU(Maximum Transmission Unit)?:https://info.support.huawei.com/info-finder/encyclopedia/zh/MTU.html
- 什么是最大传输单元 ( MTU):https://blog.51cto.com/u_15127565/468549
- TCP/IP协议:最大报文段长度(MSS)是如何确定的:https://blog.csdn.net/xiaofei0859/article/details/51051999
- MTU TCP-MSS详解:https://zhuanlan.zhihu.com/p/139537936
- 深入浅出TCPIP之Nagle算法:https://cloud.tencent.com/developer/article/1784570
- TCP-IP详解:Nagle算法:https://blog.csdn.net/wdscq1234/article/details/52432095
- Nagle算法:https://www.jianshu.com/p/bcdbfe641e81
- 点赞
- 收藏
- 关注作者
评论(0)