从零开始学Netty (三)-- TCP粘包/半包分析 及 解决方案

什么是粘包/半包

 首先来看一个Demo,在客户端连接上服务器后,给服务器发送1000次的数据。

    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf msg = null;
        String request = "demo test!" + System.getProperty("line.separator");
        for(int i = 0; i < 1000; i++){
            msg = Unpooled.buffer(request.length());
            msg.writeBytes(request.getBytes());
            ctx.writeAndFlush(msg);
        }
    }

服务端接收请求后,通过一个AtomicInteger记录数量,发现打印出来的数量并不是1000,而是2,这里就出现了问题。注意这里只添加自定义的Handler。

    private AtomicInteger counter = new AtomicInteger(0);

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        String request = in.toString(CharsetUtil.UTF_8);
        System.out.println("Server Accept:"+request
                +" the counter is "+counter.incrementAndGet());
    }

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

 

粘包/半包发生的原因

由于TCP协议本身的机制,客户端与服务器会维持一个连接。数据在连接不断开的情况下,持续不断地将多个数据包发往服务器。如果启用了Nagle算法(可以配置

.childOption(ChannelOption.TCP_NODELAY,true) 

来关闭),会对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包。而服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,也会造成粘包现象。

分包产生的原因可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包,还有可能就是一个包可能被分成了两次传输,在取数据的时候,先取到了一部分(还可能与接收的缓冲区大小有关系),总之就是一个数据包被分成了多次接收。

而UDP请求本身是无连接的,不会对数据包进行合并发送,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。UDP分包的问题同TCP一样存在。

 

解决方案

这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。

(1)在包尾增加分割符,比如回车换行符进行分割,例如FTP协议;linebase包和delimiter包下,分别使用 LineBasedFrameDecoder和DelimiterBasedFrameDecoder。

  • 定义LineBasedFrameDecoder的Handler,同时每次发送数据最后增加换行符System.getProperty("line.separator") 。
ch.pipeline().addLast(new LineBasedFrameDecoder(1024*100000));

(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;fixed包下,使用FixedLengthFrameDecoder。

  • 定义FixedLengthFrameDecoder的Handler,同时每次发送的数据包长度需要和FixedLengthFrameDecoder的设置一样。
ch.pipeline().addLast(new FixedLengthFrameDecoder(2000))

(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度,LengthFieldBasedFrameDecoder。

  • 定义DelimiterBasedFrameDecoder的Handler,这里配置的分隔符是"delimiter",同时每次发送数据最后需要加上分隔符"delimiter" 。

 

ByteBuf delimiter = Unpooled.copiedBuffer("delimiter".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值