1 什么是粘包和半包?
粘包问题
是指数据在传输时,在一条消息中读取到了另一条消息的部分数据
,这种现象就叫做粘包
。
半包问题
是指接收端只收到了部分数据,而非完整的数据的情况就叫做半包
。
2 为什么 TCP 应用中会出现粘包和半包现象?
2.1 粘包的主要原因
- 发送方每次写入数据 < 套接字缓冲区大小;
由于 TCP 协议本身的机制(面向连接的可靠的协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用
Nagle 算法
对较小的数据包进行合并,然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包。 - 接收方
读取
套接字缓冲区数据不够及时
。服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象。
2.2 半包的主要原因
- 发送方写入数据 > 套接字缓冲区大小;
- 进行 MSS 大小的 TCP 分段。
MSS=TCP 报文段长度-TCP 首部长度
- 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包。
2.3 换个角度看
- 收发:一个发送可能被多次接收,多个发送可能被一次接收。
- 传输:一个发送可能占用多个传输包,多个发送可能公用一个传输包。
2.4 根本原因
TCP 是流式协议,消息无边界。
Tips: UDP本身作为无连接的不可靠的传输协议,
不会
对数据包进行合并发送
(也就没有 Nagle 算法之说了),它直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的
(数据+UDP 头+IP 头等等,即每个数据包均有界),也就没有粘包一说了。
2.5 示例
2.5.1 服务端
/**
* 服务器端(只负责接收消息)
*/
class ServSocket {
// 字节数组的长度
private static final int BYTE_LENGTH = 20;
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(8888);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 得到客户端发送的流对象
try (InputStream inputStream = clientSocket.getInputStream()) {
while (true) {
// 循环获取客户端发送的信息
byte[] bytes = new byte[BYTE_LENGTH];
// 读取客户端发送的信息
int count = inputStream.read(bytes, 0, BYTE_LENGTH);
if (count > 0) {
// 成功接收到有效消息并打印
System.out.println("接收到客户端的信息是:" + new String(bytes));
}
count = 0;
}
}
}
}
2.5.2 客户端
/**
* 客户端(只负责发送消息)
*/
static class ClientSocket {
public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 8888);
// 发送的消息内容
final String message = "Hi,Java.";
// 使用输出流发送消息
try (OutputStream outputStream = socket.getOutputStream()) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 发送消息
outputStream.write(message.getBytes());
}
}
}
}
2.5.3 结果
3 解决粘包和半包问题的几种常用方法
解决问题的根本手段:找出消息的边界。
3.1 消息定长
发送方和接收方固定发送数据的大小,当字符长度不够
时用空字符
弥补,有了固定大小之后就知道每条消息的具体边界了,这样就没有粘包的问题了。
3.2 分隔符
以特殊的字符结尾,比如以“\n”结尾,这样我们就知道数据的具体边界了,从而避免了粘包问题。
3.3 消息头+消息体
在 TCP 协议的基础上封装一层自定义数据协议
,在自定义数据协议中,包含数据头(存储数据的大小)和 数据的具体内容,这样服务端得到数据之后,通过解析数据头就可以知道数据的具体长度了,也就没有粘包的问题了。
4 Netty 对三种常用封帧方式的支持
方式 | 解码 | 编码 |
---|---|---|
固定长度 | FixedLengthFrameDecoder | 简单 |
分割符 | DelimiterBasedFrameDecoder, LineBasedFrameDecoder(换行符) | 简单 |
固定长度字段存个内容的长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
下一章,具体了解下Netty的编解码技术。