您的位置:首页 > 理论基础 > 计算机网络

网络通信框架Netty的TCP粘包/拆包解决方案

2016-10-13 16:41 489 查看
前言】针对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:

https://netty.io/4.0/api/io/netty/handler/codec/LengthFieldBasedFrameDecoder.html#LengthFieldBasedFrameDecoder(int,%20int,%20int)

几类常用解决拆包、粘包问题编(解)码器:

http://blog.csdn.net/zhaowen25/article/details/41122501
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  框架 netty