一、粘包现象

服务端

public static void main(String[] args) {NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(bossGroup,workerGroup);serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.debug("Server erro {}",e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}
}

客户端:希望发送 10 个消息,每个消息是 16 字节

public static void main(String[] args) {NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(workerGroup);bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {for (int i = 0; i < 10; i++) {ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,0,'a','b','c','d','e','f'});ctx.writeAndFlush(buffer);}}});}});ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.debug("Client erro {}",e);} finally {workerGroup.shutdownGracefully();}
}

服务端输出:一次就接收了 160 个字节,而非分 10 次接收

09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] REGISTERED
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] ACTIVE
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] READ: 160B+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000010| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000020| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000030| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000040| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000050| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000060| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000070| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000080| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000090| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
+--------+-------------------------------------------------+----------------+
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] READ COMPLETE

二、半包现象

服务端

  • serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) :影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍
public static void main(String[] args) {NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);try {ServerBootstrap serverBootstrap = new ServerBootstrap();//调整系统底层接收缓冲区(即滑动窗口)大小serverBootstrap.option(ChannelOption.SO_RCVBUF,10);serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(bossGroup,workerGroup);serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.debug("Server erro {}",e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}
}

客户端:希望发送 1 个消息,这个消息是 160 字节

public static void main(String[] args) {NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(workerGroup);bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel ch) throws Exception {ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ByteBuf buffer = ctx.alloc().buffer();for (int i = 0; i < 10; i++) {buffer.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,0,'a','b','c','d','e','f'});}ctx.writeAndFlush(buffer);}});}});ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.debug("Client erro {}",e);} finally {workerGroup.shutdownGracefully();}
}

服务端输出:接收的消息被分为两节,第一次 20 字节,第二次 140 字节

10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] REGISTERED
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] ACTIVE
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ: 20B+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000010| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ COMPLETE
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ: 140B+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000010| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000020| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000030| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000040| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000050| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000060| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000070| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000080| 05 06 07 08 09 00 61 62 63 64 65 66             |......abcdef    |
+--------+-------------------------------------------------+----------------+
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ COMPLETE

三、现象分析

粘包

  • 现象,发送 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 回来了,窗口就可以向前滑动
    • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

Nagle 算法:粘包发送端

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

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

四、解决方案

【NIO与Netty】网络编程:netty中粘包、半包现象展示,分析及解决相关推荐

  1. java nio 客户端_Java网络编程:Netty框架学习(二)---Java NIO,实现简单的服务端客户端消息传输...

    概述 上篇中已经讲到Java中的NIO类库,Java中也称New IO,类库的目标就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO), ...

  2. 【netty篇】- 第0章netty网络编程必备知识[持续更新中]~

    一.三大组件简介 Channel与Buffer Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer).通道表示打开到 IO 设备(例如:文件.套接字)的连接.若需要使用 NI ...

  3. Netty网络编程聊天项目

                                     Netty网络编程聊天项目 后端编写 导入依赖    <dependencies><dependency>&l ...

  4. Netty网络编程实战2,使用Netty开发聊天室功能

    目录 一.服务端 1.主程序类 2.自定义初始化器 3.自定义处理器 二.客户端 1.主程序类 2.自定义初始化器 3.自定义处理器 三.启动服务端.客户端 1.服务端:你好,我是服务端,哪吒编程 2 ...

  5. 基于 Netty 网络编程项目实战课程

    一 基于 Netty 网络编程项目实战课程 1项目介绍 2Netty 介绍与相关基础知识 2.1Netty 介绍 简介 Netty 是由 JBOSS 提供的一个 java 开源框架.Netty 提供异 ...

  6. Netty网络编程第八卷

    Netty网络编程第八卷 整体架构 ByteBuf Channel EventLoop和EventLoopGroup ChannelFuture ChannelHandler和ChannelPipel ...

  7. Netty网络编程第三卷

    Netty网络编程第三卷 三. Netty 进阶 1. 粘包与半包 1.1 粘包现象 1.2 半包现象 1.3 现象分析 MSS 限制 Nagle 算法 1.4 解决方案 方法1,短链接 方法2,固定 ...

  8. 三、Netty的粘包半包问题解决

    一.定义 TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生.客户端给服务端发送了两条消息ABC和DEF,服务端这边的接收会有多少种情况呢?有可能是一次 ...

  9. Netty框架之TCP粘包/半包解决方案

    Netty框架之TCP粘包/半包解决方案 一.TCP粘包 二.TCP半包 三.TCP粘包/半包解决方案 1.FixedLengthFrameDecoder定长解析器 2.LineBasedFrameD ...

  10. Netty粘包/半包问题解析

    目录 一.什么是粘包/半包问题 二.TCP粘包/半包发生的原因 三.粘包/半包解决办法 四.Netty中粘包/半包解决示例 1. 采用固定长度数据包编解码方式 2. 采用特殊字符作为边界字符编解码方式 ...

最新文章

  1. 前端入门11-JavaScript语法之数组
  2. 【公告】【公告】【公告】【公告】
  3. 百度笔试题:malloc/free与new/delete的区别
  4. Bootstrap全局css样式_代码
  5. P4100-[HEOI2013]钙铁锌硒维生素【矩阵求逆,最大匹配】
  6. 如何在Spring Boot App中集成H2数据库
  7. Python内存管理以及垃圾回收机制
  8. 审计利用计算机,利用计算机审计手段 提高审计工作水平
  9. 如梦如幻,开源实时的天空特效算法!
  10. SQL 存储过程中,将串连的字符串当表用。
  11. mysql的压缩版安装
  12. PDF各种格式相互转换
  13. 骨传导耳机优缺点有哪些?骨传导耳机科普与推荐
  14. matlab启动慢的解决方法
  15. 如何降低企业上云的4大步骤
  16. JSP+structs图书管理系统
  17. 《ERROR: MobSDK已停止支持非严格模式版本,请按上面编译告示接入合规版本》
  18. w ndows之家,Windows10一键优化工具
  19. 电路中串联 并联 电阻作用
  20. 网口的左右两个LED灯代表什么意思

热门文章

  1. “天下武功唯快不破”--常见高频交易策略简介
  2. SAP中MF47处理COGI欠料分析测试
  3. java 微信支付以及退款拿过来直接使用
  4. 【mac】macos苹果系统终端如何进入ROOT及退出问题
  5. OGNL表达式的入门
  6. C语言如何让一个函数返回多个值
  7. linux下使用命令行辅助定位内存泄漏问题
  8. 企业信息化的规划与实施(二)
  9. 【信息奥赛题解】流感传染(详细题解 C++代码)
  10. python import .pyc_Python pyc格式解析