netty中的粘包和拆包问题处理

来源:互联网 发布:租用临时备案域名 编辑:程序博客网 时间:2024/06/05 06:15

1、tcp粘包和拆包问题说明

    我们可以通过下图来说明tcp粘包和拆包问题的说明:
 

    假设客户端发送d1和d2两个数据包到服务端,此时服务端收到包有如下几种情况:
    1:服务端正常收到d1和d2两个数据包;
    2:服务端收到一个数据包即是D1和D2粘在一起,此时称为粘包;
    3:服务端分2次收到2个数据包,第一次是d1的完整包和d2的部分包,第二次是d2剩余的包,此时就是拆包;
    4:服务端分2次收到2个数据包,第一次是d1的部分包,第二次是d1的剩余包和d2的包。
    还有一种情况是数据包较大,但tcp的接收窗口非常小,此时服务端要分多次才能收完D1和D2的数据包。

2、tcp粘包和拆包发生的原因

     1、应用程序write写入的字节大小大于套接口发送缓冲区大小;
    2、进行Mss大小的tcp分段;
    3、以太网贞的payload大于MTU进行ip分片。


3、粘包问题的解决策略

     由于底层的tcp无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计解决。根据业界的主流协议的解决方案,可以归纳如下:
     1)消息定长,例如每个报文大小长度200字节,如果不够,空位补空格;
     2)在包尾增加回车换行进行分割,例如ftp协议;
     3)将消息分为消息头和消息体,消息头包含表示消息总长度的字段,通常设计思路为消息头的第一字段使用int32来           表示消息的总长度;
     4)更复杂的应用层协议;

4、tcp粘包异常案例

   这里改造下timeServer,在收到消息的时候去除回车换行符。
package zou;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerAdapter;import io.netty.channel.ChannelHandlerContext;public class TimeServerHandler extends ChannelHandlerAdapter {private int counter;@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//读取客户端发送的字节ByteBuf buf = (ByteBuf) msg;byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String body = new String(req, "utf-8").substring(0, req.length - System.getProperty("line.separator").length());//String body = (String) msg;System.out.println("the time server receive order :" + body + "the counter:" + ++counter);String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";currentTime += System.getProperty("line.separator");ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());//进行发送消息到客户端ctx.writeAndFlush(resp);}//@Override//public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {////通过调用此方法,将发送的缓冲区的消息全部写到SocketChannel中//ctx.flush();//}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();}}
  timeClientHandler如下:
public class TimeClientHandle extends ChannelHandlerAdapter {private static final Logger logger = Logger.getLogger(TimeClientHandle.class.getName());private int counter;private byte[] req;public TimeClientHandle() {req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes();}//连接成功后发送指令@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ByteBuf message = null;for (int i = 0; i < 100; i++) {message = Unpooled.buffer(req.length);message.writeBytes(req);ctx.writeAndFlush(message);}}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buf = (ByteBuf) msg;byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String body = new String(req, "utf-8");//String body = (String) msg;System.out.println("Now is:" + body + "the counter is " + ++counter);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();}}
   这里会进行发送100次查询时间的消息,按道理服务端和客户端都应该有100次的显示,但执行后结果如下:
   the time server receive order :QUERY TIME ORDER
QUERY TIME ORDER
。。。。。
the time server receive order :Y TIME ORDER
 客户端执行如下:
Now is:BAD ORDER
BAD ORDER
the counter is 1
 此时说明已经产生了粘包。

5、利用LineBaseFrameDecoder解决tcp粘包问题

为了解决tcp的粘包/拆包问题,netty默认提供了多种编码解码器用于处理半包。只要熟练掌握这些类库的使用,tcp粘包问题从此就会变得非常容易。调整点如下,TimeServer进行handler的初始化如下:
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel arg0) throws Exception {arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));arg0.pipeline().addLast(new StringDecoder());arg0.pipeline().addLast(new TimeServerHandler());}}
   这里添加了2个编码器,lineBaseFrameDecode和StringDecoder,后面的TimeServerHandler 读取消息调整如下:
@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {//读取客户端发送的字节String body = (String) msg;System.out.println("the time server receive order :" + body + "the counter:" + ++counter);String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(System.currentTimeMillis()).toString() : "BAD ORDER";currentTime += System.getProperty("line.separator");ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());//进行发送消息到客户端ctx.writeAndFlush(resp);}
  此时代码很简洁,直接将消息转换为字符串,且已经是去除回车换行的。TimeClient的调整如下,也是增加解码器:
protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LineBasedFrameDecoder(1024));ch.pipeline().addLast(new StringDecoder());ch.pipeline().addLast(new TimeClientHandle());}
其timeClientHandler进行解码如下:
@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {String body = (String) msg;System.out.println("Now is:" + body + "the counter is " + ++counter);}

   非常简洁。相对于之前的调整有增加了解码器,另外在进行读取消息时候代码也简洁了很多,不用编码解码。
  进行运行发现没有产生tcp粘包的问题。


6、LineBaseFrameDecoder和StringDecoder原理分析

    LineBaseFrameDecoder的原理就是依次遍历ByteBuf中的可读字节,判断是否有“\n”或者是“\r\n”,如果有就以此为换行。另外它也能指定长度,如果在指定的长度内没有换行符那么就会抛出异常。
    StringDecoder非常简单,就是将接收到对象转换为字符串,然后继续后面的Handler调用。LineBaseFrameDecoder和StringDecoder结合起来使用就是按行切换的文本解码器,它被设计用来支持tcp的粘包和拆包问题。