黏包半包

滑动窗口

在深入理解黏包半包问题之前,先了解TCP的一个知识点——滑动窗口

我们都指定tcp是一种可靠的传输协议,这主要是因为在tcp中客户端给服务器端发送一条消息,要等待服务器端的应答,如果很长时间都没有应答那就需要重发这条消息。

既然引入了应答机制,那势必会影响到吞吐量,需要等到第一条消息的应答回来后才能发送第二条消息,这样就会导致吞吐量比较低,解决方法就是引入了滑动窗口的概念。

在发送方可以设置一个窗口的大小,其实就是一个有大小的缓冲区,还能有一个流量监控的功能。在这个窗口大小范围内的请求不需要等到上一个请求的响应回来就可以发送,就比如上图请求窗口大小为4,第二个请求就可以直接发送,但第五个请求就必须要前面四个请求中的某一个请求的响应回来后才能发送。
其实接收方也有一个滑动窗口,其实就是一个有大小限制的缓冲区, 如果网络比较繁忙,接收方的窗口也就是缓冲区用完了,势必接收到的数据容易发生半包现象,它可能接收到一半窗口用完了,这个时候再去读这个缓冲区,拿到的数据就是半包的数据

黏包的现象和窗口也有关系,比如说接收方的滑动窗口还比较空闲,还有很多空间没有使用,这个时候客户端发送了几条数据过来,窗口将这几条数据都接收到了,这就产生了黏包现象。

产生原因

可能产生黏包的原因:

  • 接收方的ByteBuf设置太大,netty默认的大小是1024
  • 滑动窗口:假设发送方256 bytes表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256 bytes字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
  • Nagle算法:会造成黏包。在网络层和传输层,都要对数据加上一个报头,报头占20个字节,这样就算你发送1字节的数据,然后将网络层和传输层的报头加上也要传输41个字节,这样就会出现报头的长度远远大于内容的长度,这样就出现了Nagle算法,它会尽可能多的发送数据,可以攒够了一些数据在发送

接下来是可能参数半包的原因:

  • 接收方ByteBuf设置太小
  • 滑动窗口:假设接收方的窗口只剩下了128bytes,发送方的数据大小是256bytes,这时放不下,只能先发送前128bytes,等待ack应答后,窗口向下滑动有剩余空间了 才能发送剩余部分,这就造成了半包
  • 数据链路层 MSS限制:当发送的数据超过MSS限制后,会将数据切分发送,就会造成半包。

不同的网卡对于数据包的大小是有限制的,这个限制就是MSS限制

本质是因为TCP是流式协议,消息无边界

现象演示

在nio时就已经学习了如何解决黏包与半包问题,可以用一个分隔符。现在再看在netty中如何出现黏包半包问题。首先是创建服务器端的代码用来接收数据,

public class ServerSocketChannel {public static void main(String[] args) throws InterruptedException {ServerBootstrap serverBootstrap = new ServerBootstrap().group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {// 添加一个LoggingHandler 可以帮我们打印出服务器端收到的数据nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080);channelFuture.sync();Channel channel = channelFuture.channel();channel.closeFuture().sync();}
}

接下来是客户端代码,用来发送数据,这里创建了一个循环,共发送10次数据

package com.hs.nettyIntermediate.mode1;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;/*** 客户端代码* @author hs* @date 2021/07/24*/public class SocketChannel {private static final Logger log = LoggerFactory.getLogger(SocketChannel.class);public static void main(String[] args) throws InterruptedException {NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();try {new Bootstrap().group(eventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {// 添加一个自定义的入站handlernioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){// active 事件,当客户端与服务器端连接成功后调用的方法// 如果想要发送数据可以使用以前的方式先让客户端连接服务器,然后调用sync()方法阻塞 // 然后在获取channel 再通过channel发送数据,也可以在该方法中发送数据@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 创建一个ByteBuf 向服务器端发送数据for (int i = 0; i < 10; i++) {ByteBuf buffer = ctx.alloc().buffer(16);buffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,                                                                     13, 14, 15, 16});ctx.writeAndFlush(buffer);}super.channelActive(ctx);}});}}).connect("localhost",8080).sync().channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error " , e);}finally {eventLoopGroup.shutdownGracefully();}}
}

接下来运行服务器端,再运行客户端发送数据,服务器端接收数据的结果如下

原本我们的想法是一次获取16的字节,共获取10次,可是服务器一次性将全部数据都获取了。这就是黏包现象

接下来是演示半包现象,为了能比较明显的看见效果,先将系统的的接收缓存区变小(滑动窗口)。

serverBootstrap.option(ChannelOption.SO_RCVBUF , 10)

然后运行后的结果如下

其实即使不用netty 不用nio 主要是使用tcp协议进行网络通信都会有半包问题

解决方案:短连接

短连接的思想就是,客户端建立了连接,然后发送一条数据,接着就断开连接,因为我们知道客户端正常断开连接后read()方法返回的数据为-1,这也就相当于是一个消息的结束标识,

这种方式可以解决黏包问题,可以测试一下:服务器端代码

public static void main(String[] args) throws InterruptedException {ServerBootstrap serverBootstrap = new ServerBootstrap().group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class)// 去掉之前这里设置的系统接收缓冲区 也就是滑动窗口的设置代码 //.option(ChannelOption.SO_RCVBUF , 10).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {// 添加一个LoggingHandler 可以帮我们打印出服务器端收到的数据nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080);channelFuture.sync();Channel channel = channelFuture.channel();channel.closeFuture().sync();
}

客户端的代码,需要将之前,连接成功后通过for循环发送十次数据的代码改为发送一次数据,数据发送后还要断开连接 ,然后将所有代码抽取为一个方法,在主函数中调用十次这个方法

public class SocketChannel2 {private static final Logger log = LoggerFactory.getLogger(SocketChannel2.class);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {send();}}private static void send() {NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();try {new Bootstrap().group(eventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {// 添加一个自定义的入站handlernioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 创建一个ByteBuf 向服务器端发送数据ByteBuf buffer = ctx.alloc().buffer();buffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,                                                                 14, 15, 16});ctx.writeAndFlush(buffer);ctx.close(); // 关闭连接super.channelActive(ctx);}});}}).connect("localhost",8080).sync().channel().closeFuture().sync();} catch (InterruptedException e) {log.error("client error " , e);}finally {eventLoopGroup.shutdownGracefully();}}
}

执行结果为:

虽然短连接的方式可以解决黏包的问题,但是不能解决半包问题,比如我在服务器端的代码中,调整一下接收的ByteBuf容量。

public static void main(String[] args) throws InterruptedException {ServerBootstrap serverBootstrap = new ServerBootstrap().group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class)// 调整系统的接收缓冲区(滑动窗口)//.option(ChannelOption.SO_RCVBUF , 10)// 调整netty的接收缓冲区 (ByteBuf) netty默认的Bytebuf大小为1024个字节// 这里参数二的构造方法中的三个参数分别表示 最小容量 初始容量 最大容量.childOption(ChannelOption.RCVBUF_ALLOCATOR , new AdaptiveRecvByteBufAllocator(16,16,16)).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {// 添加一个LoggingHandler 可以帮我们打印出服务器端收到的数据nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));}});ChannelFuture channelFuture = serverBootstrap.bind(8080);channelFuture.sync();Channel channel = channelFuture.channel();channel.closeFuture().sync();
}

然后将客户端发送的消息再多加几个字节就会出现半包的问题。

短连接可以解决黏包问题,但是不能解决半包问题。而且它的效率也不高,因为频繁的建立连接和断开连接

解决方案:定长解码器

netty提供了一个类 FixedLengthFrameDecoder。该方案解决黏包半包问题的方式是 服务器与客户端约定一个发送消息的长度,该长度一般为客户端发送最长的消息的长度,服务器这边每次都读取该长度的字节

如果发生了半包的情况,也就是当服务器这边接收的消息小于约定的长度时,服务器会等待接下来接收的数据凑够约定的长度了才将约定长度的数据发送给handler进行处理。

客户端这边如果发送的消息小于了定长,也不能空着,需要用一些其他字符填满,反正就是客户端每次发送的消息都是定长,服务器每次处理的消息也都定长,如果不够服务器就会等待,凑够了定长的消息才进行处理。所以缺点也很明显,如果客户端要发送的消息太少,但是每次还是要用其他的一些字符填充至定长才发送

代码需要修改的部分是,首先客户端需要准备要发送的消息,每条消息的长度为定长,然后服务器端需要添加一个handler来进行解码.

nioSocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(int 约定的定长));这个handler需要放在我们自定义入站handler的最上方,因为首先接收到数据是需要它来解码,然后再传递给下面的handler。

解决方案:行解码器

该方案是使用分隔符来界定消息的边界。netty提供了两个跟分隔符有关的实现

  • LineBasedFrameDecoder 它是通过换行符进行消息分隔。需要注意一点,使用它时 它又一个最大长度的限制,我们在构造方法中需要传递一个最大长度,如果在读取消息时超过了这个最大长度还是没有读取到换行符的话就会报一个异常

  • DelimiterBasedFrameDecoder 它可以自己定义一个分隔符,即在构造方法中除了要指定一个最大长度,还需要指定一个ByteBuf类型的自定义分隔符

客户端的代码也不用怎么修改,只是准备好多条要发送的消息,然后添加好分隔符即可。服务器这边也是需要在最上面的入站handler添加一个解码器。

该方案虽然能解决黏包半包问题,但是效率比较低,需要一个字节一个字节去判断。

解决方案:LTV解码器

该解码器类为LengthFieldBasedFrameDecoder 该类的构造方法有五个参数需要了解

public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {this(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true);
}
  • maxFrameLength 它表示Frame(帧)的最大长度,一旦超过这个标准如果还没有发现分割标准就认为失败
  • lengthFieldOffset 长度字段的偏移量 也就是从第几个字节开始才表示存储数据长度位的字节
  • lengthFieldLength 长度字段的长度 表示存储长度字段共占几个字节
  • lengthAdjustment 长度字段之后,还有几个字节之后才是真正的数据
  • initialBytesToStrip 从头开始 剥离几个字节才开始解析数据

下面还是讲解例子

该消息长度是从0开始,长度共占两个字节,然后其他的都是消息,右边为解析的结果



有的时候我不想要长度字段,也就是解析的时候将声明长度的那几个字节剥离出去,不进行解析,就要用到最后一个参数了,剥离2字节,右边就是处理的结果



有的时候我们发送消息并不是单单发送消息的长度和消息内容,有时候还会附加一些内容,这里长度字段就不是从头开始了,就需要变



这种情况就是表示长度的字段和真正存放数据中间还添加了一个附加的内容,这时候就需要用到lengthAdjustment了,表示长度字段从0开始,有3个字节的长度,然后跳过两个字节才是数据的长度。



这种情况就是所有的参数都用到了,前面一个附加信息,第二段为长度,第三段为附加信息,第四段为数据,然后解析还是从3个字节开始解析。

具体使用还是客户端准备好要发送的数据,服务器在第一个入站handler添加解码器,并设置好构造方法中的五个参数。

netty——黏包半包的解决方案、滑动窗口的概念相关推荐

  1. Netty框架之TCP粘包/半包解决方案

    Netty框架之TCP粘包/半包解决方案 一.TCP粘包 二.TCP半包 三.TCP粘包/半包解决方案 1.FixedLengthFrameDecoder定长解析器 2.LineBasedFrameD ...

  2. 三、Netty的粘包半包问题解决

    一.定义 TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生.客户端给服务端发送了两条消息ABC和DEF,服务端这边的接收会有多少种情况呢?有可能是一次 ...

  3. TCP 粘包半包 netty 编解码 三者关系

    1 何为粘包 / 半包? 对方一次性接收了多条消息这种现象,我们就称之为 粘包现象. 对方多次接收了不完整消息这种现象,我们就称之为 半包现象. 粘包的原因: 发送方发送的消息 < 缓冲区大小 ...

  4. Netty粘包/半包问题解析

    目录 一.什么是粘包/半包问题 二.TCP粘包/半包发生的原因 三.粘包/半包解决办法 四.Netty中粘包/半包解决示例 1. 采用固定长度数据包编解码方式 2. 采用特殊字符作为边界字符编解码方式 ...

  5. Netty如何解决粘包半包问题

    何为粘包 / 半包? 比如,我们发送两条消息:ABC 和 DEF,那么对方收到的就一定是 ABC 和 DEF 吗? 不一定,对方可能一次就把两条消息接收完了,即 ABCDEF:也可能分成了好多次,比如 ...

  6. websocket是否需要处理粘包半包问题分析

    结论: ​ 不需要. 背景: ​ 公司通信涉及到websocket相关,我们都知道websocket是基于tcp的,而tcp是面向字节流的,是需要处理粘包半包问题的.那么websocket是否需要处理 ...

  7. 网络:什么是TCP粘包/半包?怎么解决这个问题

    在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接.在TCP的socket编程中,发送端和接收端都有成对的socke ...

  8. 27.Linux网络编程 掌握三次握手建立连接过程掌握四次握手关闭连接的过程掌握滑动窗口的概念掌握错误处理函数封装实现多进程并发服务器实现多线程并发服务器

    基本概念叫协议 什么叫协议? 协议是一个大家共同遵守的一个规则, 那么在这个网络通信当中,其实就是双方通信和解释数据的一个规则,这个概念 你也不用记,你只要心里明白就可以了, 分层模型, 物数网传会表 ...

  9. 04、Netty学习笔记—(黏包半包及协议设计解析)

    文章目录 一.粘包与半包 1.1.现象分析 1.1.1.粘包.半包情况分析 1.1.2.滑动窗口.MSS限制.Nagle算法介绍 1.2.粘包.半包现象复现 1.2.1.粘包复现 1.2.2.半包复现 ...

最新文章

  1. exec的不同实现--鸠占鹊巢还是功成身退
  2. windows核心编程 如何等待超过64(MAXIMUM_WAIT_OBJECTS) kernal object
  3. zookeeper在window下的搭建
  4. typescript使用in关键字进行类型守卫
  5. centos7 yum安装maven_Centos7.3安装Maven私服nexus-3.x
  6. 女生转行IT与男生有什么不一样?
  7. 调用第三方接口的几种请求方式
  8. 深度学习《stackGAN》
  9. 有一个字长32位的浮点数符号位1位_边缘计算专题:(二)别看只有0和1,数学不好的勿进!...
  10. 周期置换加密算法用c语言实现,古典密码实验报告.doc
  11. 成功是需要付出代价的: 32个成功观念分享
  12. 餐厅点餐系统app总结
  13. CF984C Finite or not?
  14. SARscape之DInSAR处理(双轨法)
  15. 项目管理软件怎么选?看看中国电信天翼云的选择
  16. 【Python】阿里云对象存储OSS图床上传图片
  17. 网络安全面试、实习、校招经验打包分享
  18. EXCEL篇—时间序列分析(季节指数法)
  19. 大学Python编程试卷真题!用python循环,输出1+11+111+1111+11111的值
  20. Ai形状模式与路径查找器

热门文章

  1. 游戏戏策划设计时所要涉及的基本因素
  2. 大数据GIS系列(1)——大数据时代下的GIS技术
  3. 计算机学院科技文化节,计算机学院首届学生科技文化节拉开帷幕
  4. 虚拟网卡、虚拟交换机、虚拟机网卡
  5. java 偏向锁_Java并发之彻底搞懂偏向锁升级为轻量级锁
  6. 下属不服管,管理者怎么办?
  7. 哈夫曼树的带权路径长度和
  8. Ngrok免费实现内网穿透
  9. Rego不好用?用Pipy实现OPA
  10. pipy 常见错误及其解决方法