概述
TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题
如图所示,假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
- 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
- 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包
- 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
粘包和拆包的解决方法
- 消息长度固定,累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息
- 将回车换行符作为消息结束符
- 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符
- 通过在消息头中定义长度字段来标识消息的总长度
- 自定义协议
自定义协议
通常很多时候netty自带的处理器不满足我们的需求,所以我们必须在TCP上实现我们自己的协议
通常协议的格式如下:
协议头 | 消息长度 | 消息体 |
---|
案例
// 协议实体
class Message {
private String token; // 令牌
private int headerData = 0XFF1903; // 协议的开始
private long length; // 消息体长度
private String message; // 消息体
// ... 还可以有其它属性
}
public class MessageEncoder extends MessageToByteEncoder<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
// 写入协议头
ctx.writeInt(msg.headerData);
// 写入消息体长度
ctx.writeLong(msg.length);
// 写入消息token,token固定32个长度
ctx.write(msg.token.toBytes());
// 写入主体
ctx.write(msg.message.toBytes());
// 推送写入的消息
ctx.flush();
}
}