第五章Netty第一节 粘包和半包

2023-12-24 03:04

本文主要是介绍第五章Netty第一节 粘包和半包,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

粘包与半包

粘包

现象:发送abc def,接受到abcdef
原因:

  • 应用层:接收方ByteBuf设置太大(Netty默认是1024)
  • 传输层滑动窗口: 假设发送方256 bytes表示一个完整的报文,接收方的滑动窗口来不及处理且滑动窗口足够大,导致多个报文在一个滑动窗口中,导致粘包。(TCP协议)
  • Nagle算法:会造成粘包。Nagle算法是TCP协议中的一种机制,有时候发送一个字节,也需要加入TCP头和IP头,有点浪费网络。为了提高网络利用率,会将少量数据进行延迟发送,积攒起来一起发送。会造成粘包现象。

半包

现象:发送abcdef,接受abc def
原因:

  • 应用层:接收方的ByteBuf小于实际发送的数据,导致一个数据报文被拆分了。
  • 传输层滑动窗口:假设接收方的滑动窗口大小为128bytes,这时候发送方发了256bytes,滑动窗口接受不过来,让发送方只能先发128bytes,等待ACK后,才能发送剩余的数据。造成一个完整的报文被拆分开。(传输层TCP协议)
  • MSS(max segment size)限制:当发送的数据超出MSS限制后,会将数据切分后发送,会造成半包。
    本质上是因为TCP协议是基于字节流的(首部没有长度),消息没有边界,所以会造成粘包和半包现象。UDP是面向报文的(首部有长度),所以不会有粘包和半包现象。

解决方案

方法1 短链接

public class HelloWorldClient {static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);public static void main(String[] args) {// 分 10 次发送for (int i = 0; i < 10; i++) {send();}}private static void send() {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 ch) throws Exception {log.debug("conneted...");ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.debug("sending...");ByteBuf buffer = ctx.alloc().buffer();buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});ctx.writeAndFlush(buffer);// 发完即关ctx.close();}});}});ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error", e);} finally {worker.shutdownGracefully();}}
}

短链接相当于发送一个数据就断开一次。只能解决粘包。接受的ByteBuf还是有限的,还是会有半包问题。

方法2 固定长度

客户端和服务端商量好发送的数据包具有固定长度。如果不够长,就用占位符占位。
让所有的数据包长度固定(假设长度为10字节),服务端加入FixedLengthFrameDecoder进行处理。

ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

客户端测试代码,注意, 采用这种方法后,客户端什么时候 flush 都可以

public class HelloWorldClient {static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);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 ch) throws Exception {log.debug("connetted...");ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.debug("sending...");// 发送内容随机的数据包Random r = new Random();char c = 'a';ByteBuf buffer = ctx.alloc().buffer();for (int i = 0; i < 10; i++) {byte[] bytes = new byte[8];for (int j = 0; j < r.nextInt(8); j++) {bytes[j] = (byte) c;}c++;buffer.writeBytes(bytes);}ctx.writeAndFlush(buffer);}});}});ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error", e);} finally {worker.shutdownGracefully();}}
}

缺点是数据包的大小不好把握。长度只能定的太大,占位符就会多。浪费空间。因此长度最好定位最大的数据包的长度。仅仅适用于提前知道数据包的长度的情况。

方法3 固定分隔符

客户端和服务端商量好发送的数据包具有特定的分隔符。服务端用LineBasedFrameDecoder处理,自动将接受到的消息用\n进行分隔。

ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

客户端在每条消息之后,加入\n分隔符。

public class HelloWorldClient {static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);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 ch) throws Exception {log.debug("connetted...");ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.debug("sending...");Random r = new Random();char c = 'a';ByteBuf buffer = ctx.alloc().buffer();for (int i = 0; i < 10; i++) {for (int j = 1; j <= r.nextInt(16)+1; j++) {buffer.writeByte((byte) c);}buffer.writeByte(10);c++;}ctx.writeAndFlush(buffer);}});}});ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();channelFuture.channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error", e);} finally {worker.shutdownGracefully();}}
}

如果本身传输的数据中有分隔符,会解析错误

方法4 预设长度(推荐)

在发送消息前,先约定用定长字节表示接下来数据的长度。服务端用LengthFieldBasedFrameDecoder来处理。

// 最大长度,长度偏移,长度占用字节,长度调整(长度占用字节后偏移多少个才是真正的数据),剥离字节数(用于接受的数据去除长度占用的字节信息)
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

测试代码:

package cn.itcast.advance.c1;import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;public class TestLengthFieldDecoder {public static void main(String[] args) {EmbeddedChannel channel = new EmbeddedChannel(new LengthFieldBasedFrameDecoder(1024, 0, 4, 1,4),new LoggingHandler(LogLevel.DEBUG));//  4 个字节的内容长度, 实际内容ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();send(buffer, "Hello, world");send(buffer, "Hi!");channel.writeInbound(buffer);}private static void send(ByteBuf buffer, String content) {byte[] bytes = content.getBytes(); // 实际内容int length = bytes.length; // 实际内容长度buffer.writeInt(length);buffer.writeByte(1);buffer.writeBytes(bytes);}
}

这篇关于第五章Netty第一节 粘包和半包的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

React第五章(swc)

swc 什么是swc? SWC 既可用于编译,也可用于打包。对于编译,它使用现代 JavaScript 功能获取 JavaScript / TypeScript 文件并输出所有主流浏览器支持的有效代码。 SWC在单线程上比 Babel 快 20 倍,在四核上快 70 倍。 简单点来说swc实现了和babel一样的功能,但是它比babel快。 FAQ为什么快? 编译型 Rust 是

【Netty】netty中都是用了哪些设计模式

对于工程师来说,掌握并理解运用设计模式,是非常重要的,但是除了学习基本的概念之外,需要结合优秀的中间件、框架源码学习其中的优秀软件设计,这样才能以不变应万变。 单例模式 单例模式解决的对象的唯一性,一般来说就是构造方法私有化、然后提供一个静态的方法获取实例。 在netty中,select用于处理CONTINUE、SELECT、BUSY_WAIT 三种策略,通过DefaultSelectStra

Java语言的Netty框架+云快充协议1.5+充电桩系统+新能源汽车充电桩系统源码

介绍 云快充协议+云快充1.5协议+云快充1.6+云快充协议开源代码+云快充底层协议+云快充桩直连+桩直连协议+充电桩协议+云快充源码 软件架构 1、提供云快充底层桩直连协议,版本为云快充1.5,对于没有对接过充电桩系统的开发者尤为合适; 2、包含:启动充电、结束充电、充电中实时数据获取、报文解析、Netty通讯框架、包解析工具、调试器模拟器软件等; 源码合作 提供完整云快充协议源代码

高级编程--第五章 多线程

1、目标 理解线程的概念 掌握线程的创建和启动 了解线程的状态 掌握线程调度的常用方法 掌握线程的同步 理解线程安全的类型 2、进程 3、多线程 3.1 什么是多线程 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 如果一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程 多线程交替占用CPU资源,而非

嵌入式开发高频面试题——第五章 Linux操作系统常见面试题(上)

目录 5.1.1 Linux内核的组成5.1.2 用户空间与内核通信方式有哪些?5.1.3 系统调用read()/write(),内核具体做了哪些事情5.1.4 系统调用的作用5.1.5 内核态,用户态的区别5.1.6 Bootloader、内核、根文件系统的关系5.1.7 Bootloader多数有两个阶段的启动过程5.1.8 Linux的内核是由Bootloader装载到内存中的?5.1

Netty源码解析9-ChannelHandler实例之MessageToByteEncoder

MessageToByteEncoder框架可见用户使用POJO对象编码为字节数据存储到ByteBuf。用户只需定义自己的编码方法encode()即可。 首先看类签名: public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter 可知该类只处理出站事件,切确的说是write事件

Netty源码解析8-ChannelHandler实例之CodecHandler

编解码处理器作为Netty编程时必备的ChannelHandler,每个应用都必不可少。Netty作为网络应用框架,在网络上的各个应用之间不断进行数据交互。而网络数据交换的基本单位是字节,所以需要将本应用的POJO对象编码为字节数据发送到其他应用,或者将收到的其他应用的字节数据解码为本应用可使用的POJO对象。这一部分,又和JAVA中的序列化和反序列化对应。幸运的是,有很多其他的开源工具(prot

Netty源码解析7-ChannelHandler实例之TimeoutHandler

请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData TimeoutHandler 在开发TCP服务时,一个常见的需求便是使用心跳保活客户端。而Netty自带的三个超时处理器IdleStateHandler,ReadTimeoutHandler和WriteTimeoutHandler可完美满足此需求。其中IdleSt

Netty源码解析6-ChannelHandler实例之LoggingHandler

LoggingHandler 日志处理器LoggingHandler是使用Netty进行开发时的好帮手,它可以对入站\出站事件进行日志记录,从而方便我们进行问题排查。首先看类签名: @Sharablepublic class LoggingHandler extends ChannelDuplexHandler 注解Sharable说明LoggingHandler没有状态相关变量,

Netty源码解析5-ChannelHandler

ChannelHandler并不处理事件,而由其子类代为处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。ChannelHandler和ChannelHandlerContext通过组合或继承的方式关联到一起成对使用。事件通过ChannelHandlerContext主动调用如fireXXX()和write(msg)