Netty学习笔记

190 阅读20分钟

1.前置

1.1 IO概念:

(1)同步/异步,阻塞/非阻塞

阻塞/非阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。非阻塞遇到这些情况,都是直接返回。 同步/异步:数据就绪后需要自己去读是同步,数据就绪后系统直接读好再回调给程序是异步。

(2)BIO

BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,I/O操作都是基于流Stream的操作。

  • 它线程开销大,客户端的并发数和后端的线程数是1对1的,线程的创建销毁消耗系统资源大。
  • 且线程阻塞,当建立连接后,不进行操作,就会一直阻塞在这

(3)NIO

NIO的三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)

  • Buffer:

    • 在面向流的I/O中,数据读写是直接进入到Steam中,而在NIO中,所有数据都是用缓冲 区处理的,读数据直接从缓冲区读,写数据直接写入到缓冲区。
    • 缓冲区的本质是一个数组,通常是一个字节数组(ByteBuffer),也可以使用其他类型,但缓冲区又不仅仅是一个数组,它还提供了对数据结构化访问以及维护读写位置等操作。
  • Channel:

    • Channel和流Stream的不同之处在于Channel是双向的,流只在一个方向上移动,而Channel可以用于读写同时进行,即Channel是全双工的。
    • 网络读写,一般有ServerSocketChannel(用于Server,接收连接事件)和SocketChannel(Server和Client均可,都可用于读写事件,在Client端还有建立连接事件)
    • image-20231123193259775
  • Selector:选择器/多路复用器

    • Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,即该Channel处于就绪状态,它就会被Selector轮询出来,然后通过selectedKeys可以获取就绪Channel的集合,进行后续的I/O操作。

1.2 Reactor模型

Reactor线程模型不是Java专属,也不是Netty专属,它其实是一种并发编程模型,是一种思想,具有指导意义,Reactor模型中定义了三种角色:

  • Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
  • Acceptor:处理客户端新连接,并分派请求到处理器链中。
  • Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel

(1)单Reactor-单线程

所有的接收连接,处理数据的相关操作都在一个线程中来完成,性能上有瓶颈

image-20231123194429302

(2)单Reactor-多线程

把比较耗时的数据的编解码,业务运算操作放入线程池中来执行,提升了性能但还不是最好的方式

image-20231123194747886

image-20231123194603066

(3)单Reactor-多线程

主从多线程,对于服务器来说,接收客户端的连接是比较重要的,因此将这部分操作单独用线程去操作

image-20231123194831522

image-20231123194854550

MainReactor 线程与 SubReactor 线程交互简单,ainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据。多个 SubReactor 线程能够应对更高的并发请求。

这种模式的缺点是编程复杂度较高。这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,1 只是表示相对较少)连接建立线程+M 个 IO 线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式。

2.Netty概念

2.1 介绍

  1. Netty提供非阻塞的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

  2. 核心架构:

    • 核心:

      • 可扩展的事件模型
      • 统一的通信api,简化了通信编码
      • 零拷贝机制与丰富的字节缓冲区
    • 传输服务:

      • 支持socket以及datagram(数据报)

      • http传输服务

      • In-VM Pipe (管道协议,是jvm的一种进程)

    • 协议支持:

      • http 以及 websocket
      • SSL 安全套接字协议支持
      • Google Protobuf (序列化框架)
      • 支持zlib、gzip压缩
      • 支持大文件的传输
      • RTSP(实时流传输协议,是TCP/IP协议体系中的一个应用层协议)
      • 支持二进制协议并且提供了完整的单元测试

2.2 Netty中的Reactor实现

Netty线程模型是基于Reactor模型实现的,对Reactor三种模式都有非常好的支持,并做了一定的改进,也非常的灵活,一般情况,在服务端会采用主从架构模型。

image-20231123200825347

工作流程:

1)Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,每个线程池中都有EventLoop 线程(可 以是OIO,NIO,AIO)。BossGroup中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专 门负责处理连接上的读写, EventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环 2)EventLoop 表示一个不断循环的执行事件处理的线程,每个EventLoop 都包含一个 Selector,用 于监听注册在其上的 Socket 网络连接(Channel)。 3)每个 Boss EventLoop 中循环执行以下三个步骤: 3.1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件) 3.2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个SocketChannel,并 将其注册到某个 WorkerEventLoop 上的 Selector 上 3.3)runAllTasks:再去以此循环处理任务队列中的其他任务 4)每个 Worker EventLoop 中循环执行以下三个步骤: 4.1)select:轮训注册在其上的SocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件) 4.2)processSelectedKeys:在对应的SocketChannel 上处理 read/write 事件 4.3)runAllTasks:再去以此循环处理任务队列中的其他任务 5)在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用Channel, 即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤 处理器、自定义处理器等)。

2.3 ChannelPipeline&ChannelHandler

ChannelPipeline :

提供了 ChannelHandler 链的容器。以服务端程序为例,客户端发送过来的数据要接收,读取处理,我们称数据是入站的,需要经过一系列Handler处理后;如果服务器想向客户端写回数据,也需要经过一系列Handler处理,我们称数据是出站的。

image-20231123203950891

ChannelHandler分类:

Ø ChannelInboundHandler 入站事件处理器 Ø ChannelOutBoundHandler 出站事件处理器 Ø ChannelHandlerAdapter提供了一些方法的默认实现,可减少用户对于ChannelHandler的编写 Ø ChannelDuplexHandler:混合型,既能处理入站事件又能处理出站事件。

inbound入站事件处理顺序(方向)是由链表的头到链表尾,outbound事件的处理顺序是由链表尾到链表头。

3.Netty基础

image-20231123204209546

ServerBootstrap是服务端引导程序,new NioEventLoopGroup()里面填数字,则为对应的线程数,不填则是线程数为cup核数*2

3.1 入站事件传播

入站事件写法:

注意:如果不想往后传了,就不写传递方法,就会在当前inboundhandler停了,或想在此直接写出数据,那就直接写写出数据的方法往outboundhandler传

image-20231123204558501

inbound/outbound 加载顺序和执行顺序

ØInboundHandler是按照Pipleline的加载顺序(addLast),顺序执行 ØOutboundHandler是按照Pipeline的加载顺序(addLast),逆序执行

image-20231123204708136

image-20231123204712556

回写数据事件流转规则:

image-20231123204824964

  • ctx.writeAndFlush(buf):事件会从当前handler(即inbound2)直接流向头部(即inbound2经过outbound1到head,再写出)
  • ctx.channel().writeAndFlush(buf):事件会从pipeline尾部(即tail)流向头部(即tail经过outbound1到head,再写出)
  • 要是outbound1前面还有个outbound0,
    • ctx.channel().writeAndFlush(buf)的顺序就是tail—》outbound0—》outbound1—》head
    • ctx.writeAndFlush(buf)的顺序还是是inbound2—》outbound1—》head

3.2 出站事件传播

2种出站事件写法:

image-20231123205935443

image-20231123205944226

注意:outboundHandler中的数据修改

  • 在outboundhandler中最好不要再通过Channel写数据,会导致事件再次从尾部流动到头部,造成类似递归问题可以在事件向前传播出去之后,ctx.channel().writeAndFlush()
  • 可以通过ChannelHandlerContext写数据,ctx.writeAndFlush()

4.Netty 核心组件剖析

4.1 Bootstrap

Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动Netty服务

Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器,区别在于: 1、ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接 到远程节点的客户端应用程序所使用的 2、引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个

image-20231123211833502

ServerBootstrap的childHandler()意指给woker EventLoopGroup的每个EventLoop中的socketChannel的pipeline加handler

4.2 Channel

Netty中的Channel是与网络套接字相关的,可以理解为是socket连接,在客户端与服务端连接的时候就会建立一个Channel,它负责基本的IO操作,比如:bind()、connect(),read(),write() 等

主要作用:

  1. 通过Channel可获得当前网络连接的通道状态。
  2. 通过Channel可获得网络连接的配置参数(缓冲区大小等)。
  3. Channel提供异步的网络I/O操作,比如连接的建立、数据的读写、端口的绑定等。

Ø 不同协议、不同的I/O类型的连接都有不同的 Channel 类型与之对应

4.3 EventLoopGroup&EventLoop

  • Netty是基于事件驱动的,比如:连接注册,连接激活;数据读取;异常事件等等,有了事件,就需要一个组件去监控事件的产生和事件的协调处理,这个组件就是EventLoop(事件循环/EventExecutor),在Netty 中每个Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。每个EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。
  • EventLoopGroup (可以初步理解成Netty线程池)是用来生成 EventLoop 的,包含了一组EventLoop

image-20231123213456310

4.4 ChannelHandler

ChannelHandler复用:

每个客户端Channel创建后初始化时,均会向与该Channel绑定的Pipeline中添加handler,此种模式下,每个Channel享有的是各自独立的Handler,要想复用handler,可以在handler类上加@Sharable注解,但要注意注解只是标注可以被复用,至于线程安全问题需要开发者自行保证。

SimpleChannelInboundHandler:

image-20231123213928059

上文说到ChannelHandlerAdapter集成了一些实现,简化了开发而SimpleChannelInboundHandler继承ChannelInboundHandlerAdapter,它既是一个入站handler也简化了开发,怪不得名字是Simplexxx。

  • 它主要就简化在以下:

一般的入站处理器,处理读事件就是重写channelRead(ChannelHandlerContext ctx,Object msg)方法,而这个类直接重写channelRead0()方法即可,

此类则自带泛型使得msg的类型不是Objcet,而是可以直接转换成我们指定的类型,且这里可以直接释放掉原始的msg占用的Bytebuffer资源

  • 不过也因此要注意:

服务端异步处理数据,服务端想把客户端发送来的数据再写回等场景下最好不要继承SimpleChannelInboundHandler,因为它会释放掉原始的数据资源

客户端推荐使用SimpleChannelInboundHandler

4.5 ByteBuf

Netty使用ByteBuf来替代ByteBuffer 作为它的字节容器,它是一个强大的实现,既解决了JDK API 的局限性, 又为网络应用程序的开发者提供了更好的API。

(1)ByteBuf的三个指针

image-20231124210546950

  • readerIndex:指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与

    writerIndex 相等,ByteBuf 不可读。

  • writerIndex:指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity() 容量相等,表示 ByteBuf 已经不可写,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity

  • maxCapacity:指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足, 可以进行扩容的最大容量

image-20231124210726835

(2)常用API

image-20231124210812583

image-20231124210824524

image-20231124210846465

image-20231124210857261

image-20231124210947854

(3)三类ByteBuf

  • 堆缓冲区(HeapByteBuf):内存分配在jvm堆,分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。

  • 直接缓冲区(DirectByteBuf)【Netty默认】:内存分配的是堆外内存(系统内存),相比堆内存,它的分配和回收速度会慢一些,但是将它写入或从Socket Channel中读取时,由于减少了一次内存拷贝,速度比堆内存块。

  • 复合缓冲区(CompositeByteBuf):顾名思义就是将两个不同的缓冲区从逻辑上合并,让使用更加方便。

如果需要使用HeapByteBuf模式,则需要进行系统参数的设置


//设置HeapByteBuf模式,但ByteBuf 的分配器ByteBufAllocator要设置为非池化,否则不能切换到堆缓冲器模式 System.setProperty("io.netty.noUnsafe", "true")

关于堆外内存的理解


image-20231124211824820

(4)ByteBuf 的分配器

Netty 提供了两种 ByteBufAllocator 的实现,分别是:

  • PooledByteBufAllocator【Netty默认】:实现了 ByteBuf 的对象的池化,提高性能减少并最大限度地减少内存碎片,池化思想通过预先申请一块专用内存地址作为内存池进行管理,从而不需要每次都进行分配和释放
  • UnpooledByteBufAllocator:没有实现对象的池化,每次会生成新的对象实例

使用非池化模式:

可以通过引导类设置非池化模式,源码:DefaultChannelConfig种的allocator属性:

//引导类中设置非池化模式
bootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
//或者通过系统参数设置
System.setProperty("io.netty.allocator.type", "pooled");
System.setProperty("io.netty.allocator.type", "unpooled");
  • pooled & unpooled
  1. 对于Pooled类型的ByteBuf,不管是PooledDirectByteBuf还是PooledHeapByteBuf都只能由Netty内部自己使用(构造是私有和受保护的),开发者可以使用Unpooled类型的ByteBuf。
  2. Netty提供Unpooled工具类创建的ByteBuf都是unpooled类型,默认采用的Allocator是direct类型;当然用户可以自己选择创建UnpooledDirectByteBuf和UnpooledHeapByteBuf

(5)ByteBuf 的释放

  • 释放原理


    • 堆缓冲区模式:由JVM的gc回收
    • 直接缓冲区:Netty自身引入了引用计数,得我们手动释放,它提供了ReferenceCounted接口,当对象的引用计数>0时要保证对象不被释放,当为0时需要被释放
  • 如何释放


    • 手动释放:就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放,这种方式的弊端就是一旦忘记释放就可能会造成内存泄露

    • 自动释放:三种方式

      • TailContext:Inbound流水线的末端,如果前面的handler都把消息向后传递最终由TailContext释放该消息,需要注意的是,如果没有进行向下传递,是不会进行释放操作的

      • SimpleChannelInboundHandler:自定义的InboundHandler继承自SimpleChannelInboundHandler,在SimpleChannelInboundHandler中自动释放

      • HeadContext:outbound流水线的末端,出站消息一般是由应用所申请,到达最后一站时,经过一轮复杂的调用,在flush完成后终将被release掉

    • 总结:

      • 出战:不管,因为信息都会走到head
      • 入站:若消息能传播到tail也不用管,若传播不了,则要手动释放掉,或当原消息被转换成新消息并传下去了,原消息也要释放掉

4.6 Future & Promise

Netty是一个异步编程框架,Future/Promise就是其异步编程模型。

(1)Future

Java原生的Future就是线程提交任务获得一个future对象(返回值),Netty的Future则是多加个监听器

image-20231125112006546

  • ChannelFuture:

跟Channel的操作有关,Netty中的Handler处理都是异步IO,通过ChannelFuture添加事件监听,可获取Channel异步IO操作的结果;当然也可等待获取,但最好不要在handler中通过future的sync或await来获取异步操作的结果。

image-20231125163147501

(2)Promise

在 Java 的 Future 中,业务逻辑为一个Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。

image-20231125163447268

image-20231125163456941

  • ChannelPromise

    ChannelPromise接口,则继承扩展了Promise和ChannelFuture。所以,ChannelPromise既绑定了Channel,又具备了设置监听回调的功能,还可以设置IO操作的结果,是Netty实际编程使用的最多的接口。

5.Netty编解码

5.1 一次编解码

(1)作用:

解决tcp粘包、拆包问题

(2)粘包、拆包

粘包:一段数据中包含多个消息数据


  • 发送方每次写入数据<套接字缓冲区大小

    • 例:若要传输“ABC”,“DEF”两个消息数据,但套接字缓冲区大小能同时容得下两个消息数据,于是它把两个消息数据拼接在一起发送出去了,这就形成粘包。
  • 接收方读取套接字缓冲区数据不够及时,导致这一个完整消息还没读完,下一个又发过来了,形成粘包

拆包(半包):一段数据中只有一个不完整的消息数据


  • 发送方写入数据> 套接字缓冲区大小

    • 例:若要传输“ABC”这个消息数据,但套接字缓冲区大小只能装“AB”的数据,这就形成拆包。
  • 发送的最大报文长度大于MSS, 数据包大于协议的MTU(最大传输单元,1500字节),必须拆包

<1>根本原因

TCP 协议是面向连接的、可靠的、基于字节流的传输层通信协议,是一种流式协议,消息无边界。

<2>解决:

解决TCP粘包,半包问题的根本:找出消息的边界

image-20231125170516478

(3)Netty 消息编解码器

Netty提供了针对封装成帧这种形式下不同方式的拆包器,所谓的拆包其实就是数据的解码,所谓解码就是将网络中的一些原始数据解码成上层应用的数据,那对应在发送数据的时候要按照同样的方式进行数据的编码操作然后发送到网络中

image-20231125170610546

在发送方加入编码器,在接收方加入解码器

<1>基于长度的域解码器

-LengthFieldBasedFrameDecoder

image-20231125170827445

lengthFieldOffset和lengthAdjustment这两个参数可以用于数据加密的一种方式,设置偏移量迷惑对方

<2>基于长度的域编码器

-LengthFieldPrepender

image-20231125171051259

<3>其他编解码器

Netty中提供了ByteToMessageDecoder的抽象实现,自定义解码器只需要继承该类,实现decode()即可。Netty也提供了一些常用的解码器实现,用于数据入站的解码操作,基本都是开箱即用的;当然数据出站也需要采用对应的编码器

image-20231125171124528

5.2 二次编解码-序列化/反序列化

(1)作用:

将收发的原始数据转换为我们所需要的业务数据(即Java对象),便于业务处理,或者将将一种格式转化为另一种格式(譬如将应用数据转化成某种协议数据)。

(2)常用的二次编解码器:

image-20231125171937456

<1>Protostuff编解码

protostuff是一个基于protobuf(Protobuf序列化 :性能高,体积小,但是可读性差)实现的序列化方法,它较于protobuf最明显的好处是,在几乎不损耗性能的情况下做到了不用我们写.proto文件来实现序列化

<2>HTTP 编解码

基于Netty开发的HTTP服务器有如下优势:

  • Netty的线程模型和异步非阻塞特性能够支持高并发
  • 相比于Tomcat HTTP,Netty HTTP更加轻量、小巧、可靠,占用资源更少

image-20231125173529669

HTTP 协议抽象-Content-Length,chunked

  • HTTP协议通常使用Content-Length来标识body的长度,在服务器端,需要先申请对应长度的buffer,然后再赋值。如果需要一边生产数据一边发送数据,就需要使用"Transfer-Encoding: chunked" 来代替Content-Length,也就是对数据进行分块传输。
  • Content-Length:http server接收数据时,发现header中有Content-Length属性,则读取Content-Length 的值,确定需要读取body的长度;http server发送数据时,根据需要发送byte的长度,在header中增加 Content-Length 项,其中value为byte的长度,然后将byte数据当做body发送到客户端
  • chunked:http server接收数据时,发现header中有Transfer-Encoding: chunked,则会按照truncked协议分批读取数据;httpserver发送数据时,如果需要分批发送到客户端,则需要在header中加上 Transfer-Encoding: chunked,然后按照truncked协议分批发送数据

HTTP 协议抽象-响应压缩

image-20231125173829815

Netty HTTP 协议抽象的实现-请求和响应

image-20231125173857165

HttpRequestDecoder & HttpObjectAggregator & HttpResponseEncoder

image-20231125173916993

6.心跳监测

(1)Keepalvie

image-20231125174312941

TCP Keepalvie

image-20231125174507160

应用层 Keepalvie

image-20231125174516749

(2)idle监测

Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定Idle 监测的最终用途,一般用来配合keepalive ,减少keepalive 消息

image-20231125174602063

image-20231125174652402

7.优化

系统参数

image-20231125174842203

image-20231125174856781

添加线程名称

image-20231125174925094

添加Handler名称

image-20231125174946470

线程模型

image-20231125175048021

8.零拷贝-Zero Copy

Netty中的零拷贝:

Netty 中的 Zero-copy 与上面我们所提到到 OS 层面上的 Zero-copy 不太一样, Netty的 Zero-coyp 完全是在用户态(Java 层面)的,它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念,Netty的Zero-copy主要体现在如下几个方面: 1、Direct Buffer: 直接堆外内存区域分配空间而不是在堆内存中分配, 如果使用传统的堆内存分配,当我们需要将数据通过socket发送的时候,需要将数据从堆内存拷贝到堆外直接内存,然后再由直接内存拷贝到网卡接口层,通过Netty提供的DirectBuffers直接将数据分配到堆外内存,避免多余的数据拷贝 2、 Composite Buffers:传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝;同时也支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝。 3、通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作 4、通过 FileRegion 包装的FileChannel.tranferTo (Java nio)实现文件传输, 可以直接将文件缓冲区的数据发送到目标Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题