前言
黏包 是指网络上有多条数据发送给服务端, 但是由于某种原因这些数据在被接受的时候进行了重新组合, 这就是黏包, 本篇文章用来演示一种最简单的黏包解析方法, 适用于初初初级选手
正常来讲客户端发送给服务端的消息, 都是存在专门的通讯协议的, 为了避免黏包现象, 我们通常有几种方式去制定相应的规则: 消息长度固定, 特定分隔符, 消息长度固定+特定分隔符等
本文是采用了 特定分隔符 的方式, 每条数据包都以 \n 结尾
例如以下三条原始数据数据: hell\n ningxuan\n thanks\n 变成了以下两个: hello\nningxuan\nth anks\n 这就是黏包 复制代码
黏包产生的原因
在 socket 网络编程中, TCP 和 UDP 分别是面向连接和非面相连接的. 但是他们都存在产生黏包问题吗?
本文不会对 tcp 和 udp 进行详细的讲解, 感兴趣的可以自行百度或者掘金
tcp
先说结论: tcp 会产生黏包问题
由于 tcp 协议本身的机制(面向连接的可靠性协议-三次握手机制) 客户端与服务端会维持一个连接(Channel), 数据在连接不断开的情况下, 可以将多个数据包持续不断的发送到服务器上.
但是如果发送的网络数据包太小, tcp就会启用Nagle算法对多个数据包进行合并再发送到服务器上. 这种情况下服务器在接收到消息的时候无法区分哪些数据包是分开的, 所以产生了黏包
还有一种可能是: 服务器在接收到数据之后, 将数据放入到缓冲区中, 如果消息没有被及时的从缓冲区取走, 下次在取数据的时候就会出现一次取到多个数据包的情况, 造成黏包现象
tcp三次握手:
- 客户端向服务端发送建立通道请求
- 服务端向客户端发送允许客户端建立一个单向的数据通道; 服务端向客户端发送建立通道请求
- 客户端向服务端发送允许服务端建立一个单向的数据通道
此时数据通道是双向的, 允许客户端、服务端互相发送消息
Nagle算法:
- 如果包长度达到 MSS, 则允许发送
- 如果该包中含有 FIN, 则允许发送
- 设置了 TCP_NODELAY 选项, 若所有发出去的小数据包(长度小于 MSS )均被确认, 则允许发送
- 若上述条件均未满足, 但发送了超时(一般为 200ms ), 则立即发送
udp
upd 不存在黏包问题
udp本身是无连接的不可靠传输协议, 不会对数据包进行合并发送, 也就没有Nagle算法, 不会存在数据合并的情况, 每一个数据包都是完整的, 所以不存在黏包现象
最简单的黏包解析
黏包解析也很简单:
- 遍历当前的 ByteBuffer 缓冲区
- 判断元素为 '\n' 的下标
- 生成新的 ByteBuffer 缓冲区
- 将起始下标到标记下标的字符写到新的缓冲区
具体代码如下所示:
public class ByteBufferTest { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(32); buffer.put("hello\nningxuan\nth".getBytes()); split(buffer); buffer.put("anks\n".getBytes()); split(buffer); } private static void split(ByteBuffer buffer){ // 将 buffer 切换为 读模式 buffer.flip(); // 根据 buffer 当前的长度进行遍历 for (int i = 0; i < buffer.limit(); i++) { // 判断当前下标元素是不是数据包切割符 \n if (buffer.get(i) == '\n'){ // 注意这个时候 buffer 的 position 属性一直为 0 // 计算当前数据包长度 int length = i + 1 - buffer.position(); // 根据当前数据包长度, 动态生成新的 缓冲区 ByteBuffer target = ByteBuffer.allocate(length); for (int j = 0; j < length; j++) { target.put(buffer.get()); // 注意这个时候 buffer 的 position 属性在 ++ } // 打印 target 当前的元素和属性 ByteBufferUtils.selectAll(target); } } buffer.compact(); } } 复制代码