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

news2024/11/15 13:41:07

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>() {
				@Override
				protected 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>() {
				@Override
				protected void initChannel(SocketChannel channel) throws Exception {
					channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
						// 会在连接 channel 成功后,触发active 事件
						@Override
						public 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});
								// 写入channel
								channel.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>() {
				@Override
				protected void initChannel(SocketChannel channel) throws Exception {
					channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
						// 会在连接 channel 成功后,触发active 事件
						@Override
						public 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>() {
				@Override
				protected void initChannel(SocketChannel channel) throws Exception {
					channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
						// 会在连接 channel 成功后,触发active 事件
						@Override
						public 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1486245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux多线程控制:深入理解与应用(万字详解!)

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;どうして (feat. 野田愛実) 0:44━━━━━━️&#x1f49f;──────── 3:01 &#x1f504; ◀️ ⏸ ▶️ …

基于redis实现【最热搜索】和【最近搜索】功能

目录 一、前言二、分析问题三、针对两个问题&#xff0c;使用redis怎么解决问题&#xff1f;1、字符串String2、列表List3、字典Hash4、集合Set5、有序集合ZSet6、需要解决的五大问题 四、编写代码1.pom依赖2.application.yml配置3.Product商品实体4.用户最近搜索信息5.redis辅…

C-V2X系列:C-V2X芯片及模组整理总结

C-V2X、车路协同、车联网、智能网联车学习 C-V2X芯片及模组整理总结

Typora旧版链接(Win+Mac+Linux版)

记得点赞本文&#xff01;&#xff01;&#xff01; 链接&#xff1a;https://pan.baidu.com/s/1IckUvQUBzQkfHNHXla0zkA?pwd8888 提取码&#xff1a;8888 –来自百度网盘超级会员V7的分享

2.模拟问题——4.日期问题

日期问题难度并不大&#xff0c;但是代码量非常大&#xff0c;需要较高的熟练度&#xff0c;因此需要着重练习&#xff0c;主要涉及数组和循环两个方面的知识点&#xff0c;需要熟练的测试代码。 两个经典题型 闰年 闰年满足以下两个条件的任意一个 能够被400整除不能够被1…

Golang Vs Java:为您的下一个项目选择正确的工具

Java 首次出现在 1995 年&#xff0c;由 James Gosling 和 Sun Microsystems 的其他人开发的一种新编程语言。从那时起&#xff0c;Java 已成为世界上最受欢迎和广泛使用的编程语言之一。Java 的主要特点包括其面向对象的设计、健壮性、平台独立性、自动内存管理以及广泛的内置…

JavaSec 基础之 JNDI 注入

文章目录 JNDI简介JNDI 支持的服务协议JNDI 注入JNDI 复现修复 JNDI 简介 JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API&#xff0c;一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API&#xff0c;通过不同的访问提供者接口JNDI服务供应接口(…

武器大师——操作符详解(下)

目录 六、单目操作符 七、逗号表达式 八、下标引用以及函数调用 8.1.下标引用 8.2.函数调用 九、结构体 9.1.结构体 9.1.1结构的声明 9.1.2结构体的定义和初始化 9.2.结构成员访问操作符 9.2.1直接访问 9.2.2间接访问 十、操作符的属性 10.1.优先性 10.2.结合性 …

Ubuntu20.04使用XRDP安装原生远程桌面

Ubuntu20.04使用XRDP安装原生远程桌面 1.安装gnome桌面 # 如果没有更新过源缓存&#xff0c;先更新一下 sudo apt update# 安装gnome桌面 # 可选参数 --no-install-recommends&#xff0c;不安装推荐组件&#xff0c;减少安装时间和空间占用 sudo apt install ubuntu-desktop…

2.2_5 调度算法

文章目录 2.2_5 调度算法一、适用于早期的批处理系统&#xff08;一&#xff09;先来先服务&#xff08;FCFS&#xff0c;First Come First Serve&#xff09;&#xff08;二&#xff09;短作业优先&#xff08;SJF&#xff0c;Shortest Job First&#xff09;&#xff08;三&a…

力扣706:设计哈希映射

题目&#xff1a; 不使用任何内建的哈希表库设计一个哈希映射&#xff08;HashMap&#xff09;。 实现 MyHashMap 类&#xff1a; MyHashMap() 用空映射初始化对象void put(int key, int value) 向 HashMap 插入一个键值对 (key, value) 。如果 key 已经存在于映射中&#x…

设计模式学习笔记 - 设计原则 - 8.迪米特法则(LOD)

前言 迪米特法则&#xff0c;是一个非常实用的原则。利用这个原则&#xff0c;可以帮我们实现代码的 “高内聚、松耦合”。 围绕下面几个问题&#xff0c;来学习迪米特原则。 什么是 “高内聚、松耦合”&#xff1f;如何利用迪米特法则来实现 高内聚、松耦合&#xff1f;哪些…

【python debug】python常见编译问题解决方法_2

序言 记录python使用过程中碰到的一些问题及其解决方法上一篇&#xff1a;python常见编译问题解决方法_1 1. PermissionError: [Errno 13] Permission denied: ‘/lostfound’ 修改前&#xff1a; 修改后&#xff08;解决&#xff09;&#xff1a; 此外&#xff0c;可能文件夹…

开发者38万+,鸿蒙开发岗为何却无人敢应聘?

鸿蒙校园公开课已走进135家高校&#xff0c;305所高校学生参与鸿蒙活动&#xff0c;286家企业参加鸿蒙生态学堂&#xff0c;38万开发者通过鸿蒙认证。 居上华为官方是说有通过鸿蒙开发者认证的已有38万。具体有多少开发者并没有明确表示。除此之外还有200家头部应用加速鸿蒙原…

机器人 标准DH与改进DH

文章目录 1 建立机器人坐标系1.1 连杆编号1.2 关节编号1.3 坐标系方向2 标准DH(STD)2.1 确定X轴方向2.2 建模步骤2.3 变换顺序2.4 变换矩阵3 改进DH(MDH)3.1 确定X轴方向3.2 建模步骤3.3 变换顺序3.4 变换矩阵4 标准DH与改进DH区别5 Matlab示例参考链接1 建立机器人坐标系 1.1…

Java二叉树(1)

&#x1f435;本篇文章将对二叉树的相关概念、性质和遍历等知识进行讲解 一、什么是树 在讲二叉树之前&#xff0c;先了解一下什么是树&#xff1a;树是一种非线性结构&#xff0c;其由许多节点和子节点组成&#xff0c;整体形状如一颗倒挂的树&#xff0c;比如下图&#xff1…

探索设计模式的魅力:备忘录模式揭秘-实现时光回溯、一键还原、后悔药、历史的守护者和穿越时空隧道

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;并且坚持默默的做事。 备忘录模式揭秘-实现时光回溯、一键还原、后悔药和穿越时空隧道 文章目录 一、案例场景&…

Docker架构概述

Docker是基于Go语言实现的开源容器项目&#xff0c;能够把开发的应用程序自动部署到容器的开源的应用容器引擎。Docker的构想是要实现"Build, Ship and Run Any App, Anywhere"&#xff0c;即通过对应用的封装(Packaging)、分发(Distribution)、部署(Deployment)、运…

Autosar Appl介绍

AUTOSAR架构中的应用层 AUTOSAR 应用层构成AUTOSAR 架构中的最顶层,被认为对所有车辆应用至关重要。AUTOSAR 标准使用“组件”概念指定应用层实现。 在谈论应用层实现时,应该考虑的三个最重要的部分是: AUTOSAR 应用软件组件这些组件的 AUTOSAR 端口AUTOSAR 端口接口 AUTOS…

LeetCode受限条件下可到达节点的数目

题目描述 现有一棵由 n 个节点组成的无向树&#xff0c;节点编号从 0 到 n - 1 &#xff0c;共有 n - 1 条边。 给你一个二维整数数组 edges &#xff0c;长度为 n - 1 &#xff0c;其中 edges[i] [ai, bi] 表示树中节点 ai 和 bi 之间存在一条边。另给你一个整数数组 restr…