粘包和半包的解决(一)

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: 粘包和半包的解决

粘包产生

1. public class HelloWordServer {
2. static final Logger log = LoggerFactory.getLogger(HelloWordServer.class);
3. 
4. public static void main(String[] args) {
5. NioEventLoopGroup boss = new NioEventLoopGroup(1);
6. NioEventLoopGroup worker = new NioEventLoopGroup();
7. try {
8. ServerBootstrap serverBootstrap = new ServerBootstrap()
9.                     .channel(NioServerSocketChannel.class)
10.                     .group(boss, worker)
11.                     .childHandler(new ChannelInitializer<SocketChannel>() {
12. protected void initChannel(SocketChannel ch) throws Exception {
13.                             ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
14.                             ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
15. public void channelActive(ChannelHandlerContext ctx) throws Exception {
16.                                     log.debug("connected{}", ctx.channel());
17. super.channelActive(ctx);
18.                                 }
19. 
20. @Override
21. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
22.                                     log.debug("disconnected{}", ctx.channel());
23. super.channelActive(ctx);
24.                                 }
25. 
26.                             });
27.                         }
28.                     });
29. ChannelFuture channelFuture = serverBootstrap.bind(8080);
30.             channelFuture.sync();
31.             log.debug("{} bing",channelFuture.channel());
32.             channelFuture.channel().closeFuture().sync();
33. 
34.         } catch (Exception e) {
35.             log.error("server error",e);
36.         }
37.     }
38. 
39. }
1. public class HelloWordClient {
2. static final Logger log = LoggerFactory.getLogger(HelloWordServer.class);
3. public static void main(String[] args) {
4. NioEventLoopGroup work = new NioEventLoopGroup();
5. try {
6. Bootstrap bootstrap = new Bootstrap()
7.                     .channel(NioSocketChannel.class)
8.                     .group(work)
9.                     .handler(new ChannelInitializer<SocketChannel>() {
10. protected void initChannel(SocketChannel ch) throws Exception {
11.                             log.debug("connetred");
12.                             ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
13. public void channelActive(ChannelHandlerContext ctx) throws Exception {
14.                                     log.debug("sending");
15. Random r = new Random();
16. char c = 'a';
17. for (int i = 0; i < 10; i++) {
18. ByteBuf buffer = ctx.alloc().buffer();
19.                                         buffer.writeBytes(new byte[]{0, 1, 2, 3, 4,5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
20.                                         ctx.writeAndFlush(buffer);
21.                                     }
22. super.channelActive(ctx);
23.                                 }
24.                             });
25.                         }
26.                     });
27. ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
28.             channelFuture.channel().closeFuture().sync();
29. 
30. 
31.         }catch (Exception e){
32.             log.error("client error...");
33.         }finally {
34.             work.shutdownGracefully();
35.         }
36. 
37.     }

如上服务器端的某次输出,可以看到一次就接收了 160 个字节,而非分 10 次接收

半包产生

客户端代码希望发送 1 个消息,这个消息是 160 字节,代码改为

1. ByteBuf buffer = ctx.alloc().buffer();
2. for (int i = 0; i < 10; i++) {
3.     buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
4. }
5. ctx.writeAndFlush(buffer);

为现象明显,服务端修改一下接收缓冲区,其它代码不变

serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);

服务器端的某次输出,可以看到接收的消息被分为两节,第一次 20 字节,第二次 140 字节

注意

serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) 影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍

现象分析

粘包

  • 现象,发送 abc def,接收 abcdef
  • 原因
  • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
  • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
  • Nagle 算法:会造成粘包

半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
  • 应用层:接收方 ByteBuf 小于实际发送数据量
  • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
  • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

本质是因为 TCP 是流式协议,消息无边界

滑动窗口

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
  • 图中深色的部分即要发送的数据,高亮的部分即窗口
  • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
  • 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动
  • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

MSS 限制

  • 链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如
  • 以太网的 MTU 是 1500
  • FDDI(光纤分布式数据接口)的 MTU 是 4352
  • 本地回环地址的 MTU 是 65535 - 本地测试不走网卡
  • MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数
  • ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
  • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
  • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

Nagle 算法

  • 即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
  • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
  • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
  • 如果 TCP_NODELAY = true,则需要发送
  • 已发送的数据都收到 ack 时,则需要发送
  • 上述条件不满足,但发生超时(一般为 200ms)则需要发送
  • 除上述情况,延迟发送

解决方案

  1. 短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
  2. 每一条消息采用固定长度,缺点浪费空间
  3. 每一条消息采用分隔符,例如 \n,缺点需要转义
  4. 每一条消息分为 head 和 body,head 中包含 body 的长度


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
6月前
|
缓存 移动开发 网络协议
tcp业务层粘包和半包理解及处理
tcp业务层粘包和半包理解及处理
106 1
|
6月前
|
JSON 移动开发 网络协议
数据拆散与黏连:深入剖析Netty中的半包与粘包问题
数据拆散与黏连:深入剖析Netty中的半包与粘包问题
158 0
|
6月前
|
XML 缓存 网络协议
面试题:TCP的粘包和拆包
面试题:TCP的粘包和拆包
56 1
|
6月前
|
存储 网络协议 算法
Netty使用篇:半包&粘包
Netty使用篇:半包&粘包
|
缓存 移动开发 网络协议
Netty 中的粘包和拆包详解
Netty 中的粘包和拆包详解
4313 1
|
移动开发
粘包和半包的解决(二)
粘包和半包的解决(二)
Netty(三)之数据之粘包拆包
Netty(三)之数据之粘包拆包
78 0
Netty(三)之数据之粘包拆包
|
存储 网络协议
TCP拆包和粘包的作用是什么
首先我们思考一个问题,应用层的传输一个10M的文件是一次性传输完成,而对于传输层的协议来说,为什么不是一次性传输完成呢。 这个有很多原因,比如稳定性,一次发送的数据越多,出错的概率越大。再比如说为了效率,网络中有时候存在并行的路径,拆分数据包就就能更好的利用这些并行的路径。再有,比如发送和接收数据的时候,都存在缓冲区,缓冲区是在内存中开辟的一块空间,目的是缓冲大量的应用频繁的通过网卡收发数据,这个时候,网卡只能一个一个处理应用的请求。当网卡忙不过来的时候,数据就需要排队了。也就是将数据放入缓冲区。如果每个应用都随意发送很大的数据,可能导致其他应用的实时性遭到破坏。
75 0
|
移动开发 网络协议 Java
TCP 粘包/拆包问题
《基础系列》
160 0
TCP 粘包/拆包问题