【前言】针对TCP底层网络通信设计时,当在接收或者发送数据消息时,都需要考虑TCP粘包或者拆包的问题。即可以认为TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,因此一个完整的数据包可能被被TCP拆分成多个包进行发送,也可能把若干个小的数据包封装成一个大的包发送,从而导致数据接收的不完整问题,这就是TCP粘包和拆包问题。
【问题说明】TCP粘包拆包问题图解
如图1,D1,D2两个数据包,服务端分两次接收到数据包,没有发送粘包和拆包;
图2中D1、D2连在一起,服务端一起接收到数据包,发送了粘包;
图3中发生了拆包,服务端分两次接收到数据包,第一次读取到D1数据包的一部分D1-1,第二次读取到数据包D1的第二部分D1-2和数据包D2
【解决策略】TCP以流的方式进行数据传输,上层的应用协议对消息进行区分,经常采用下面几种方式
1.消息长度固定:累计读取到长度总长为定长Len的报文后,就可以认为读取到一个完整的消息,从而可以将计数器置位进行下一个数据报的读取;
2.消息结束符的设置,如将回车换行符或特殊的分隔符作为消息结束符
3.在消息头中定义消息长度字段来标识消息的总长度
针对以上应用做了统一的抽象,netty提供了工具类来解决对应的问题。使用者不需要对自己读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包问题。
下面给出相应的代码示例,如基于消息长度字段的定长消息解码器
系统在未加入该解码器时:
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
//服务端返回应答消息时,该方法被调用
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
//消息处理线程
}
}
以上代码可以看到,在代码中并没有考虑读半包问题。这在功能测试时往往没有问题,但当系统请求压力过大或者发送大报文时,就可能出现粘包/拆包的问题,从而导致消息解码出错,程序就不能达到我们预期的效果。
系统在加入该解码器后:
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000);
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(2000,0,4,-4,0));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
//服务端返回应答消息时,该方法被调用
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
// Logger.info("client channelRead");
executor.execute(new ReadResponseProcessor((ByteBuf)msg));//reveive datas
}
}
由代码中可以看到,我们把支持基于长度解码的handler加入ChannelPipeline中,当它依次遍历ByteBuf中的可读字节时,根据长度判断是否已经读取完该条完整消息,因此不会出现因为数据包拆包或粘包导致的一系列问题。
所用解码器类字段释义:
public LengthFieldBasedFrameDecoder(int maxFrameLength,int lengthFieldOffset,int lengthFieldLength,int lengthAdjustment,int initialBytesToStrip)
参数:
maxFrameLength 这个定义最大帧的长度
lengthFieldOffset 长度属性的起始指针(偏移量)
lengthFieldLength 长度属性的长度,即存放数据包长度的变量的的字节所占的长度
lengthAdjustment 这个是一个长度调节值,例如当总长包含头部信息的时候,这个可以是个负数,就比较好实现了
initialBytesToStrip 这个属性也比较好理解,就是解码后的数据包需要跳过的头部信息的字节数
【总结】系统在数据消息处理handler之前加入基于长度字段的解码器后,在读取服务端的数据包时发现拆包粘包问题明显改善,基本可以避免消息数据读取越界或不完整的问题。
【附录】
LengthFieldBasedFrameDecoder 详细API:
几类常用解决拆包、粘包问题编(解)码器:
http://blog.csdn.net/zhaowen25/article/details/41122501