Netty中粘包/拆包处理

源码共读

共 1745字,需浏览 4分钟

 · 2021-01-09

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群


作者丨ytao

来源丨ytao

TCP 是基于流传输的协议,请求数据在其传输的过程中是没有界限区分,所以我们在读取请求的时候,不一定能获取到一个完整的数据包。如果一个包较大时,可能会切分成多个包进行多次传输。同时,如果存在多个小包时,可能会将其整合成一个大包进行传输。这就是 TCP 协议的粘包/拆包概念。

本文基于 Netty5 进行分析

粘包/拆包描述

假设当前有 123abc两个数据包,那么他们传输情况示意图如下:

  • I 为正常情况,两次传输两个独立完整的包。

  • II 为粘包情况, 123和 abc封装成了一个包。

  • III 为拆包情况,图中的描述是将 123拆分成了 1和 23,并且 1和 abc一起传输。 123和 abc也可能是 abc进行拆包。甚至 123和 abc进行多次拆分也有可能。

Netty 粘包/拆包问题

为突出 Netty 的粘包/拆包问题,这里通过例子进行重现问题,以下为突出问题的主要代码:

服务端:

  1. /**

  2. * 服务端网络事件的读写操作类

  3. *

  4. * Created by YangTao.

  5. */

  6. publicclassServerHandlerextendsChannelHandlerAdapter{

  7. // 接收消息计数器

  8. privateint i = 0;


  9. // client端消息

  10. @Override

  11. publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throwsException{

  12. i++;


  13. System.out.print(msg);


  14. // 对每条读取到的消息进行打数标记

  15. System.out.println("================== ["+ i +"]");

  16. // 发送应答消息给客户端

  17. ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes());

  18. ctx.write(rmsg);

  19. }


  20. // 其他操作 .......

  21. }

客户端:

  1. /**

  2. * 客户端发送数据

  3. *

  4. * Created by YangTao.

  5. */

  6. publicclassNettyClient{


  7. publicvoid send() {

  8. Bootstrap bootstrap = newBootstrap();

  9. NioEventLoopGroup group = newNioEventLoopGroup();


  10. try{

  11. bootstrap.group(group)

  12. .channel(NioSocketChannel.class)

  13. .option(ChannelOption.TCP_NODELAY, true)

  14. .handler(newChannelInitializer<NioSocketChannel>() {

  15. @Override

  16. protectedvoid initChannel(NioSocketChannel ch) {

  17. ChannelPipeline pipeline = ch.pipeline();

  18. pipeline.addLast(newStringDecoder());

  19. pipeline.addLast("logger", newLoggingHandler(LogLevel.INFO));

  20. pipeline.addLast(newClientHandler());

  21. }

  22. });

  23. Channel channel = bootstrap.connect(HOST, PORT).channel();

  24. int i = 1;

  25. while(i <= 300){

  26. channel.writeAndFlush(String.format("【时间 %s: \t%s】", newDate(), i));

  27. // 打印发送请求的次数

  28. System.out.println(i);

  29. i++;

  30. }

  31. }catch(Exception e){

  32. e.printStackTrace();

  33. }finally{

  34. if(group != null)

  35. group.shutdownGracefully();

  36. }

  37. }

  38. }

以上代码中,我们第一反应理解的是,如果非异常情况下客户端所有数据发送成功,并且服务端全部接收到。那么从打印信息中可以看到客户端的发送次数 i和服务端的接收消息计数 i应该是相同的数。那么下面通过运行程序,查看打印结果。

如上图所示, 【】中的最后一个数字与 []中数字对上的是已独立完整的包接收到(粘包/拆包示意图中的情况 I)。但是 【】中为 3738的出现了粘包情况(粘包/拆包示意图中的情况 II),两条数据粘合在一起。

上图中可以看到 【】167的数据被拆分为了两部分(图中画绿线数据),该情况为拆包(粘包/拆包示意图中的情况 III)

上面程序没有考虑到 TCP 的粘包/拆包问题,所以如果是我们实际应用的程序的话,不能保证数据的正常情况,就会导致程序异常。

Netty 解决粘包/拆包问题

LineBasedFrameDecoder 换行符处理

Netty 的强大,方便,简单使用的优势,在粘包/拆包问题上也提供了多种编解码解决方案,并且很容易理解和掌握。这里使用 LineBasedFrameDecoder 和 StringDecoder(将接收到的对象转换成字符串) 来解决粘包/拆包问题。只需在服务端和客户端分别添加 LineBasedFrameDecoder 和 StringDecoder解码器,因为是双向会话,所以两端都要添加,由于我一开始就添加 StringDecoder 编码器,所以只需添加 LineBasedFrameDecoder 就够了。服务端:

客户端:

服务端网络事件操作:

  1. /**

  2. * 服务端网络事件的读写操作类

  3. *

  4. * Created by YangTao.

  5. */

  6. publicclassServerHandlerextendsChannelHandlerAdapter{

  7. // 接收消息计数器

  8. privateint i = 0;


  9. // client端消息

  10. @Override

  11. publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throwsException{

  12. i++;


  13. System.out.print(msg);


  14. // 对每条读取到的消息进行打数标记

  15. System.out.println("================== ["+ i +"]");

  16. // 发送应答消息给客户端

  17. ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes());

  18. ctx.write(rmsg);

  19. }


  20. // 其他操作 .......

  21. }

客户端发送数据:

  1. /**

  2. * 客户端发送数据

  3. *

  4. * Created by YangTao.

  5. */

  6. publicclassNettyClient{


  7. publicvoid send() {

  8. // 连接操作 .......


  9. try{

  10. // 获取 channel

  11. Channel channel = channel();

  12. int i = 1;

  13. ByteBuf buf = null;

  14. while(i <= 300){

  15. String str = String.format("【时间 %s: \t%s】", newDate(), i) + System.getProperty("line.separator");

  16. byte[] bytes = str.getBytes();

  17. // 写入缓冲区

  18. buf = Unpooled.buffer(bytes.length);

  19. buf.writeBytes(bytes);

  20. channel.writeAndFlush(buf);

  21. // 打印发送请求的次数

  22. System.out.println(i);

  23. i++;

  24. }

  25. }catch(Exception e){

  26. e.printStackTrace();

  27. }


  28. // 退出操作 .......

  29. }

  30. }

细心观察代码的变化,应该会发现现在的代码每次在发送消息的时候,在消息末尾后加了换行分隔符。注意,使用 LineBasedFrameDecoder 时,换行分隔符必须加,否则接收消息端收不到消息,如果手写换行分割,要记得区分不同系统的适配

经过多次测试 3W 条请求,没有再出现过粘包/拆包情况,看最后一条数据数字是否相同便知。

DelimiterBasedFrameDecoder 自定义分隔符

自定义分隔符和换行分隔符差不多,只需将发送的数据后换行符换成你自己设定的分割符即可。

服务端和客户端均在 pipeline 添加 DelimiterBasedFrameDecoder:

  1. // 指定的分隔符

  2. publicstaticfinalString DELIMITER = "$@$";


  3. // 如果当前数据2048个字节中没有分隔符,就会抛出异常,避免内存溢出。也可以自定义预检查当前读取的数据,自定义这里超过的规则

  4. pipeline.addLast(newDelimiterBasedFrameDecoder(

  5. 2048,

  6. Unpooled.wrappedBuffer(DELIMITER.getBytes())) // 分割符缓冲对象

  7. );

FixedLengthFrameDecoder 根据固定长度

设定固定长度,进行数据传输,如果不达固定长度,使用空格补全。

服务端和客户端均在 pipeline 添加 FixedLengthFrameDecoder:

  1. // 100为指定的固定长度

  2. ch.pipeline().addLast(newFixedLengthFrameDecoder(100));

每次读取数据时都会按照 FixedLengthFrameDecoder 中设置的固定长度进行解码,如果出现粘包,那么会进行多次解码,如果出现拆包的情况,那么 FixedLengthFrameDecoder 会先缓存当前部分包的信息,当接收下一个包时,会与缓存的部分包进行拼接,知道满足规定的长度。

动态指定长度

动态指定长度就是说,每条消息的长度都是随着消息头进行指定,这里使用的编码器为 LengthFieldBasedFrameDecoder。

  1. pipeline().addLast(

  2. newLengthFieldBasedFrameDecoder(

  3. 2048, // 帧的最大长度,即每个数据包最大限度

  4. 0, // 长度字段偏移量

  5. 4, // 长度字段所占的字节数

  6. 0, // 消息头的长度,可以为负数

  7. 4) // 需要忽略的字节数,从消息头开始,这里是指整个包

  8. );

发送消息时,创建自己的消息对象编码器

  1. // 创建 byteBuf

  2. ByteBuf buf = getBuf();


  3. // .....


  4. // 设置该条消息内容长度

  5. buf.writeInt(msg.length());

  6. // 设置消息内容

  7. buf.writeBytes(msg.getBytes("UTF-8"));

服务端读取的时候就直接读取即可,没其他特殊操作。

除了以上 Netty 提供的现成方案,还可以通过重写 MessageToByteEncoder 编码实现自定义协议。

总结

Netty 极大的为使用者提供了多种解决粘包/拆包方案,并且可以很愉快的对多种消息进行自动解码,在使用过程中也极容易掌握和理解,很大程度上提升开发效率和稳定性。

程序员专栏
 扫码关注填加客服 
长按识别下方二维码进群

近期精彩内容推荐:  

 肝了一晚上搞出来微信订阅号鉴黄机器人

 不允许程序员透露薪资!!!凭啥?

 程序员带娃有多“恐怖” ?!

 有个大神级女朋友是什么体验





在看点这里好文分享给更多人↓↓

浏览 17
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报