Netty

目录

1. Netty主要组件

1.1 Channel

1.2 EventLoop

1.3 ChannelHandler

1.3.1 ChannelInboundHandler

1.3.2 ChannelOutboundHandler

1.4 ChannelPipeline

1.5 ByteBuf

(1) 优势

(2) 内存原理

​(3) 主要操作

(4) ByteBuf的类型

2. Netty Server

2.1 Netty Server启动主要流程

2.2 Netty Server执行主要流程

3. 线程模型

3.1 串行化处理模型

3.2 并行化处理模型

3.3 Reactor线程模型


1. Netty主要组件

1.1 Channel

Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channe l的 EventLoop; (对应NIO中的channel)

在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势:

  • 在Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
  • Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
  • 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,有 Channel 统一负责和调度,功能实现更加灵活。

在Netty中,每种Channel都有对应的配置,用ChannelConfig来表示,ChannelConfig是一个接口,每个特定的Channel实现类都有自己对应的ChannelConfig实现类,如:

NioSocketChannel的对应的配置类为NioSocketChannelConfig
NioServerSocketChannel的对应的配置类为NioServerSocketChannelConfig

1.2 EventLoop

Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象(对应于NIO中的while循环)

Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。

  • 一个 EventLoopGroup 包含一个或多个 EventLoop
  • 一个 EventLoop 在它的生命周期内只能与一个线程绑定
  • 所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理
  • 一个 Channel 在它的生命周期内只能注册于一个 EventLoop
  • 一个 EventLoop 可被分配至一个或多个 Channel 

当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。

ServerBootstrap: 包括2个不同类型的EventLoopGroup
        1. Parent EventLoop: 负责处理Accept事件,接收请求
        2. Child EventLoop:负责处理读写事件

1.3 ChannelHandler

ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

ChannelHandler  - 业务处理核心逻辑,用户自定义
Netty 提供2个重要的 ChannelHandler 子接口:

   ChannelInboundHandler: 处理进站数据和所有状态更改事件
   ChannelOutboundHandler:  处理出站数据,允许拦截各种操作

ChannelHandlerAdapter、ChannelInboundHandlerAdapter 、ChannelOutboundHandlerAdapter是Netty提供的适配器,对于输入输出事件,只需要继承适配器,重写感兴趣的方法即可。

在处理channel的IO事件时,我们通常会分成几个阶段。以读取数据为例,通常我们的处理顺序是:

处理半包或者粘包问题 --> 数据的解码(或者说是反序列化) --> 数据的业务处理

Channel的状态及其转换如下图所示:

ChannelHandler 添加到 ChannelPipeline,或者从ChannelPipeline 移除后,对应的方法将会被调用

1.3.1 ChannelInboundHandler

ChannelInboundHandler 回调方法在下表中:
    当接收到数据或者与之关联的 Channel 状态改变时调用
    与 Channel 的生命周期接近

1.3.2 ChannelOutboundHandler

1.4 ChannelPipeline

ChannelPipeline 是ChannelHandler容器

  • 包括一系列的ChannelHandler 实例,用于拦截流经一个 Channel 的入站和出站事件
  • 每个Channel都有一个其ChannelPipeline
  • 可以修改 ChannelPipeline 通过动态添加和删除 ChannelHandler
  • 定义了丰富的API调用来回应入站和出站事件

ChannelHandlerContext表示 ChannelHandler 和ChannelPipeline 之间的关联
在 ChannelHandler 添加到 ChannelPipeline 时创建 

  1. 每一个Channel被创建,就会生成对应的一个ChannelPipeline和它绑定。
  2. ChannelPipeline中包含了一个处理该Channel消息的ChannelHandler链。
  3. 当每一个ChannelHandler被注册到该ChannelPipeline中就会生成一个对应的 ChannelHandlerContext,将ChannelHandler维护到ChannelHandlerContext中, 由ChannelHandlerContext来维护链表关系
  4. 一个ChannelHandler可以从属于(注册到)多个ChannelPipeline。所以,一个ChannelHandler可以绑定多个ChannelHandlerContext。不过,这样的ChannelHandler必须使用@Sharable注解标注,保证它的线程安全性,否则试图将它注册到多个ChannelHandlerPipeline中时将会抛出异常。

ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。(对应于NIO中的客户逻辑实现handleRead/handleWrite)

当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个 ChannelOutboundHandler 。

当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext,它代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下:
    1. 一个 ChannelInitializer 的实现被注册到了 ServerBootStrap中
    2. 当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler
    3. ChannelInitializer 将它自己从 ChannelPipeline 中移除

注意:
       1. 默认情况下,一个ChannelPipeline实例中,同一个类型ChannelHandler只能被添加一次,如果添加多次,则会抛出异常,具体参见io.netty.channel.DefaultChannelPipeline#checkMultiplicity。如果需要多次添加同一个类型的ChannelHandler的话,则需要在该ChannelHandler实现类上添加@Sharable注解

      2. 在ChannelPipeline中,每一个ChannelHandler都是有一个名字的,而且名字必须的是唯一的,如果名字重复了,则会抛出异常,参见io.netty.channel.DefaultChannelPipeline#checkDuplicateName

      3. 如果添加ChannelHanler的时候没有显示的指定名字,则会按照规则其起一个默认的名字。具体规则如下,如果ChannelPipeline中只有某种类型的handler实例只有一个,如XXXHandler,YYYHandler,则其名字分别为XXXHandler#0,YYYHandler#0,如果同一类型的Handler有多个实例,则每次之后的编号加1。具体可参见io.netty.channel.DefaultChannelPipeline#generateName方法

示例:

Request过程: InboundHandlerA  ==>  InboundHandlerB  ==>  InboundOutboundHandlerX

Response过程: InboundOutboundHandlerX  ==> OutboundHandlerB  ==> OutboundHandlerA

1.5 ByteBuf

(1) 优势

ByteBuf对应于NIO 中的ByteBuffer, 但是相比JDK ByteBuffer

更加易于使用:
      1. 为读/写分别维护单独的指针,不需要通过flip()进行读/写模式切换
      2. 容量自动伸缩(类似于 ArrayList,StringBuilder)
      3. Fluent API (链式调用)
更好的性能:
     1. 通过内置的CompositeBuffer来减少数据拷贝(Zero copy)
     2. 支持内存池,减少GC压力

(2) 内存原理

ByteBuf通过两个索引(reader index、writer index)划分为三个区域:

  •  reader index前面的数据是已经读过的数据,这些数据可以丢弃
  • 从reader index开始,到writer index之前的数据是可读数据
  • 从writer index开始,为可写区域


(3) 主要操作

顺序读/写: (改变reader/writer index)
    writeByte()
    writeLong()
    writeXXX() - 增加write index
    readByte()
    readLong()
    readXXX() - 增加read index
随机读/写: (不改变read/write index)
   getXXX(index)
   setXXX(index, byte) 
mark/reset:
   markReaderIndex()
   markWriterIndex()
   resetReaderIndex()
   resetWriterIndex()
   writerIndex(index)
   readerIndex(index)

discardReadBytes()方法:

discardReadBytes() 可以用来清空 ByteBuf 中已读取的数据,从而使 ByteBuf 有多余的空间容纳新的数据,但是discardReadBytes() 可能会涉及内存复制,因为它需要移动 ByteBuf 中可读的字节到开始位置,这样的操作会影响性能,一般在需要马上释放内存的时候使用收益会比较大

clear()方法:

clear()方法清空操作,使得readerindex和writerindex都归零,但是ByteBuf中的数据并没有清空

Derived Buffers(衍生缓冲区):
   duplicate()
   slice()
   slice(start, stop)
   unmodifiableBuffer(...)

具有各自的index和mark; 返回的 ByteBuf 与原ByteBuf 共享底层存储

缓冲区拷贝:
  copy() 或 copy(int, int)
  返回的 ByteBuf 有数据的独立副本

(4) ByteBuf的类型

a. 根据内存的位置
   HeapByteBuf 
      基于数组- 内部为一个字节数组 (byte array)
      hasArray()返回True
      array()返回其内部的数组,可以对数组进行直接操作
   DirectByteBuf 
      堆外内存
      具有更好的性能
      创建和释放开销更大
b. 根据是否使用内存池
   Pooled 
      PooledByteBuf为对象池缓冲区,当对象释放后会归还给对象池,所以可循环使用。当需要大量且频繁创建缓冲区时,建议使用该类缓冲区。Netty4.1默认使用对象池缓冲区,4.0默认使用非对象池缓冲区
   Unpooled
       UnpooledByteBuf为不使用对象池的缓冲区,不需要创建大量缓冲区对象时建议使用该类缓冲区

c. 根据是否使用Unsafe操作(Unsafe)
  Safe vs Unsafe

d. 复合缓冲区(CompositeByteBuf)

  • 多个ByteBuf组合的视图
  • 一个ByteBuf列表,可动态的添加和删除其中的 ByteBuf
  • 可能既包含堆缓冲区,也包含直接缓冲区

 


2. Netty Server

2.1 Netty Server启动主要流程

1. 设置服务端ServerBootStrap启动参数

     1.1 group(parentGroup, childGroup):
     1.2 channel(NioServerSocketChannel): 设置通道类型
     1.3 handler():设置NioServerSocketChannel的ChannelHandlerPipeline
     1.4 childHandler(): 设置NioSocketChannel的ChannelHandlerPipeline

2. 通过ServerBootStrap的bind方法启动服务端,bind方法会在parentGroup中注册NioServerScoketChannel,监听客户端的连接请求     会创建一个NioServerSocketChannel实例,并将其在parentGroup中进行注册

2.2 Netty Server执行主要流程

1. 客户端发起连接CONNECT请求,parentGroup中的NioEventLoop不断轮循是否有新的客户端请求,如果有,ACCEPT事件触发
2. ACCEPT事件触发后,parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到childGroup中
3. childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理

3. 线程模型

并发系统可以采用多种并发编程模型来实现。并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同; 对于网络请求一般可以分为两个处理阶段,一是接收请求任务,二是处理网络请求

3.1 串行化处理模型

这个模型中用一个线程来处理网络请求连接和任务处理,当worker接受到一个任务之后,就立刻进行处理,也就是说任务接受和任务处理是在同一个worker线程中进行的,没有进行区分。这样做存在一个很大的问题是,必须要等待某个task处理完成之后,才能接受处理下一个task。

而通常情况下,任务的处理过程会比任务的接受流程慢得多。例如在处理任务的时候,我们可能会需要访问远程数据库,这属于一种网络IO。通常情况下IO操作是比较耗时的,这直接影响了下一个任务的接受,而且通常在IO操作的时候,CPU是比较空闲的,白白浪费了资源。

因此可以把接收任务和处理任务两个阶段分开处理,一个线程接收任务,放入任务队列,另外的线程异步处理任务队列中的任务。
这里写图片描述

单线程Reactor:

总结: 请求接收, 请求读取, 业务逻辑处理在一个线程上处理

3.2 并行化处理模型

这里写图片描述

由于任务处理一般比较缓慢,会导致任务队列中任务积压长时间得不到处理,这时可以使用多线程来处理。这里使用的是一个公共的任务队列,多线程环境中不免要通过加锁来保证线程安全,我们常用的线程池就是这种模式。可以通过为每个线程维护一个任务队列来改进这种模型

多线程Reactor:

总结: 请求接收, 请求读取, 但是对业务逻辑处理进行分离, 交由单独的线程池处理

3.3 Reactor线程模型

这里写图片描述

reactor线程模型关注的是:任务接受之后,对处理过程继续进行切分,划分为多个不同的步骤,每个步骤用不同的线程来处理,也就是原本由一个线程处理的任务现在由多个线程来处理,每个线程在处理完自己的步骤之后,还需要将任务转发到下阶段线程继续进行处理

Multiple Reactor:

总结: 其中mainReacotor,subReactor,Thread Pool是三个线程池。mainReactor负责处理客户端的连接请求,将accept的连接注册到mainReactor的其中一个线程上, 当有请求处于可接收状态, 则调用accept()接收请求, 将返回的SocketChannel注册到select()上由subReactor负责执行处理客户端通道上的数据读写;Thread Pool是具体的业务逻辑线程池,处理具体业务

Netty具体线程模型


1. 如何理解NioEventLoop和NioEventLoopGroup
    1)NioEventLoop实际上就是工作线程,可以直接理解为一个线程。NioEventLoopGroup是一个线程池,线程池中的线程就是 NioEventLoop。
    2)实际上bossGroup中有多个NioEventLoop线程,每个NioEventLoop绑定一个端口,也就是说,如果程序只需要监听1个端口的话,bossGroup里面只需要有一个NioEventLoop线程就行了。

2. 每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selecotr在监听IO就绪事件。而Channel注册到Selector

3. 一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。

4. 一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行检查

注意: 在监听一个端口的情况下,一个NioEventLoop通过一个NioServerSocketChannel监听端口,处理TCP连接。后端多个工作线程NioEventLoop处理IO事件。每个Channel绑定一个NioEventLoop线程,1个NioEventLoop线程关联一个selector来为多个注册到它的Channel监听IO就绪事件。NioEventLoop是单线程执行,保证Channel的pipline在单线程中执行,保证了ChannelHandler的执行顺序

参考播客: Netty整体架构

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值