1. 粘包和拆包的原因
TCP是个流协议,就是没有界限的一串数据。TCP底层并不了解上层的业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包划分所以业务上认为一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包。这就是TCP的粘包和拆包问题。(HTTP为什么没)
虽然HTTP也是用了TCP传输机制,但如果利用tcp每次发送数据就与对方建立连接然后双方发送玩一段数据后挥手断开这样就不会出现如HTTP。因为数据发送&接受完成主动关闭连接,或者文件存储这样发送方只管发送即可如FTP。
总结来看原因如下:
1.应用程序write写入的字节大小大于套接口发送缓存区大小。
2. 进行MSS大小的TCP分段???
3.以太网的payload大于MTU进行分片???
解决之策
1. 消息定长
2.在包尾增加回车换行符进行分割如FTP
3. 将消息分为消息头和消息体,消息头包含消息的总长度
Netty解决之道
LineBaseFrameDecoder() + StringDecoder()
.childHandler(new ChildChannelHandler(){
protected void initChannel(Socket args){
args.pipline().addLast(new LineBaseFrameDecoder(1024));
args.pipline().addLast(new StringDecoder(1024));
}
})
LineBaseFrameDecoder的工作原理是是一次遍历ByteBuf中的可读字节判断是否有换行符"\n"或者"\r\n",并以此位置为结束位置。StringDecoder()功能就是将接受的对象转换成字符串。如果不是换行符的可以考虑多种Netty的TCP粘包/拆包解码器。如分隔符解码器
RocketMQ解决之道
RocketMQ主要是使用了 LengFieldBasedFrameDecoder 解码器。它的报文构造如下
编码
@Override
public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out)
throws Exception {
try {
ByteBuffer header = remotingCommand.encodeHeader();
out.writeBytes(header);
byte[] body = remotingCommand.getBody();
if (body != null) {
out.writeBytes(body);
}
} catch (Exception e) {
}
}
public ByteBuffer encodeHeader(final int bodyLength) {
// 1> header length size
int length = 4;
// 2> header data length
byte[] headerData;
headerData = this.headerEncode();
length += headerData.length;
// 3> body data length
length += bodyLength;
ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength);
// length 在Netty解码器时识别整个报文段的长度=4+h.length+b.length
result.putInt(length);
// header length
result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));
// header data
result.put(headerData);
result.flip();
return result;
}
public static byte[] markProtocolType(int source, SerializeType type) {
// 4B
byte[] result = new byte[4];
// 组合
result[0] = type.getCode(); // 第一个字节方类型
result[1] = (byte) ((source >> 16) & 0xFF); // 依次与运算后移
result[2] = (byte) ((source >> 8) & 0xFF);
result[3] = (byte) (source & 0xFF);
return result;
}
解码
public static RemotingCommand decode(final ByteBuffer byteBuffer) {
int length = byteBuffer.limit();
// 拿到组合的头长度
int oriHeaderLen = byteBuffer.getInt();
// 开始反解 & 0xFFFFFF 拿到低24位
int headerLength = getHeaderLength(oriHeaderLen);
// get()头文件
byte[] headerData = new byte[headerLength];
byteBuffer.get(headerData);
RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));
// get Body长度
int bodyLength = length - 4 - headerLength;
byte[] bodyData = null;
if (bodyLength > 0) {
bodyData = new byte[bodyLength];
byteBuffer.get(bodyData);
}
cmd.body = bodyData;
return cmd;
}
Dubbo解决之道
Dubbo是通过自己控制编解码协议来解决粘包拆包问题,使用方式和LengFieldBasedFrameDecoder相似.
看一下Dubbo协议
Dubbo会对每次TCP进入的数据进行验证,读取12-16字节即消息总长度后和读取的数据流进行比较如果大于则继续等待下一个数据包的到来再次判断,否则未发生拆包下开始读取并记录读取的位置此时如果读取吓一跳报文的时候数据不够即发生了粘包,那么写下读取的位置并跳出循环等待下一个数据包的到来。
有个问题,如果在TCP数据传输的过程发生包丢失而又重新发生怎么办?