概述
本文介绍什么是 TCP 粘包和拆包现象,并通过 Netty 编写详细的案例来重现 TCP 粘包问题,最后再通过一个 Netty 的 demo 来解决这个问题。具体内容如下
- 什么是 TCP 粘包和拆包现象
- 重现 TCP 粘包和拆包现象
- Netty 解决 TCP 粘包和拆包现象带来的问题
什么是 TCP 粘包和拆包现象
TCP 编程底层都有粘包和拆包机制,因为我们在 C/S 这种传输模型下,以 TCP 协议传输的时候,在网络中的 byte 其实就像是河水,TCP 就像一个搬运工,将这流水从一端转送到另一端,这时又分两种情况:
- 如果客户端的每次制造的水比较多,也就是我们常说的客户端给的包比较大,TCP 这个搬运工就会分多次去搬运
- 如果客户端每次制造的水比较少的话,TCP 可能会等客户端多次生产之后,把所有的水一起再运输到另一端
- 对于第一种情况,TCP 会再客户端先进行拆包,在另一端接收的时候,需要把多次获取的结果组合在一起,变成我们可以理解的信息
- 对于第二种情况,TCP 会在客户端先进行粘包,在另一端接收的时候,就必须进行拆包处理,因为每次接收的信息,可能是另一个远程端多次发送的包,被 TCP 粘在一起的
重现 TCP 粘包和拆包现象
- 通过在客户端 1 次发送超大数据包给服务器端来重现 TCP 拆包现象
- 通过在客户端分 10 次发送较小的数据包给服务器端来重现 TCP 粘包现象
下面通过 Netty 重现 TCP 粘包和拆包现象。
Netty maven 依赖
1 | <dependency> |
通过 Netty 重现 TCP 拆包现象
- Netty 客户端启动类:NettyClient
1 | package com.ckjava.test.client; |
- Netty 客户端通道处理类:NettyClientHandler
1 | package com.ckjava.test.client; |
其中关键的代码如下
1 | // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 |
- Netty 客户端启动类:NettyClient
1 | package com.ckjava.test.server; |
- Netty 服务器端通道处理类:NettyServer
1 | package com.ckjava.test.server; |
- 分别先启动服务器端后,再启动客户端,服务器端的输出如下
- 客户端的输出如下
1 | 17:03:24.474 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! |
- 从服务器端和客户端的输出结果来看:客户端只发送了 1 次数据,但是服务器端却收到了 3 次数据,说明 tcp 在客户端拆包后分 3 次发送了;并且客户端之后只收到了一次数据,说明服务器的回复数据在服务器端也出现了粘包现象,并且导致了数据无法区分的问题。
通过 Netty 重现 TCP 粘包现象
- 还用上面的例子,将客户端通道处理类:NettyClientHandler 中的 channelActive 方法修改成如下的方式
1 | // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 |
- 分别先启动服务器端后,再启动客户端,服务器端的输出如下
1 | 17:12:27.239 [nioEventLoopGroup-2-1] INFO com.ckjava.test.server.NettyServer - 服务器端启动成功,开放端口:8080 |
- 客户端的输出如下
1 | 17:12:36.917 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! |
- 从服务器端和客户端的输出结果来看:客户端只发送了 10 次数据,但是服务器端却收到了 1 次数据,说明 tcp 在客户端粘包后一次性发送了全部的数据。
Netty 解决 TCP 粘包和拆包现象带来的问题
TCP 粘包和拆包现象带来的问题
从上面的案例可以发现当出现 TCP 粘包和拆包现象后会出现下面的问题:
- tcp 在粘包的时候,数据混合后,接收方不能正确区分数据的头尾,如果是文件类型的数据,会导致文件破坏。
- tcp 在拆包的时候,数据拆分后,接收方不能正确区分数据的头尾,导致收到的消息错乱,影响语义。
如何解决 TCP 粘包和拆包现象带来的问题
由于 TCP 粘包和拆包现象会导致不能正确区分数据的头尾,那么解决的办法也挺简单的,通过 特殊字符串 来分隔消息体或者使用 定长消息 就能够正确区分数据的头尾。
目前的主流解决方式有以下几种:
- 使用定长消息,Client 和 Server 双方约定报文长度,Server 端接受到报文后,按指定长度解析;
- 使用特定分隔符,比如在消息尾部增加分隔符。Server 端接收到报文后,按照特定的分割符分割消息后,再解析;
- 将消息分割为消息头和消息体两部分,消息头中指定消息或者消息体的长度,通常设计中使用消息头第一个字段 int32 表示消息体的总长度;
Netty 中也提供了基于分隔符实现的半包解码器和定长的半包解码器:
- LineBasedFrameDecoder 使用”\n”和”\r\n”作为分割符的解码器
- DelimiterBasedFrameDecoder 使用自定义的分割符的解码器
- FixedLengthFrameDecoder 定长解码器
通过 Netty 的 DelimiterBasedFrameDecoder 解码器 来解决 TCP 粘包和拆包现象带来的问题
使用 DelimiterBasedFrameDecoder 可以确保收到的数据会自动通过 自定义的分隔符 进行分隔。发送的时候消息的后面只需要增加上 自定义的分隔符 即可。
- 基于上面的例子,服务器端 NettyServer 改动如下
1 | public void start() { |
- 服务器端 NettyServerHandler 改动如下
1 |
|
- 客户端 NettyClient 改动如下
1 | public static void main(String[] args) throws Exception { |
- 客户端 NettyClientHandler 中接收数据的部分不变,发送数据的地方改动如下
1 | // 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端 |
- 服务器端输出如下
1 | 18:14:33.627 [nioEventLoopGroup-2-1] INFO com.ckjava.test.server.NettyServer - 服务器端启动成功,开放端口:8080 |
- 客户端输出如下
1 | 18:14:50.056 [nioEventLoopGroup-2-1] INFO com.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功! |
- 从上面的例子可以看出 DelimiterBasedFrameDecoder 会帮自动帮我们把消息切割好,确保收到的数据都是基于 自定义分隔符 分隔好的数据,但是不要忘记在发送数据的时候添加上 自定义分隔符。