惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
G
GRAHAM CLULEY
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
The Last Watchdog
The Last Watchdog
S
Secure Thoughts
Webroot Blog
Webroot Blog
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
Hacker News: Ask HN
Hacker News: Ask HN
N
News and Events Feed by Topic
H
Heimdal Security Blog
H
Help Net Security
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
F
Full Disclosure
小众软件
小众软件
S
Securelist
罗磊的独立博客
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
S
Schneier on Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
T
Tenable Blog
The Register - Security
The Register - Security
C
Check Point Blog
AWS News Blog
AWS News Blog
Cloudbric
Cloudbric
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google Online Security Blog
Google Online Security Blog
博客园 - 叶小钗
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 司徒正美

牛牛技术客栈

设计模式-备忘录模式 - OXOXTECH 牛牛技术客栈 设计模式-中介者模式 - OXOXTECH 牛牛技术客栈 Linux【Ubuntu】修改ssh默认端口 - OXOXTECH 牛牛技术客栈 设计模式-迭代器模式 - OXOXTECH 牛牛技术客栈 scheduled定时任务的三种基本实现方式 - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(三) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(二) - OXOXTECH 牛牛技术客栈 Apriori - 基于关联规则的推荐算法(一) - OXOXTECH 牛牛技术客栈 基于JavaFX的桌面端网络调试工具 - OXOXTECH 牛牛技术客栈 Golang Channel的原理介绍 - OXOXTECH 牛牛技术客栈 Go语言Map的原理分析 - OXOXTECH 牛牛技术客栈 Go语言错误处理(panic)的最佳实践 - OXOXTECH 牛牛技术客栈 设计模式-解释器模式 - OXOXTECH 牛牛技术客栈 Redis报错Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. go-webpbin库在Linux报错failed to encode image to WebP: exit status 1.......的问题 exe4j 打包加密的jar - OXOXTECH 牛牛技术客栈 Go生成图形验证码示例 - OXOXTECH 牛牛技术客栈 澳门一天游:一日尽享东方与西方的交融之美 - OXOXTECH 牛牛技术客栈 设计模式-命令模式 - OXOXTECH 牛牛技术客栈 别再自己瞎写工具类了,SpringBoot内置工具类应有尽有 - OXOXTECH 牛牛技术客栈 中山一日游 - OXOXTECH 牛牛技术客栈 设计模式-责任链模式 - OXOXTECH 牛牛技术客栈 起舞吧,齐舞吧 - OXOXTECH 牛牛技术客栈 设计模式-组合模式 - OXOXTECH 牛牛技术客栈 Go语言Web开发|GoFrame框架入门笔记 - OXOXTECH 牛牛技术客栈 Java打包exe教程 - OXOXTECH 牛牛技术客栈 设计模式-代理模式 - OXOXTECH 牛牛技术客栈 MySQL存储过程的优缺点有哪些? - OXOXTECH 牛牛技术客栈 前端渲染优化有哪些? - OXOXTECH 牛牛技术客栈 HTTP状态码及其含义 - OXOXTECH 牛牛技术客栈 从浏览器地址栏输入url到显示页面的步骤 - OXOXTECH 牛牛技术客栈 TypeScript事件派发管理器 - OXOXTECH 牛牛技术客栈 MQTT保留消息的使用方法 - OXOXTECH 牛牛技术客栈 世界工程-港珠澳大桥游 - OXOXTECH 牛牛技术客栈 Golang逃逸分析 - OXOXTECH 牛牛技术客栈 设计模式-享元模式 - OXOXTECH 牛牛技术客栈 牛牛成长记录 - OXOXTECH 牛牛技术客栈 ffmpeg常用命令 - OXOXTECH 牛牛技术客栈 设计模式-外观模式 - OXOXTECH 牛牛技术客栈 设计模式-装饰器模式 - OXOXTECH 牛牛技术客栈 设计模式-桥接模式 - OXOXTECH 牛牛技术客栈 5周年恋爱纪念日 - OXOXTECH 牛牛技术客栈 2024新年快乐,龙腾四海 - OXOXTECH 牛牛技术客栈 迎接新年:除夕的美好时刻 - OXOXTECH 牛牛技术客栈 设计模式-适配器模式 - OXOXTECH 牛牛技术客栈 设计模式-原型模式 - OXOXTECH 牛牛技术客栈 设计模式-建造者模式 - OXOXTECH 牛牛技术客栈 设计模式-工厂模式 - OXOXTECH 牛牛技术客栈 设计模式-单例模式 - OXOXTECH 牛牛技术客栈 SpringBoot在Linux环境下发送163邮件失败(No appropriate protocol (protocol is disabled or cipher suites are inappropriate)) 海与日落 - OXOXTECH 牛牛技术客栈 Swagger比较常用的注解 - OXOXTECH 牛牛技术客栈 猫🐱牛 - OXOXTECH 牛牛技术客栈 2023年最后一个晚霞 - OXOXTECH 牛牛技术客栈 Linux(Centos)部署Nginx教程 - OXOXTECH 牛牛技术客栈 Linux MySQL下载安装详细教程(CentOS版) - OXOXTECH 牛牛技术客栈 JavaFx打包成exe - OXOXTECH 牛牛技术客栈 Flux脚本语言入门教程 - OXOXTECH 牛牛技术客栈 演唱会出图 - OXOXTECH 牛牛技术客栈 SpringBoot实现订单超时取消的几种方案 - OXOXTECH 牛牛技术客栈 详解Java并发中的各种锁 - OXOXTECH 牛牛技术客栈 SpringBoot集成支付宝支付 - OXOXTECH 牛牛技术客栈 雪花算法:分布式系统唯一ID生成算法 - OXOXTECH 牛牛技术客栈 Java解决空指针的神器Optional - OXOXTECH 牛牛技术客栈 与兴一起 - OXOXTECH 牛牛技术客栈 Java17新特性详解与安装 - OXOXTECH 牛牛技术客栈 Jdk17安装+环境配置详细教程 - OXOXTECH 牛牛技术客栈 孤注一掷 - OXOXTECH 牛牛技术客栈 解决WinSCP经常断线重连 - OXOXTECH 牛牛技术客栈 内存不足导致Tomcat崩溃问题排查与解决办法 - OXOXTECH 牛牛技术客栈 influxDB初识,一个高效的时序数据库 - OXOXTECH 牛牛技术客栈 SpringBoot 服务接口限流方案 - OXOXTECH 牛牛技术客栈 Docker 安装 Portainer - OXOXTECH 牛牛技术客栈 Linux 安装Docker - OXOXTECH 牛牛技术客栈 物料宣传 - OXOXTECH 牛牛技术客栈 Java使用EMQX实现MQTT通信 - OXOXTECH 牛牛技术客栈 Java实现常见的排序算法 - OXOXTECH 牛牛技术客栈 FreeSwitch Windows安装教程 - OXOXTECH 牛牛技术客栈 MQTT单向SSL数据加密 - OXOXTECH 牛牛技术客栈 随性 - OXOXTECH 牛牛技术客栈 mysql报错Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre的解决方案 Git Push项目报 push to origin/master was rejected 错误解决方案 游行记——珠海金沙滩与金湖公园之行 - OXOXTECH 牛牛技术客栈 Tomcat:解决Tomcat启动警告:"无法将资源添加到Web应用程序缓存中....请考虑增加缓存空间" 的问题 - OXOXTECH 牛牛技术客栈 励骏庞都广场,迷一般的皇宫 - OXOXTECH 牛牛技术客栈 Docker 常用命令集合 - OXOXTECH 牛牛技术客栈 ElasticSearch Windows版-安装教程 - OXOXTECH 牛牛技术客栈 Java去除对象中为null的字段 - OXOXTECH 牛牛技术客栈 我和我的青春 - OXOXTECH 牛牛技术客栈 Java实现螺旋矩阵算法: - OXOXTECH 牛牛技术客栈 Java直接内存分配和释放的理解 - OXOXTECH 牛牛技术客栈 FreeSwitch将默认数据库迁移至MySQL - OXOXTECH 牛牛技术客栈 别错过路上的风景,别错过刹那间的深情! - OXOXTECH 牛牛技术客栈 Viewer.js:一款强大的图片预览组件 - OXOXTECH 牛牛技术客栈 Java JDK Proxy和CGLib动态代理示例 - OXOXTECH 牛牛技术客栈 redis常用命令 - OXOXTECH 牛牛技术客栈 SpringBoot查询IP归属地 - OXOXTECH 牛牛技术客栈 Spring 事务失效的六种情况 - OXOXTECH 牛牛技术客栈 #张艺兴每时每刻# - OXOXTECH 牛牛技术客栈 Nginx常用命令及具体应用 - OXOXTECH 牛牛技术客栈
Netty TCP解决粘包拆包 - OXOXTECH 牛牛技术客栈
军阁 · 2023-10-27 · via 牛牛技术客栈

1、前言

TCP(Transmission Control Protocol)是一种在计算机网络中广泛使用的协议,用于可靠的、面向连接的数据通信。

20231020154532894.webp

  1. 可靠性:TCP是一种可靠的协议,它确保数据从发送方传输到接收方,无丢失、无损坏、有序传递。如果发生数据包的丢失或损坏,TCP会重新发送这些数据包,直到接收方成功接收。
  2. 面向连接:TCP是一种面向连接的协议,这意味着在数据传输之前,发送方和接收方需要建立一个连接,数据在连接上传输。传输完成后,连接会被释放。
  3. 全双工通信:TCP支持全双工通信,这意味着在建立连接后,双方可以同时发送和接收数据。
  4. 流式传输:TCP提供了一种流式传输服务,数据被划分为小的数据块,以字节流的方式传输。接收方根据数据块的边界进行数据的解析。
  5. 拥塞控制:TCP包含拥塞控制机制,用于避免过多的数据流量导致网络拥塞。TCP发送方会根据网络的拥塞情况来动态调整数据传输速度。
  6. 校验和:TCP使用校验和来检测数据的完整性。接收方会验证数据的校验和,以确保数据在传输过程中没有发生错误。
  7. 有序传输:TCP保证数据包的有序传输,即使数据包到达的顺序与发送的顺序不同,接收方也会对其进行重新排序。

2、粘包与拆包现象

由于TCP是面向流式传输的一种协议,所以就像水管里的水一样,无界限的传输。然而在接收端,数据可能以不同的方式到达,就比如正常包粘包、拆包。

2.1、现象描述

假设客户端发送2个连续的数据包到服务器,数据包用packet1packet2分别表示,则服务器接收到的数据可以分为3种情况:

情况1: 服务器接收到2个数据包,没有拆包,也没有粘包问题;

202310201457267796.webp

情况2: 服务器只接收到一个数据包(存在粘包问题)

  • 因为tcp不会丢失数据包,因此这一个数据包就封装了2个原生数据包的信息,这种现象叫做粘包
  • 在这种情况,接收者并不知道2个原生包的界限,因此接收者很难处理;

202310201458212759.webp

情况3: 接收者接收到2个冗余或不完整的数据包(粘包与拆包问题同时发生)

  • 接收者接收到2个数据包,但这2个数据包要么不完整,要么掺杂了其他数据包的部分数据
  • 在这种情况下,粘包拆包同时发生。
  • 如果这2个包不被特殊处理,对于接收者来说也很难处理;

202310201459125302.webp

2.2、代码演示粘包拆包现象

业务场景:客户端连续发送10条消息(字符串)到服务器,查看服务器接收情况

客户端发送消息代码:

202310201502516428.webp

服务器接收消息代码:

202310201503055952.webp

接收消息的打印效果:

=================================
服务器收到的数据 hello server0
 
服务器累计收到 [1] 个消息包
=================================
服务器收到的数据 hello server1
 
服务器累计收到 [2] 个消息包
=================================
服务器收到的数据 hello server2
hello server3
hello server4
hello server5
hello server6
 
服务器累计收到 [3] 个消息包
=================================
服务器收到的数据 hello server7
hello server8
hello server9
 
服务器累计收到 [4] 个消息包

解释:

  • 客户端发送了10条消息,服务器接收到了 4个数据包,而不是10个数据包 ;
  • 显然,发生了tcp粘包;
  • 这10条消息本来是10个数据报文,却被合并(粘)为4个数据包;
  • 问题是: 如何把这4个数据包还原为10个数据包呢 (在高并发情况下,各式各样的数据包会更多)
  • 如果无法还原,则服务器无法正确解析报文并做相应处理;

3、粘包与拆包主要原因

1、粘包原因:

发送的数据大小 小于 发送缓冲区,TCP就会把发送的数据多次写入缓冲区,此时发生粘包

接收数据方的应用层没有及时从 接收缓冲区读取数据,也会发生粘包

2、拆包原因:

发送的数据大小 大于 TCP发送缓冲区,就会发生拆包

发送的数据大小 大于 报文最大长度,也会拆包

4、粘包与拆包解决方法

解决粘包拆包的关键在于 为每一个数据包添加界限标识,一般常用的方法如下:

方法1发送方为每一个数据包添加报文头部。头部至少包含数据包长度(类似http协议的头部length)。 通过这种方式,接收方通过读取头部的长度知道当前数据包的界限,并在界限处停止读取。

方法2:发送方以固定长度封装数据包。如果不足,则补0填充。

方法3:自定义设置数据包的界限标识,如添加特别标识(如#)。接收方通过标识可以识别不同的数据包;

5、代码实现

这里的解决方法是采用方法1,设置每个数据包的长度到报文头部;

5.1、协议数据包封装类

/**
 * @Description 协议数据包

 */
public class ProtocolMessage {
 
    private int length;
    private byte[] content;
 
    /**
     * @description 构造器

     */
    public ProtocolMessage() {
    }
 
    public int getLength() {
        return length;
    }
 
    public void setLength(int length) {
        this.length = length;
    }
 
    public byte[] getContent() {
        return content;
    }
 
    public void setContent(byte[] content) {
        this.content = content;
    }
}

5.2、Netty服务端

public class ProtocolNettyServer89 {
 
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ProtocolNettyServerInitializer()); // 自定义一个初始化类
            // 自动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(8089).sync();
            System.out.println("服务器启动成功");
            // 监听关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

初始化类 ProtocolNettyServerInitializer()

public class ProtocolNettyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加入站解码器-把字节转为协议报文便于业务逻辑处理
        pipeline.addLast(new ProtocolMessageDecoder());
        // 添加出站编码器-把协议报文转为字节便于网络传输
        pipeline.addLast(new ProtocolMessageEncoder());
 
        // 添加业务逻辑handler
        pipeline.addLast(new ProtocolNettyServerHandler());
    }
}

处理类 ProtocolNettyServerHandler()

public class ProtocolNettyServerHandler extends SimpleChannelInboundHandler<ProtocolMessage> {
 
    private int count = 0;
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) throws Exception {
        // 接收到数据并处理
        int length = msg.getLength();
        String bodyStr = new String(msg.getContent(), StandardCharsets.UTF_8);
        System.out.println("====================================");
        System.out.println("服务器接收的消息如下:");
        System.out.println("报文长度:" + length);
        System.out.println("报文体内容: " + bodyStr);
        System.out.println("服务器累计接收到的消息包数量 = " + ++this.count);
 
        //  回复客户端
        byte[] body = ("我是服务器" + count).getBytes(StandardCharsets.UTF_8);
        int responseLen = body.length;
        // 构建一个响应协议包
        ProtocolMessage responseMsg = new ProtocolMessage();
        responseMsg.setLength(responseLen);
        responseMsg.setContent(body);
        ctx.writeAndFlush(responseMsg);
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

5.3、Netty客户端

public class ProtocolNettyClient89 {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ProtocolNettyClientInitializer());  // 自定义一个初始化类
            // 连接服务器
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8089).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

初始化类 ProtocolNettyClientInitializer()

public class ProtocolNettyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 添加出站处理器- 协议报文转字节以便网络传输
        pipeline.addLast(new ProtocolMessageEncoder());
        // 添加入站解码器-把字节转为协议报文对象以便业务逻辑处理
        pipeline.addLast(new ProtocolMessageDecoder());
 
        // 添加一个自定义handler,处理业务逻辑
        pipeline.addLast(new ProtocolNettyClientHandler());
    }
}

处理类 ProtocolNettyClientHandler()

public class ProtocolNettyClientHandler extends SimpleChannelInboundHandler<ProtocolMessage> {
    private int count;
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) throws Exception {
        // 读取服务器响应报文
        int length = msg.getLength();
        byte[] body = msg.getContent();
        System.out.println("=============================");
        System.out.println("客户端接收的消息如下:");
        System.out.println("长度 = " + length);
        System.out.println("报文体 = " + new String(body, StandardCharsets.UTF_8));
        System.out.println("客户端累计接收的消息包数量 = " + ++count);
    }
 
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 发送10条数据到服务器
        for (int i = 1; i <= 5; i++) {
            byte[] body = ("你好服务器,我是客户端张三" + i).getBytes(StandardCharsets.UTF_8);
            // 创建协议包对象
            ProtocolMessage message = new ProtocolMessage();
            message.setContent(body);
            message.setLength(body.length);
            // 发送
            ctx.writeAndFlush(message);
        }
    }
 
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

5.4、编码类与解码类

解码类 ProtocolMessageDecoder():

/**
 * @Description 协议报文解码器

 */
public class ProtocolMessageDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("ProtocolMessageDecoder.decode() 被调用");
        //  把字节 转为 协议报文
        int length = in.readInt();
        byte[] body = new byte[length];
        in.readBytes(body);
        // 封装成 ProtocolMessage,放入out,送入下一个 Handler处理
        ProtocolMessage protocolMessage = new ProtocolMessage();
        protocolMessage.setLength(length);
        protocolMessage.setContent(body);
        // 添加到out
        out.add(protocolMessage);
    }
}

编码类 ProtocolMessageEncoder ():

/**
 * @Description 协议消息编码器

 */
public class ProtocolMessageEncoder extends MessageToByteEncoder<ProtocolMessage> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf out) throws Exception {
        System.out.println("ProtocolMessageEncoder.encode() 被调用");
        out.writeInt(msg.getLength());
        out.writeBytes(msg.getContent());
    }
}

6、再次测试

客户端发送5条消息到服务器:

202310201521035351.webp

服务器接收的数据包为 5个,如下(显然没有发生拆包粘包现象):

ProtocolMessageDecoder.decode() 被调用
====================================
服务器接收的消息如下:
报文长度:40
报文体内容: 你好服务器,我是客户端张三1
服务器累计接收到的消息包数量 = 1
ProtocolMessageEncoder.encode() 被调用
ProtocolMessageDecoder.decode() 被调用
====================================
服务器接收的消息如下:
报文长度:40
报文体内容: 你好服务器,我是客户端张三2
服务器累计接收到的消息包数量 = 2
ProtocolMessageEncoder.encode() 被调用
ProtocolMessageDecoder.decode() 被调用
====================================
服务器接收的消息如下:
报文长度:40
报文体内容: 你好服务器,我是客户端张三3
服务器累计接收到的消息包数量 = 3
ProtocolMessageEncoder.encode() 被调用
ProtocolMessageDecoder.decode() 被调用
====================================
服务器接收的消息如下:
报文长度:40
报文体内容: 你好服务器,我是客户端张三4
服务器累计接收到的消息包数量 = 4
ProtocolMessageEncoder.encode() 被调用
ProtocolMessageDecoder.decode() 被调用
====================================
服务器接收的消息如下:
报文长度:40
报文体内容: 你好服务器,我是客户端张三5
服务器累计接收到的消息包数量 = 5
ProtocolMessageEncoder.encode() 被调用

7、结尾

以上内容就是netty使用数据包添加报文头部的方式解决粘包拆包的现象

感谢大佬提供参考: https://blog.csdn.net/PacosonSWJTU/article/details/126798838?spm=1001.2101.3001.6650.15&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-15-126798838-blog-119154039.235%5Ev38%5Epc_relevant_sort_base1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-15-126798838-blog-119154039.235%5Ev38%5Epc_relevant_sort_base1&utm_relevant_index=22