从零开始学习Netty - 学习笔记 -Netty入门【半包,黏包】

2024-03-04 05:44

本文主要是介绍从零开始学习Netty - 学习笔记 -Netty入门【半包,黏包】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Netty进阶

1.黏包半包

1.1.黏包

服务端代码

public class HelloWorldServer {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(bossGroup, workerGroup);serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("server error !", e);} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}

客户端代码

public class HelloWorldClient {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 会在连接 channel 成功后,触发active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 连接建立后,模拟发送数据,每次发送 16个字节 一共发送 10 次for (int i = 0; i < 10; i++) {ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});// 写入channelchannel.writeAndFlush(buffer);}}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}
}

image-20240302100630626

半包

需要对服务端 和客户端 的代码稍微修改下

// 设置每次接收缓冲区的大小,所以但是客户端每次发送的是16个字节 所以可以模拟半包情况
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);// 注意 如果不生效的话,建议服务端也设置响应的缓冲区大小
// 设置发送方缓冲区大小
bootstrap.option(ChannelOption.SO_SNDBUF, 10);

image-20240302102147457

1.2.滑动窗口

TCP以一个段(segment)为单位,每次发送一个段就需要进行一次确认应答(ACK),为了保证消息传输过程的稳定性,但是这样做的缺点就是会导致包的往返时间越长,性能就越差。

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

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用

    • 图中深色的部门表示即将要发送的数据,高亮的部分就是窗口
    • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
    • 如果1001 - 2000 这个段的数据ACK回来了,窗口就可以向前滑动
    • 接收方也会维护一个窗口,只有落在窗口内的数据才允许接收

1.3.黏包半包现象分析

  1. 黏包
    • 现象
      • 发送 abc def 接收 abdcef
    • 原因
      • 应用层:接收方ByteBuf设置太大(Netty默认1024)
      • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但是由于接收方处理不及时,且窗口大小足够大,这256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口缓冲了多个报文就会黏包
      • Nagle算法:会造成黏包
  2. 半包
    • 现象:发送 abcefg 接收方 abc efg
    • 原因
      • 应用层:接收方ByteBuf 设置容量大小,小于实际发送的数据量
      • 滑动窗口:假设接收方的窗口只剩下了,128byte,发送方的报文大小是 256 byte,这时就会放不下,只能先发送 128 byte数据,然后等待ack确认后,才能发送剩下的部门,这时就造成了半包。
      • MSS限制:当发送的数据超过了MSS的限制后,会将数据切割,然后分批发送,就会造成半包
        • 为什么在数据传输截断存在数据分割呢?一个TCP报文的有效数据(净荷数据)是有大小容量限制的,这个报文有效数据的大小就被称为**MSS(Mixinum Segment Size) 最大报文字段长度**。具体MSS的值会在三次握手阶段进行协商,但是最大长度不会超过**1460**个字节

出现黏包半包的主要原因就是 TCP的消息没有边界

1.4.黏包半包解决

1.4.1.短链接(解决黏包)

客户端发送完后立马进行断开

短链接并不能半包问题

短链接虽然能解决黏包问题,但是缺点也是很明显的

  • 连接建立开销高,因为需要进行握手等操作。
  • 频繁的连接管理会增加服务器负担。
  • 可能导致资源浪费,如 TCP 连接的建立和释放。
  • 存在网络拥塞风险,特别是在高并发情况下。
  • 难以维护状态,增加开发和维护的复杂性。
public class HelloWorldClient {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {// 短链接发发送for (int i = 0; i < 10; i++) {shortLinkedSend();}}/*** 短链接发送 测试*/private static void shortLinkedSend() {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);// 设置发送方缓冲区大小bootstrap.option(ChannelOption.SO_SNDBUF, 10);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 会在连接 channel 成功后,触发active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 连接建立后,模拟发送数据ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});// 发送数据ctx.writeAndFlush(buffer);// 主动断开链接ctx.channel().close();}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}}

image-20240302135845886

image-20240302140556134

1.4.2.定长解码器
  • 固定长度限制:消息长度必须是固定的,这限制了处理可变长度消息的能力。
  • 资源浪费:对于短消息,会浪费网络带宽和系统资源。
  • 消息边界问题:无法处理不符合固定长度的消息,可能导致解码器阻塞或消息边界错误。
  • 不适用于多种消息类型:无法处理多种长度不同的消息类型。
  • 性能影响:对于长消息,可能会影响性能。

客户端代码

	public static void main(String[] args) {fixedLengthDecoder();}/*** 定长解码器 测试*/private static void fixedLengthDecoder () {NioEventLoopGroup worker = new NioEventLoopGroup();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);// 设置发送方缓冲区大小bootstrap.option(ChannelOption.SO_SNDBUF, 10);bootstrap.group(worker);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {// 会在连接 channel 成功后,触发active 事件@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {super.channelActive(ctx);// 连接建立后,模拟发送数据ByteBuf buffer = ctx.alloc().buffer(16);for (int i = 0; i < 10; i++) {String s = "hello," + new Random().nextInt(100000000);logger.error("send data:{}", s);buffer.writeBytes(fillString(16, s));}// 发送数据ctx.writeAndFlush(buffer);}});}});ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (Exception e) {logger.error("client error!");} finally {worker.shutdownGracefully();}}/*** 编写要给方法 给定一个长度,和数值,* 例如长度 16  数值 abc 剩下的填充**/private static byte[] fillString(int length, String value) {if (value.length() > length) {return value.substring(0, length).getBytes();}StringBuilder sb = new StringBuilder(value);for (int i = 0; i < length - value.length(); i++) {sb.append("*");}return sb.toString().getBytes();}

服务端

服务端的代码没有太大改动

@Override
protected void initChannel(SocketChannel channel) throws Exception {// 在打印日志前添加了定长解码器// 添加定长解码器 16  消息长度必须发送方 和 接收方一致// 注意顺序,必须要先解码,然后才能打印日志channel.pipeline().addLast(new FixedLengthFrameDecoder(16));channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}

image-20240302142715768

image-20240302142842684

1.4.3.行解码器(分隔符)

\r \r\n

客户端

这里的客户端 代码 和上面一致,我们只针对客户端消息代码进行修改

// 每次发送消息的结尾加上换行符
String s = "hello," + new Random().nextInt(100000000) + "\n";

服务端

用的不多

// 添加行解码器,设置每次接收的数据大小
// 注意顺序,必须要先解码,然后才能打印日志
channel.pipeline().addLast(new LineBasedFrameDecoder(1024));

image-20240302151110375

1.4.4.LTC解码器

LengthFieldBasedFrameDecoder方法的工作原理以及各个参数的含义:

  1. maxFrameLength(最大帧长度):这个参数指定了一个帧的最大长度。当接收到的帧长度超过这个限制时,解码器会抛出一个异常。设置一个适当的最大帧长度可以防止你的应用程序受到恶意或错误消息的影响。
  2. lengthFieldOffset(长度字段偏移量):这个参数表示长度字段的偏移量,也就是在接收到的字节流中,长度字段从哪里开始的位置。通常,这个偏移量是相对于字节流的起始位置而言的。
  3. lengthFieldLength(长度字段长度):这个参数指定了长度字段本身所占用的字节数。在接收到的字节流中,长度字段通常是一个固定长度的整数,用来表示消息的长度。
  4. lengthAdjustment(长度调整值):在某些情况下,长度字段可能包括了消息头的长度,而不是整个消息的长度。这个参数允许你进行一些调整,以便准确地计算出消息的实际长度。
  5. initialBytesToStrip(要剥离的初始字节数):在解码器将帧传递给处理器之前,会先从帧中剥离一些字节。通常,这些字节是长度字段本身,因为处理器只需要处理消息的有效负载部分。这个参数告诉解码器要剥离的初始字节数。
Client 客户端 LengthFieldBasedFrameDecoder NextHandler 下一个处理器 Network 网络 发送字节流 接收字节流 读取长度字段 解析长度字段来确定消息的长度 返回等待更多数据 读取完整消息 传递完整消息给下一个处理器 alt [消息长度不足] [消息长度足够] loop [消息解析过程] 处理完整的消息 Client 客户端 LengthFieldBasedFrameDecoder NextHandler 下一个处理器 Network 网络

假设有一个网络协议,它的消息格式如下:

  • 消息长度字段占据前4个字节。
  • 长度字段之后是实际的消息内容。

现在假设你收到了一个包含以上格式的字节流。你希望用Netty的LengthFieldBasedFrameDecoder来解码这个消息。

在这种情况下,你需要设置以下参数:

  • lengthFieldOffset: 偏移量为0,因为长度字段从消息的开头开始。
  • lengthFieldLength: 长度字段本身是4个字节。
  • lengthAdjustment: 在这种情况下,长度字段表示的是消息内容的长度,不包括长度字段本身,所以这个值是0。
  • initialBytesToStrip: 需要剥离长度字段本身,也就是4个字节。(因为用4个字节表示了字段的长度)

假设你收到的字节流如下:

[消息长度字段] [消息内容]
[0, 0, 0, 5] [72, 101, 108, 108, 111]
  • 长度字段 [0, 0, 0, 5] 表示消息长度为5个字节。
  • 后面的5个字节 [72, 101, 108, 108, 111] 则是实际的消息内容,代表着 “Hello”。

LengthFieldBasedFrameDecoder 将会将这个字节流解析成一条消息,其中包含了 “Hello” 这个字符串。

测试

public class TestLengthFiledDecoder {private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());public static void main(String[] args) {// 创建一个 EmbeddedChannel 并添加一个 LengthFieldBasedFrameDecoder// 该解码器会根据长度字段的值来解码数据// EmbeddedChannel 是一个用于测试的 Channel 实现EmbeddedChannel channel = new EmbeddedChannel(/** maxFrameLength: 最大帧长度* lengthFieldOffset: 长度字段的偏移量* lengthFieldLength: 长度字段的长度* lengthAdjustment: 长度字段的值表示的长度与整个帧的长度之间的差值(如果消息后面再加上一个长度字段,那么这个字段的值就是lengthAdjustment*  sendInfo("Netty",buffer);后面再加上一个长度字段,那么这个字段的值就是lengthAdjustment) 不加会报错* initialBytesToStrip: 解码后的数据需要跳过的字节数*/new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4),new LoggingHandler(LogLevel.DEBUG));ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();// 4 个字节内容的长度 实际内容sendInfo("Hello,World111111111111111111111111111111111", buffer);sendInfo("Hello", buffer);sendInfo("Netty",buffer);// 模拟写入数据channel.writeInbound(buffer);}private static void sendInfo(String s, ByteBuf buffer) {byte[] bytes = s.getBytes();// 写入内容 大端模式 写入长度 4 个字节int length = bytes.length;buffer.writeInt(length);buffer.writeBytes(bytes);}
}

image-20240302170706970

image-20240302191215747

这篇关于从零开始学习Netty - 学习笔记 -Netty入门【半包,黏包】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/772171

相关文章

从入门到精通MySQL联合查询

《从入门到精通MySQL联合查询》:本文主要介绍从入门到精通MySQL联合查询,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下... 目录摘要1. 多表联合查询时mysql内部原理2. 内连接3. 外连接4. 自连接5. 子查询6. 合并查询7. 插入查询结果摘要前面我们学习了数据库设计时要满

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

从入门到精通MySQL 数据库索引(实战案例)

《从入门到精通MySQL数据库索引(实战案例)》索引是数据库的目录,提升查询速度,主要类型包括BTree、Hash、全文、空间索引,需根据场景选择,建议用于高频查询、关联字段、排序等,避免重复率高或... 目录一、索引是什么?能干嘛?核心作用:二、索引的 4 种主要类型(附通俗例子)1. BTree 索引(

Redis 配置文件使用建议redis.conf 从入门到实战

《Redis配置文件使用建议redis.conf从入门到实战》Redis配置方式包括配置文件、命令行参数、运行时CONFIG命令,支持动态修改参数及持久化,常用项涉及端口、绑定、内存策略等,版本8... 目录一、Redis.conf 是什么?二、命令行方式传参(适用于测试)三、运行时动态修改配置(不重启服务

MySQL DQL从入门到精通

《MySQLDQL从入门到精通》通过DQL,我们可以从数据库中检索出所需的数据,进行各种复杂的数据分析和处理,本文将深入探讨MySQLDQL的各个方面,帮助你全面掌握这一重要技能,感兴趣的朋友跟随小... 目录一、DQL 基础:SELECT 语句入门二、数据过滤:WHERE 子句的使用三、结果排序:ORDE

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

详解如何使用Python从零开始构建文本统计模型

《详解如何使用Python从零开始构建文本统计模型》在自然语言处理领域,词汇表构建是文本预处理的关键环节,本文通过Python代码实践,演示如何从原始文本中提取多尺度特征,并通过动态调整机制构建更精确... 目录一、项目背景与核心思想二、核心代码解析1. 数据加载与预处理2. 多尺度字符统计3. 统计结果可

Python中OpenCV与Matplotlib的图像操作入门指南

《Python中OpenCV与Matplotlib的图像操作入门指南》:本文主要介绍Python中OpenCV与Matplotlib的图像操作指南,本文通过实例代码给大家介绍的非常详细,对大家的学... 目录一、环境准备二、图像的基本操作1. 图像读取、显示与保存 使用OpenCV操作2. 像素级操作3.