netty浅述

Posted by KANG's BLOG on Tuesday, May 18, 2021

定义

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,其包含以下优点:

  • 封装JDK自带NIO的API
  • 可靠性能力补齐
  • 高性能,高吞吐量,低资源消耗

特点

NIO

传统BIO模型如下:

img

一个请求Socket请求都有对应一个Thread来承接,由于Thread数量有限,所以该模式不支持高并发,且Thread执行过程中会阻塞请求。

而NIO模型如下:

img

Selector处理所有请求,断开请求和处理线程的耦合,达到多路复用的目的,从而提高并发。

Linux提供select、poll、epoll三种方式来实现IO多路复用,Linux 2.6内核正式引入epoll,其采用Reactor模式实现,完全基于事件驱动,性能相比select和poll有较大提升,Netty、Nginx、Redis等组件中对于网络IO的处理,均使用epoll。

Netty的线程模型

Netty内部实现了两个线程池,boss线程池work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责将请求的read和write事件分发给对应的Handler处理。

零拷贝

一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点。

针对这种情况,当Netty需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在Netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。

核心设计

1 Channel

Channel是Java NIO的一个基本构造,它代表一个实体的开放连接,包括读写操作。

可以把Channel看作是数据传输的载体。

2 Callback

回调是常用的被调用方触发调用方的编程方式之一,Netty内部使用回调的方式来处理事件。

3 Future

Future提供了另一种在操作完成时通知应用程序的方式。

这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。

比如ChannelFuture.connect()方法,作用是异步连接远程节点,方法直接返回,不会阻塞。

4 Event和ChannelHandler

Netty是基于事件驱动的,所以内部定义了非常多的事件。

事件按照入站或出站数据流的相关性进行分类的,其中入站数据或者相关的状态更改而触发的事件包括:

  • 连接已被激活或者连接失活

  • 数据读取

  • 用户事件

  • 错误事件

出站事件是未来将会触发的某个动作的操作结果,这些动作包括:

  • 打开或者关闭到远程节点的连接

  • 将数据写到或者冲刷到套接字

这些事件都可以被分发给ChannelHandler类中的某个用户实现的方法,你可以把ChannelHandler当作为处理特定事件而执行的回调。

核心组件

ByteBuf

网络传输主要靠字节,Java NIO中使用ByteBuffer作为其字节容器,Netty中则提供了更简单易使用的ByteBuf。

工作方式

ByteBuf具有两个索引:一个用来读、一个用来写。

读索引记录已经读到的位置,写索引记录已经写到的位置。read和write开头的方法,都将移动这两个索引。set和get开头的方法则不会。

读索引肯定小于写索引,否则同数组越界类似,抛出异常IndexOutOfBoundsException。分别有isReadable()isWritable()方法来判定是否可以读取或写入索引。

使用模式

1 堆缓冲区

最常用的模式是将数据存储在JVM堆空间中。其内部数据是一个Java数组,所以这种模式被称为**“支撑数组”**。

可以通过hasArray()方法来判定是否有一个支撑数组。

2 直接缓冲区

对于直接缓冲区,其数据都存储在操作系统的物理内存上。

由于本地I/O操作前,都会先从堆缓冲区复制到直接缓冲区上。所以使用直接缓冲区可以省去一次数据复制的代价。

缺点如下:

  • 创建和销毁它需要调用操作系统的方法,非常昂贵,通常建议池化使用。

  • 在Java中操作直接缓冲区上的数据时,需要先复制到堆上。

3 复合缓冲区

Netty通过ByteBuf的子类CompositeByteBuf来实现符合缓冲区,特点是可以根据需要添加或者删除ByteBuf实例。

比如http协议传输消息包含头部和主体,如果发送多个消息均适用同一个主体,而仅仅是头部不同,那么就很适合使用CompositeByteBuf

CompositeByteBuf不支持直接访问其支撑数组,需要先拷贝到一个Byte数组后再进行操作。

清理ByteBuf

清理ByteBuf有两种方式:一个是clean(),另一个是discardReadBytes()

clean()方法会直接重置两个索引,而discardReadBytes()方法则是对数据进行了移动。

读索引左侧的空间被称为“可丢弃空间”,discardReadBytes()方法的原理就是将读索引右侧的数据,覆盖到索引位置为0的地方,简单的说就是将数据“整体左移归零”。

派生缓冲区

调用duplicate()slice()等方法,将建立一个新的ByteBuf实例,具有独立的读写索引和标记索引,但和原ByteBuf实例共享存储空间,这就意味着改变其中一个,另一个的值也会发生变化。

ByteBufHolder

Netty提供了ByteBufHolder接口,用来操作ByteBuf和一些其他属性值,起到了包装ByteBuf的作用。

Netty提供了一个默认的实现DefaultByteBufHolder

ByteBuf的分配

Netty通过ByteBufAllocator接口实现了ByteBuf的分配。

Netty对其有两种实现PooledByteBufAllocatorUnpooledByteBufAllocator

前者默认池化对象,后者则每次访问都返回一个新对象。

引用计数

Netty通过引用计数来计算池化对象的使用情况,如果引用计数为0,所以该对象可以被释放了。

试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCountException

ChannelHandler和ChannelPipeline

ChannelHandler

每一个Channel代表了一个网络连接和IO处理之间的桥梁,Netty中通过一连串的Channelhandler来处理Channel。

Netty定义了ChannelInBoundHandler和ChannelOutBoundHandler两个接口来分别定义入站和出站的处理行为。

ByteBuf内存泄漏

其中入站时,需要在ChannelInBoundHandler的channelRead()方法中手动释放池化的ByteBuf实例相关的内存。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
  ReferenceCountUtil.release(msg); 
}

Netty提供了一个简单的入站处理器实现:SimpleChannelInboundHandler,它会自动释放资源。

如果想要将消息传递给后续处理器而不能释放,则需要增加引用计数,然后通过ChannelhandlerContext传递:

ctx.fireChannelRead(msg.retain());

出站时,如果你处理了write()操作并丢弃了一个消息,那么你也应该负责释放它。释放资源后需要通知ChannelPromise,否则可能会出现ChannelFutureListener收不到某个消息已经被处理了的通知的情况。

ChannelPipeline

ChannelPipiline是可以动态调整的。

每一个ChannelHandler都可以通过添加、删除、替换ChannelHandler来实时修改ChannelPipeline的布局。

ChannelHandlerContext

每一个ChannelHandler都对应了一个ChannelHandlerContext,那么当在 Channel 建立或 ChannelHandler 处理完一个事件后需要传递给下一个 ChannelHandler 时,就会创建一个ChannelHandlerContext。

在一条ChannelPipeline上,从一个ChannelHandler移动到另一个ChannelHandler,是通过ChannelHandlerContext的调用来完成的。

也可以直接操作ChannelHandlerContext,来从ChannelPipeline中某个特定的ChannelHandler来开始调用,而这是一个常见场景。

其他用法

可以通过调用ChannelHandlerContext.pipelines()方法来获取ChannelPipeline的引用,从而通过对其中的ChannelHandler进行操作来完成一些复杂设计。

异常处理

入站异常

如果需要处理异常情况需要重写如下方法:

public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception

异常会按照入站顺序继续在ChannelPipeline中移动,所以后面所有的ChannelHandler中的exceptionCaught()方法仍然会被执行。

出站异常

每个出站操作都将返回一个ChannelFuture,包括异常,所以可通过ChannelFuture的cause()方法来获取异常:

ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
  @Override
  public void operationComplete(ChannelFuture f) {
    if (!f.isSuccess()) {
      f.cause().printStackTrace();
      f.channel().close();
    }
  }
});

EventLoop

主要作用是处理异步事件、管理I/O、管理线程。

当Channel建立时,Netty中的EventLoopGroup会为其分配一个EventLoop,其中包含一个Thread。多个Channel可以共享同一个EventLoop,这有利于提高系统的并发能力。

Channel执行具体的I/O操作时,由EventLoop中的Thread来处理其产生的I/O事件,并将事件分发给对应的ChannelHandler来处理。