从零开始学习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

相关文章

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

Java List 使用举例(从入门到精通)

《JavaList使用举例(从入门到精通)》本文系统讲解JavaList,涵盖基础概念、核心特性、常用实现(如ArrayList、LinkedList)及性能对比,介绍创建、操作、遍历方法,结合实... 目录一、List 基础概念1.1 什么是 List?1.2 List 的核心特性1.3 List 家族成

Python学习笔记之getattr和hasattr用法示例详解

《Python学习笔记之getattr和hasattr用法示例详解》在Python中,hasattr()、getattr()和setattr()是一组内置函数,用于对对象的属性进行操作和查询,这篇文章... 目录1.getattr用法详解1.1 基本作用1.2 示例1.3 原理2.hasattr用法详解2.

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

史上最全MybatisPlus从入门到精通

《史上最全MybatisPlus从入门到精通》MyBatis-Plus是MyBatis增强工具,简化开发并提升效率,支持自动映射表名/字段与实体类,提供条件构造器、多种查询方式(等值/范围/模糊/分页... 目录1.简介2.基础篇2.1.通用mapper接口操作2.2.通用service接口操作3.进阶篇3

Python自定义异常的全面指南(入门到实践)

《Python自定义异常的全面指南(入门到实践)》想象你正在开发一个银行系统,用户转账时余额不足,如果直接抛出ValueError,调用方很难区分是金额格式错误还是余额不足,这正是Python自定义异... 目录引言:为什么需要自定义异常一、异常基础:先搞懂python的异常体系1.1 异常是什么?1.2

Python实现Word转PDF全攻略(从入门到实战)

《Python实现Word转PDF全攻略(从入门到实战)》在数字化办公场景中,Word文档的跨平台兼容性始终是个难题,而PDF格式凭借所见即所得的特性,已成为文档分发和归档的标准格式,下面小编就来和大... 目录一、为什么需要python处理Word转PDF?二、主流转换方案对比三、五套实战方案详解方案1:

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与

Spring Boot 与微服务入门实战详细总结

《SpringBoot与微服务入门实战详细总结》本文讲解SpringBoot框架的核心特性如快速构建、自动配置、零XML与微服务架构的定义、演进及优缺点,涵盖开发环境准备和HelloWorld实战... 目录一、Spring Boot 核心概述二、微服务架构详解1. 微服务的定义与演进2. 微服务的优缺点三