tcp是个流协议,所谓流,就是没有界限的一串数据。tcp底层并不了解上层业务的具体含义,它会根据tcp缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被tcp拆分为多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送。这就是所谓的tcp拆包/粘包问题。
问题发生的原因有3个:
1,应用程序write写入的字节大小大于套接口发送缓冲区大小
2,进行MSS大小的tcp分段
3,以太网帧的payload大于MTU进行IP分片。
netty中发生粘包的例子
ctx.writeAndFlush(resp);
ctx.writeAndFlush(resp);
ctx.writeAndFlush(resp);
ctx.writeAndFlush(resp);
连续4个发送,其实是发送了4个包,对方应该把其看成是4个消息。但是因为发送的过快,对方几乎一定会把其当作一个包来处理。看成是发送了一条消息。这个就发生了粘包。
解决策略
由于底层的tcp无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。
1,消息定长,例如每个报文的大小固定为128个字节,如果不够,空位补空格。注意,这个补位是程序员补!
2,在包尾增加回车换行符进行分割,例如ftp协议
3,将消息分为消息头跟消息体,消息头中包含消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用Int32来表示消息的总长度。类似http协议
4,更为复杂的应用层协议,如自定义协议
在netty中,前两种协议已经实现。
bootstrap.group(workGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//设置特殊分割符,解决粘包问题
ByteBuf byteBuf = Unpooled.copiedBuffer("$$$".getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,byteBuf));
socketChannel.pipeline().addLast(new ClientHandler());
}
});
注意,以后所有发送的内容都要加我们设置的后缀才行。
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//设置定长分割符,解决粘包问题
socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(128));
socketChannel.pipeline().addLast(new ClientHandler());
}
});
注意,位数超了会把一个包拆成两个包。位数不够需要补位,这个补位需要程序员补。
netty使用半包解码器来解决拆包跟粘包问题。