远方的灯塔 - 专注于服务端技术分享 远方的灯塔 - 专注于服务端技术分享
首页
  • Java SE
  • Struts2
  • Hibernate
  • MyBatis
  • JAX-WS
  • 并发
  • 分布式
  • Git
  • 文章分类
  • 文章标签
  • 文章归档
  • 《C程序设计语言》
心情随笔
友情链接
给我留言 (opens new window)
关于我
GitHub (opens new window)

Terwer Green

一个后端老菜鸟
首页
  • Java SE
  • Struts2
  • Hibernate
  • MyBatis
  • JAX-WS
  • 并发
  • 分布式
  • Git
  • 文章分类
  • 文章标签
  • 文章归档
  • 《C程序设计语言》
心情随笔
友情链接
给我留言 (opens new window)
关于我
GitHub (opens new window)
  • JavaSE

  • 开源框架

  • Linux

  • Struts2

  • Hibernate

  • Webservice

  • 分布式

    • RPC架构设计及IO模型
    • NIO编程及其三大核心原理
    • NIO三大核心之缓冲区(Buffer)
    • NIO三大核心之通道(Channel)
    • NIO三大核心之选择器(Selector)
    • Netty核心原理
    • 线程模型以及传统IO阻塞模型
    • Reactor模型
    • Netty线程模型
    • Netty核心API介绍
    • Netty入门与异步模型
    • Netty高级进阶之Netty编解码器
    • Netty高级进阶之基于Netty的群聊天室案例
    • Netty高级进阶之基于Netty的HTTP服务器开发
    • Netty高级进阶之基于Netty的Websocket开发网页聊天室
    • Netty高级进阶之Netty中的粘包和拆包的解决方案
      • 粘包和拆包简介
      • TCP粘包和拆包产生的原因
      • 粘包和拆包的代码演示
      • 粘包和拆包的解决方案
      • 本文源码地址
    • Nety源码剖析
    • 自定义RPC框架之分布式架构网络通信理论
    • 自定义RPC框架之基于Netty实现RPC框架
    • 分布式架构理论
    • 分布式理论之数据一致性
    • 分布式理论之CAP定理
    • 分布式理论之BASE定理
    • 分布式一致性协议之两阶段提交协议(2PC)
    • 分布式一致性协议之三阶段提交协议(3PC)
    • 分布式一致性协议之NWR协议
    • 分布式一致性协议之Gossip协议
    • 分布式一致性协议之Paxos协议
    • 分布式一致性协议之Raft协议
    • 分布式一致性协议之Lease机制
    • 分布式系统设计策略之心跳检测
    • 分布式系统设计策略之高可用
    • 分布式系统设计策略之容错性
    • 分布式系统设计策略之负载均衡
    • 分布式架构服务调用
    • 分布式服务治理之服务协调
    • 分布式服务治理之服务削峰
    • 分布式服务治理之服务降级
    • 分布式服务治理之服务限流
    • 分布式服务治理之服务熔断
    • 分布式服务治理之服务链路追踪
    • 架构设计基本原则之开闭原则(OCP)
    • 架构设计基本原则之单一职责原则(SRP)
    • 架构设计基本原则之接口隔离原则(ISP)
    • 架构设计基本原则之里式替换原则(LSP)
    • 架构设计基本原则之依赖倒置原则(DIP)
    • 架构设计基本原则知识扩展
    • 分布式架构知识拓展与总结
  • 分布式框架

  • 后端开发
  • 分布式
terwer
2022-04-27
目录

Netty高级进阶之Netty中的粘包和拆包的解决方案

本文阐述了Netty中常见的粘包和拆包问题,并深入分析了常用的解决方案。

# Netty高级进阶之Netty中的粘包和拆包的解决方案

# 粘包和拆包简介

粘包和拆包是TCP网络编程中不可避免的,无论是服务端和客户端,当读取或发送消息时,都需要考虑TCP底层的粘包/拆包机制。

TCP是个流协议,流,就是没有界限的一组数据。

TCP底层并不了解上层业务数据的具体含义它会根据TCP的缓冲区的实际情况进行包的拆分。在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小包封装成一个大的数据包发送,这就是TCP的粘包和拆包问题。

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

  1. 服务器两次读到了两个独立的数据包,分别是D1和D2,没有粘包和拆包

    image-20220502205307893

  2. 服务器一次读到了两个数据包,D1和D2粘在一起,就是TCP粘包

    image-20220502205335162

  3. 如果D2 的数据包比较大,服务端分两次读到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,就是TCP拆包

    image-20220502205358020

  4. 如果D1、D2包都很大,服务端分多次才能将D1和D2读取完全,期间可能发生多次拆包

    image-20220502205420569

# TCP粘包和拆包产生的原因

数据从发送方到接收方需要经过操作系统得到缓冲区,造成粘包和拆包的主要原因就是这个缓冲区。

粘包可以理解为缓冲区的数据堆积,导致多个请求粘在一起,拆包可以理解为发送的数据大于缓冲区,进行拆分处理。

# 粘包和拆包的代码演示

  1. 粘包

    • 客户端

      /**
       * 通道就绪事件
       *
       * @param ctx
       * @throws Exception
       */
      @Override
      public void channelActive(ChannelHandlerContext ctx) throws Exception {
          for (int i = 0; i < 10; i++) {
              ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是Netty客户端" + i, CharsetUtil.UTF_8));
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
    • 服务端

      /**
       * 通道读取事件
       *
       * @param ctx
       * @param msg
       * @throws Exception
       */
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
          ByteBuf byteBuf = (ByteBuf) msg;
          System.out.println("客户端发过来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
          System.out.println("读取次数:" + (++count));
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
    • 运行结果

      image-20220502214233739

      image-20220502214303600

      服务端一次读取了客户端发送过来的消息,应该读取10次, 因此发生粘包。

  2. 拆包

    • 客户端

      /**
       * 通道就绪事件
       *
       * @param ctx
       * @throws Exception
       */
      @Override
      public void channelActive(ChannelHandlerContext ctx) throws Exception {
          // 一次发送102400字节数据
          char[] chars = new char[102400];
          Arrays.fill(chars, 0, 102399, 'a');
          for (int i = 0; i < 10; i++) {
              ctx.writeAndFlush(Unpooled.copiedBuffer(chars, CharsetUtil.UTF_8));
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
    • 服务端

      /**
       * 通道读取事件
       *
       * @param ctx
       * @param msg
       * @throws Exception
       */
      @Override
      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
          ByteBuf byteBuf = (ByteBuf) msg;
          // System.out.println("客户端发过来的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
          System.out.println("读取的长度:" + byteBuf.readableBytes());
          System.out.println("读取次数:" + (++count));
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
    • 运行结果

      image-20220502214643017

      image-20220502214109440

      客户端发送的数据比较大时,读取了18次,应该读取10次,因此发生了拆包

# 粘包和拆包的解决方案

  1. 业内的解决方案

    底层的TCP无法理解上层的业务数据,因此底层是无法解决粘包与拆包的。

    只能通过上层协议栈的设计来解决,目前业内主流的解决方案如下:

    • 消息长度固定,累计读取到长度和为定长LEN的报文之后,就认为读取到了一个完整的信息
    • 将换行符作为消息结束符
    • 将特殊分隔符作为消息结束标志,回车换行符就是特殊的结束分隔符
    • 通过在消息头中定义长度字段来标识消息的长度
  2. Netty的解决方案

    Netty提供了4中解码器来解决,分别如下:

    • 固定长度的拆包器 FixedLengthFrameDecoder , 每个应用层数据包的拆分都是固定长度大小

    • 行拆包器 LineBasedFrameDecoder ,每个应用层数据包,都以换行符作为分隔符,进行分割拆分

    • 分隔符拆包器 DelimiterBasedFrameDecoder ,每个应用层数据包,都通过自定义分隔符,进行分割拆分

    • 基于数据包长度的拆包器,LengthFieldBasedFrameDecoder 将应用层数据包的长度,作为接收端应用层数据包的拆分依据。

      按照应用层数据包的大小,进行拆包。

      这个拆包器有个要求,应用层协议包含数据包长度。

  3. 代码实现

    • LineBasedFrameDcoder解码器

      // 添加解码器,解决粘包问题
      ch.pipeline().addLast(new LineBasedFrameDecoder(2048));
      
      1
      2
      ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是Netty客户端" + i + "\n", CharsetUtil.UTF_8));
      
      1
    • DelimiterBasedFrameDecoder解码器

      ByteBuf byteBuf =
      Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
      ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));
      
      1
      2
      3
      ctx.writeAndFlush(Unpooled.copiedBuffer("你好呀,我是Netty客户端"+i+"$", CharsetUtil.UTF_8));
      
      1

# 本文源码地址

netty-sticking-unpacking (opens new window)

编辑 (opens new window)
#netty#bag
上次更新: 2023/02/22, 13:47:25
Netty高级进阶之基于Netty的Websocket开发网页聊天室
Nety源码剖析

← Netty高级进阶之基于Netty的Websocket开发网页聊天室 Nety源码剖析→

最近更新
01
解决css部分border被圆角切掉之后圆角的边框消失问题
03-18
02
使用TypeScript开发一个自定义的Node-js前端开发脚手架
03-08
03
Github-Actions使用release-please实现自动发版
03-06
更多文章>
Theme by Vdoing | Copyright © 2011-2023 Terwer Green | MIT License | 粤ICP备2022020721号-1 | 百度统计
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式