首页 > 其他分享 >Netty 黏包半包分析

Netty 黏包半包分析

时间:2023-10-04 17:02:08浏览次数:25  
标签:Netty 窗口 黏包 MSS 发送 解码器 长度 msg 半包

Netty 黏包半包分析

1 具体现象

粘包

  • 现象,发送 abc def,接收 abcdef
  • 原因
    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle 算法:会造成粘包

半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

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

那么究竟什么是滑动窗口呢?

滑动窗口

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

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

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

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

MSS 限制

  • 链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如

  • 以太网的 MTU 是 1500

  • FDDI(光纤分布式数据接口)的 MTU 是 4352

  • 本地回环地址的 MTU 是 65535 - 本地测试不走网卡

  • MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数

  • ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460

  • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送

  • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

Nagle 算法

  • 即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
    • 如果 TCP_NODELAY = true,则需要发送
    • 已发送的数据都收到 ack 时,则需要发送
    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送
    • 除上述情况,延迟发送

2 几种解决办法

1)短连接

  1. 短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
  2. 每一条消息采用固定长度,缺点浪费空间
  3. 每一条消息采用分隔符,例如 \n,缺点需要转义
  4. 每一条消息分为 head 和 body,head 中包含 body 的长度

注意:滑动窗口与缓冲区的设置

// 系统服务器的接受缓冲区 ---> 滑动窗口
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
// netty 的接受缓冲区 ---> ByteByf
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16))

半包用这种办法还是不好解决,因为接收方的缓冲区大小是有限的

2)定长解码器

简而言之,就是发送方的消息要使用统一长度,这样即使在发送方产生黏包现象,也可以在接受方通过定长解码器进行解析。

那么,究竟是神魔是定长解码器呢?

在使用上比较简单,其实就是直接加一个 handler 就可以,但是要注意入站顺序。

源码

非常统一的继承了 ByteToMessageDecoder,这应该是所有解码器都会继承的一个类。但是值得一提的是该类再次继承了ChannelInboundHandlerAdapter,想必这个类大家非常的熟悉吧。

当客户端通过Channel发送数据的时候,服务端会在ByteToMessageDecoder的channelRead方法接收到。我们以channelRead方法为入口看服务端是怎么进行消息解码的。

在channelRead方法中首先会判断消息msg是不是ByteBuf类型的:

①、如果msg不是ByteBuf类型的,直接调用ctx.fireChannelRead(msg)方法,fireChannelRead方法实际上是调用pipeline管道的下一个Handler去继续处理消息其他的逻辑去了,我们进入到AbstractChannelHandlerContext的fireChannelRead方法,发现只有一个方法invokeChannelRead,寻找下一个绑定的Handler并且调用ChannelRead方法。

    public ChannelHandlerContext fireChannelRead(Object msg) {
        invokeChannelRead(this.findContextInbound(32), msg);
        return this;
    }

接着进去看 invokeChannelRead 方法

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }

    }

最终都会调用invokeChannelRead,而invokeChannelRead就是将Handler转换成ChannelInboundHandler类型去执行具体的channelRead方法,去进一步的读取消息并处理相关Handler逻辑。

    private void invokeChannelRead(Object msg) {
        if (this.invokeHandler()) {
            try {
                ((ChannelInboundHandler)this.handler()).channelRead(this, msg);
            } catch (Throwable var3) {
                this.invokeExceptionCaught(var3);
            }
        } else {
            this.fireChannelRead(msg);
        }

    }

②、如果msg是ByteBuf类型的,去调用解码逻辑callDecode方法去解析数据。

啊啊啊啊啊,这里看不懂了,有朝一日再来些吧。

3)行解码器

大白话:按分隔符解码

这样的解码器有俩种:

  • LineBasedFrameDecoder ---> 回车或者换行(按平台区分)
  • DelimiterBasedFrameDecoder ----> 自定义分隔符

缺点,处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误

4)LTC解码器

基于长度字段的解码器 -------> LengthFieldBasedFrameDecoder

上面用到的自定义长度解码器LengthFieldBasedFrameDecoder构造器,涉及5个参数,都与长度域(数据包中的长度字段)相关,具体介绍如下:

(1) maxFrameLength - 发送的数据包最大长度;

(2) lengthFieldOffset - 长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;

(3) lengthFieldLength - 长度域的自己的字节数长度。

(4) lengthAdjustment – 长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。

(5) initialBytesToStrip – 丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。

第一、第二、第三个参数比较简单,不再啰嗦。

第四个参数长度域的矫正值为 -4,为什么呢? 它计算的方法是:包长(X+2)- 长度域的值(X) – 长度域偏移(2) – 长度域长(4)= -4 。

这里假定长度域的值为X,那么包长为X+2。因为在这个例子中,长度域的值,已经包括了长度域的长度值。长度域值与整个包长度相比,就少了前面的Header的2个字节。按照公式进行计算,最终的值为 2-2-4 = -4 。

第五个参数丢弃的起始字节数为6,为什么呢? 因为,最终的有效的应用层数据,需要去掉前面的6个字节。其中,包括2个字节的Header,4个字节的长度域长。

标签:Netty,窗口,黏包,MSS,发送,解码器,长度,msg,半包
From: https://www.cnblogs.com/shenzizai/p/17742457.html

相关文章

  • Netty -- ChannelOption
    1、ChannelOption.SO_BACKLOGChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(intsocketfd,intbacklog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处......
  • 为什么用netty
    1.BIO什么样?在JDK1.4以前java的IO都是BIO(BlockingIO),即阻塞型IO。BIO模型解读:客户端的请求和后端线程数1:1,导致在高并发下,大量创建和销毁线程,开销非常大。甚至可能会发生OOM。创建连接后,会创建一个线程,当改线程没有任何操作时候,该线程会一直阻塞,浪费资源。 2.NIO什么样......
  • netty发送socket短连接请求,自定义报文头
    packagecom.chinaums.japi.util;importio.netty.bootstrap.Bootstrap;importio.netty.buffer.ByteBuf;importio.netty.buffer.Unpooled;importio.netty.channel.*;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;......
  • 33socket套接字/黏包问题
    socket套接字#需求:编写一个cs架构的程序实现数据交互思考:需要编写代码操作OSI七层相当的复杂由于操作OSI七层是所有cs架构的程序都需要经历的过程所以有固定的模块socket套接字是一门技术socket模块>>>:提供了快捷方式不需要自己处理每一层"""以后我们写......
  • netty系列之ChannelOption
    netty系列之ChannelOption 1、概述在netty启动的时候会设置相关的ChannelOption,无论是在ServerBootstrap还是在Bootstrap,接下来解释一下常用的ChannelOption2、常用ChannelOptionChannelOption.SO_BACKLOG(一般用于option–>boss)BACKLOG用于构造服务端套接字ServerSocket......
  • netty WebSocket客户端实践
    在之前的Socket学习中,主要都是基于两个Socket客户端:WebSocket和Socket.IO。在做测试的时候也是基于WebSocket消息的发送和接收为主要测试对象。但是对于超多Socket连接没有涉及。在实践中会发现,这两个实现类都存在一个问题,为了维护1个Socket连接及其功能,通常需要创建多个线程。在......
  • Netty 的 ChannelOption.SO_BACKLOG 知识点整理
    Netty的ChannelOption.SO_BACKLOG知识点整理 一个基于Netty的应用,在压力测试时,Socket请求数量一多,就发送失败,监测JVM内存大小比较稳定,猜测可能是ChannelOption.SO_BACKLOG这个配置导致的,设置的值是128。调整为1024后,连接失败的次数确实减少了一些,那么这个配置到......
  • Netty源码学习3——Channel ,ChannelHandler,ChannelPipeline
    系列文章目录和关于我零丶引入在Netty源码学习2——NioEventLoop的执行中,我们学习了NioEventLoop是如何进行事件循环以及如何修复NIO空轮询的bug的,但是没有深入了解IO事件在netty中是如何被处理的,下面我们以服务端demo代码为例子,看下和IO事件处理密切的Channel如上在编写nett......
  • 【Netty】关于netty的入门问题
    1、netty是什么2、关于netty中的pipeline.addLast(xxxxHandler)这个xxxHandler是ChannelHandlerAdapter的实现类,ChannelHandlerAdapter有好些方法,也很常见,一直有一些问题,这里加了这么多handler,它在执行的时候,是每一个都会经过的吗?发送的时候会经过吗?响应的时候会经过......
  • netty实现同一个端口接收并解析多种解析
    1、背景项目需求,一个端口既能接收tcp协议数据又能接收http协议数据并解析,如果简单使用javasocket也能做到,但是当客户端使用post请求发送的是二进制文件时,socket将无法解析,因为无法判断二进制文件的开始和结束。由于netty有现成的解析http协议的工具包,所以使用netty可极大方便实......