Channel和Unsafe

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shenchaohao12321/article/details/89298302

类似于NIO的 Channel,Netty提供了自己的 Channel和其子类实现,用于异步I/O操作和其他相关的操作。
Unsafe是个内部接口,聚合在 Channel中协助进行网络读写相关的操作,因为它的设计初衷就是 Channel的内部辅助类,不应该被Netty框架的上层使用者调用,所以被命名为 Unsafe。这里不能仅从字面理解认为它是不安全的操作,而要从整个架构的设计层面体会它的设计初衷和职责。

1、Channe功能说明

io.netty.channel.Channel是Netty网络操作抽象类,它聚合了一组功能,包括但不限于网路的读、写,客户端发起连接,主动关闭连接,链路关闭,获取通信双方的网络地址等。
它也包含了Netty框架相关的一些功能,包括获取该 Channel的 EventLoop,获取缓冲分配器 ByteBufAllocator和 pipeline等。
下面我们先从 Channel的接口分析,讲解它的主要API和功能,然后再一起看下它的子类的相关功能实现,最后再对重要子类和接口进行源码分析。

1.1、Channel的工作原理

Channel是 Netty抽象出来的网络IO读写相关的接口,为什么不使用 JDK NIO原生的 Channel而要另起炉灶呢,主要原因如下。
(1)JDK的 SocketChannel和 ServerSocketChannel没有统一的 Channel接口供业务开发者使用,对于用户而言,没有统一的操作视图,使用起来并不方便。
(2)JDK的 Socketchannel和 ServerSocketChannel的主要职责就是网络IO操作,由于它们是SPI类接口,由具体的虚拟机厂家来提供,所以通过继承SPI功能类来扩展其功能的难度很大;直接实现 ServerSocketChannel和 SocketChannel抽象类,其工作量和重新开发一个新的 Channel功能类是差不多的。
(3)Netty的 Channel需要能够跟Netty的整体架构融合在一起,例如lO模型、基于ChannelPipeline的定制模型,以及基于元数据描述配置化的TCP参数等,这些JDK的Socketchannel和 ServerSocketChannel都没有提供,需要重新封装。
(4)自定义的 Channel,功能实现更加灵活。
基于上述4个原因, Netty重新设计了 Channel接口,并且给予了很多不同的实现。它的设计原理比较简单,但是功能却比较繁杂,主要的设计理念如下。
(1)在 Channel接口层,采用 Facade模式进行统一封装,将网络IO操作、网络IO相关联的其他操作封装起来,统一对外提供
(2) Channel接口的定义尽量大而全,为 Socketchannel和 Server SocketChannel提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
(3)具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel中,由 Channel统一负责分配和调度,功能实现更加灵活。

1.2、Channel的功能介绍

Channel的功能比较繁杂,我们通过分类的方式对它的主要功能进行介绍。
1.2.1、网络IO操作
Channel网络I/O相关的方法定义如图16-1所示。

下面我们对这些API的功能进行分类说明,读写相关的API列表。
(1) Channel read():从当前的 Channel中读取数据到第一个 inbound缓冲区中,如果数据被成功读取,触发ChannelHandler.channelRead(ChannelHandlerContext, Object)事件读取操作API调用完成之后,紧接着会触发ChannelHandler.channelReadComplete(ChannelHandlerContext)事件,这样业务的Channelhandler可以决定是否需要继续读取数据。如果已经有读操作请求被挂起,则后续的读操作会被忽略。
(2) ChannelFuture write(Object msg):请求将当前的msg通过 ChannelPipeline写入到目标 Channel中。注意, write操作只是将消息存入到消息发送环形数组中,并没有真正被发送,只有调用flush操作才会被写入到 Channel中,发送给对方。
(3) ChannelFuture write(Object msg, ChannelPromise promise): 功能与write(Object msg)相同,但是携带了 ChannelPromise参数负责设置写入操作的结果。
(4) ChannelFuture writeAndFlush(Object msg, ChannelPromise promise): 与方法(3)功能类似,不同之处在于它会将消息写入Channel中发送,等价于单独调用 write和flush操作的组合。
(5) ChannelFuture writeAndFlush(Object msg):功能等同于方法(4),但是没有携带writeAndFlush(Object msg)参数。
(6) Channel flush():将之前写入到发送环形数组中的消息全部写入到目标 Chanel中,发送给通信对方。
(7) ChannelFuture close(ChannelPromise promise):主动关闭当前连接,通过ChannelPromise设置操作结果并进行结果通知,无论操作是否成功,都可以通过 ChannelPromise获取操作结果。该操作会级联触发 ChannelPipeline中所有 ChannelHandler的ChannelHandler.close(ChannelHandlerContext, ChannelPromise)事件。
(8) ChannelFuture disconnect(ChannelPromise promise):请求断开与远程通信对端的连接并使用 Channelpromise来获取操作结果的通知消息。该方法会级联触发 ChannelHandler.disconnect(ChannelHandlerContext, ChannelPromise)事件。
(9) ChannelFuture connect(SocketAddress remoteAddress):客户端使用指定的服务端地址remoteAddress发起连接请求,如果连接因为应答超时而失败, ChannelFuture中的操作结果就是ConnectTimeoutException异常;如果连接被拒绝,操作结果为ConnectException。该方法会级联触发 ChannelHandler.connect(ChannelHandlerContext, SocketAddress, SocketAddress,ChannelPromise)事件
(10) ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress):与方法(9)功能类似,唯一不同的就是先绑定指定的本地地址 localAddress,然后再连接服务端。
(11) ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise)与方法(9)功能类似,唯一不同的是携带了ChannelPromise参数用于写入操作结果。
(12) connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise):与方法(11)功能类似,唯一不同的就是绑定了本地地址。
(13) ChannelFuture bind(Socketaddress localAddress):绑定指定的本地 Socket地址localAddress,该方法会级联触发Channelhandler.bind(ChannelhandlerContext, SocketAddress,ChannelPromise)事件。
(14) ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise): 与方法(13)功能类似,多携带了了一个ChannelPromise用于写入操作结果
(15) ChannelConfig config():获取当前 Channel的配置信息,例如CONNECT_TIMEOUT_MILLIS。
(16) boolean isOpen():判断当前 Channel是否已经打开。
(17) boolean isRegistered():判断当前 Channel是否已经注册到 EventLoop上。
(18) boolean isActive():判断当前 Channel是否已经处于激活状态
(19) ChannelMetadata metadata():获取当前 Channel的元数据描述信息,包括TCP参数配置等。
(20) SocketAddress localAddress():获取当前 Channel的本地绑定地址。
(21) SocketAddress remoteAddress():获取当前 Channel通信的远程 Socket地址。

1.2.2、其他常用的API功能说明

第一个比较重要的方法是 eventLoop()。 Channel需要注册到 EventLoop的多路复用器上,用于处理I/O事件,通过 eventLoop()方法可以获取到 Channel注册的 EventLoop。EventLoop本质上就是处理网络读写事件的 Reactor线程。在Netty中,它不仅仅用来处理网络事件,也可以用来执行定时任务和用户自定义 NioTask等任务。
第二个比较常用的方法是 metadata()方法。熟悉TCP协议的读者可能知道,当创建Socket的时候需要指定TCP参数,例如接收和发送的TCP缓冲区大小、TCP的超时时间、是否重用地址等。在Netty中,每个 Channel对应一个物理连接,每个连接都有自己的TCP参数配置。所以, Channel会聚合一个 ChannelMetadata用来对TCP参数提供元数据描述信息,通过 metadata()方法就可以获取当前 Channe的TCP参数配置。
第三个方法是 parent()。对于服务端 Channe而言,它的父 Channel为空:对于客户端Channel,它的父 Channel就是创建它的 ServerSocketChannel。
第四个方法是用户获取 Channel标识的id(),它返回 ChannelId对象, ChannelId是Channe的唯一标识,它的可能生成策略如下
(1)机器的MAC地址(EUI-48或者EUI-64)等可以代表全局唯一的信息;
(2)当前的进程ID;
(3)当前系统时间的毫秒— System.currentMillis();
(4)当前系统时间纳秒数— System.nanoTimeo();
(5)32位的随机整型数;
(6)32位自增的序列数。

2、Channel源码分析

Channel的实现子类非常多,继承关系复杂,从学习的角度我们抽取最重要的两个Channel——io.netty.channel.socket.nio.NioServerSocketChannel和io.netty.channel.socket.nio.NioSocketChannel进行重点分析。

2.1、Channel的主要继承关系类图

为了便于学习和阅读源码,我们分别看下 NioSocketChannel和 NioServerSocketChannel的继承关系类图。
服务端 NioServerChannel的继承关系类图如图16-2所示

客户端 NioSocketChannel的继承关系类图如图16-3所示。

2.2、AbstractChannel源码分析

2.2.1、成员变量定义
在分析AbstractChannel源码之前,我们先看下它的成员变量定义。

static final ClosedChannelException CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException();
static final NotYetConnectedException NOT_YET_CONNECTED_EXCEPTION = new NotYetConnectedException();

static {
    CLOSED_CHANNEL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
    NOT_YET_CONNECTED_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
}

private MessageSizeEstimator.Handle estimatorHandle;

private final Channel parent;
private final ChannelId id = DefaultChannelId.newInstance();
private final Unsafe unsafe;
private final DefaultChannelPipeline pipeline;
private final ChannelFuture succeededFuture = new SucceededChannelFuture(this, null);
private final VoidChannelPromise voidPromise = new VoidChannelPromise(this, true);
private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
private final CloseFuture closeFuture = new CloseFuture(this);

private volatile SocketAddress localAddress;
private volatile SocketAddress remoteAddress;
private final EventLoop eventLoop;
private volatile boolean registered;

/** Cache for the string representation of this channel */
private boolean strValActive;
private String strVal;

首先定义了两个静态全局异常,如下。
◎ CLOSED_CHANNEL_EXCEPTION:链路已经关闭已经异常;
◎ NOT_YET_CONNECTED_EXCEPTION:物理链路尚未建立异常。
声明完上述两个异常之后,通过静态块将它们的堆栈设置为空的 StackTraceElement。
estimatorHandle用于预测下一个报文的大小,它基于之前数据的采样进行分析预测。
根据之前的 Channel原理分析,我们知道 Abstractchannel采用聚合的方式封装各种功能,从成员变量的定义可以看出,它聚合了以下内容。
◎ parent:代表父类 Channel
◎ id:采用默认方式生成的全局唯一ID:
◎ unsafe: Unsafe实例;
◎ pipeline:当前 Channel对应的 DefaultChannelPipeline;
◎ eventLoop:当前 Channel注册的 EventLoop;

在此不一一枚举。通过变量定义可以看出, AbstractChannel聚合了所有 Channel使用到的能力对象,由 AbstractChannel提供初始化和统一封装,如果功能和子类强相关,则定义成抽象方法由子类具体实现,下面的小节就对它的主要API进行源码分析。

2.2.2、核心AP源码分析

首先看下网络读写操作,前面介绍网络IO操作时讲到,它会触发 ChannelPipeline中对应的事件方法。Netty基于事件驱动,我们也可以理解为当 Chnanel进行IO操作时会产生对应的IO事件,然后驱动事件在 ChannelPipeline中传播,由对应的 ChannelHandler对事件进行拦截和处理,不关心的事件可以直接忽略。采用事件驱动的方式可以非常轻松地通过事件定义来划分事件拦截切面,方便业务的定制和功能扩展,相比AOP,其性能更高,但是功能却基本等价网络IO操作直接调用 DefaultChannelPipeline的相关方法,由 DefaultChannelPipeline中对应的 ChannelHandler进行具体的逻辑处理,如下所示。

@Override
public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
    return pipeline.connect(remoteAddress, localAddress);
}

@Override
public ChannelFuture disconnect() {
    return pipeline.disconnect();
}

@Override
public ChannelFuture close() {
    return pipeline.close();
}

@Override
public Channel flush() {
    pipeline.flush();
    return this;
}

AbstractChannel也提供了一些公共API的具体实现,例如 localAddress()和remoteAddress()方法,它的源码实现如下所示。

@Override
public SocketAddress remoteAddress() {
    SocketAddress remoteAddress = this.remoteAddress;
    if (remoteAddress == null) {
        try {
            this.remoteAddress = remoteAddress = unsafe().remoteAddress();
        } catch (Throwable t) {
            // Sometimes fails on a closed socket in Windows.
            return null;
        }
    }
    return remoteAddress;
}

首先从缓存的成员变量中获取,如果第一次调用为空,需要通过 unsafe的remoteAddress获取,它是个抽象方法,具体由对应的 Channel子类实现。

2.3、AbstractNioChannel源码分析

2.3.1、成员变量定义

首先,还是从成员变量定义入手,来了解下它的功能实现,成员变量定义如下所示。

private final SelectableChannel ch;
protected final int readInterestOp;
private volatile SelectionKey selectionKey;
private volatile boolean inputShutdown;

/**
 * The future of the current connection attempt.  If not null, subsequent
 * connection attempts will fail.
 */
private ChannelPromise connectPromise;
private ScheduledFuture<?> connectTimeoutFuture;
private SocketAddress requestedRemoteAddress;

由于 NIOChannel、NioSocketChannel和 NioServerSocketChannel需要共用,所以定义了一个 java.nio.SocketChannel和 Java.nio.ServerSocketChannel的公共父类 SelectableChannel,用于设置 SelectableChannel参数和进行IO操作。
第二个参数是 readInterestOp,它代表了 JDK SelectionKey的 OP_READ。
随后定义了一个 volatile修饰的 SelectionKey,该 SelectionKey是 Channel注册到EventLoop后返回的选择键。由于 Channel会面临多个业务线程的并发写操作,当SelectionKey由 SelectionKey修改之后,为了能让其他业务线程感知到变化,所以需要使用 volatile保证修改的可见性,后面的多线程章节会专门对 volatile的使用进行说明。
最后定义了代表连接操作结果的 ChannelPromise以及连接超时定时器ScheduledFuture和请求的通信地址信息。

2.3.2、核心API源码分析

我们一起看下在 AbstractNioChannel实现的主要API,首先是 Channel的注册,如下所示。

@Override
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                // Force the Selector to select now as the "canceled" SelectionKey may still be
                // cached and not removed because no Select.select(..) operation was called yet.
                eventLoop().selectNow();
                selected = true;
            } else {
                // We forced a select operation on the selector before but the SelectionKey is still cached
                // for whatever reason. JDK bug ?
                throw e;
            }
        }
    }
}

定义一个布尔类型的局部变量 selected来标识注册操作是否成功,调用 SelectableChannel的 register()方法,将当前的 Channel注册到 EventLoop的多路复用器上, SelectableChannel的注册方法定义如图16-9所示。

注册 Channel的时候需要指定监听的网络操作位来表示 Channel对哪几类网络事件感兴趣,具体的定义如下。
◎ public static final int OP READ=1<<0:读操作位;
◎ public static final int OP WRITE=1<<2:写操作位;
◎ public static final int OP CONNECT=1<<3:客户端连接服务端操作位;
◎ public static final int OP ACCEPT=1<<4:服务端接收客户端连接操作位。
AbstractNioChannel注册的是0,说明对任何事件都不感兴趣,仅仅完成注册操作。注册的时候可以指定附件,后续 Channel接收到网络事件通知时可以从 SelectionKey中重新获取之前的附件进行处理,此处将 AbstractChannel的实现子类自身当作附件注册。如果注册 Channel成功,则返回 selectionKey,通过 selectionKey可以从多路复用器中获取Channel对象。

如果当前注册返回的 selectionKey已经被取消,则抛出 CancelledKeyException异常,捕获该异常进行处理。如果是第一次处理该异常,调用多路复用器的 selectNow()方法将已经取消的 selectionKey从多路复用器中删除掉。操作成功之后,将 selected置为true,说
明之前失效的 selectionKey已经被删除掉。继续发起下一次注册操作,如果成功则退出,如果仍然发生 CancelledKeyException异常,说明我们无法删除已经被取消的 selectionKey,按照JDK的AP说明,这种意外不应该发生。如果发生这种问题,则说明可能NIO的相
关类库存在不可恢复的BUG,直接抛出 CancelledKeyException异常到上层进行统一处理下面继续看另一个比较重要的方法:准备处理读操作之前需要设置网路操作位为读,代码如下所示。

@Override
protected void doBeginRead() throws Exception {
    if (inputShutdown) {
        return;
    }

    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

先判断下 Channel是否关闭,如果处于关闭中,则直接返回。获取当前的 SelectionKey进行判断,如果可用,说明 Channel当前状态正常,则可以进行正常的操作位修改。将SelectionKey当前的操作位与读操作位进行按位与操作,如果等于0,说明目前并没有设置读操作位,通过 interestOps I readInterestOp设置读操作位,最后调用 selectionKey的interestOps方法重新设置通道的网络操作位,这样就可以监听网络的读事件了。
实际上,对于读操作位的判断和修改与 JDK NIO SelectionKey的相关方法实现是等价的,如图16-11所示。

2.4、AbstractNioByteChannel源码分析

由于成员变量只有一个 Runnable类型的 flushTask来负责继续写半包消息,所以对成员变量不再单独进行介绍。
最主要的方法就是 doWrite(ChannelOutboundBuffer in),下面一起看看它的实现,由于该方法过长,所以我们按照其逻辑进行拆分介绍。如图16-12所示。

从发送消息环形数组 ChannelOutboundBuffer弹出一条消息,判断该消息是否为空,如果为空,说明消息发送数组中所有待发送的消息都己经发送完成,清除半包标识,然后退出循环。清除半包标识的 clearOpWrite方法实现如图16-13所示。

从当前 SelectionKey中获取网络操作位,然后与 SelectionKey.OP_WRITE做按位与,如果不等于0,说明当前的 SelectionKey是isWritable的,需要清除写操作位。清除方法很简单,就是 SelectionKey.OP_WRITE取非之后与原操作位做按位与操作,清除SelectionKey的写操作位。继续看源码,如果需要发送的消息不为空,则继续处理。如图16-14所示。

首先判断需要发送的消息是否是 ByteBuf类型,如果是,则进行强制类型转换,将其转换成 ByteBuf类型,判断当前消息的可读字节数是否为0,如果为0,说明该消息不可读,需要丢弃。从环形发送数组中删除该消息,继续循环处理其他的消息声明消息发送相关的成员变量,包括:写半包标识、消息是否全部发送标识、发送的总消息字节数。
这些局部变量创建完成之后,对循环发送次数进行判断,如果为-1,则从 Channel配置对象中获取循环发送次数。循环发送次数是指当一次发送没有完成时(写半包),继续循环发送的次数。设置写半包最大循环次数的原因是当循环发送的时候,IO线程会一直尝试进行写操作,此时O线程无法处理其他的IO操作,例如读新的消息或者执行定时任务和 NioTask等,如果网络IO阻塞或者对方接收消息太慢,可能会导致线程假死。继续看循环发送的代码如图16-15所示。

调用 doWriteBytes进行消息发送,不同的 Channel子类有不同的实现,因此它是抽象方法。如果本次发送的字节数为0,说明发送TCP缓冲区已满,发生了ZERO_WINDOW。此时再次发送仍然可能出现写0字节,空循环会占用CPU的资源,导致IO线程无法处理其他IO操作,所以将写半包标识 setOpWrite设置为true,退出循环,释放IO线程。
如果发送的字节数大于0,则对发送总数进行计数。判断当前消息是否已经发送成功(缓冲区没有可读字节),如果发送成功则设置done为true,退出当前循环。
消息发送操作完成之后调用 ChannelOutboundBuffer更新发送进度信息,然后对发送结果进行判断。如果发送成功,则将已经发送的消息从发送数组中删除;否则调用incompleteWrite方法,设置写半包标识,启动刷新线程继续发送之前没有发送完全的半包消息(写半包)。如图16-16所示。

处理半包发送任务的方法 incompleteWrite的实现如图16-17所示。

首先判断是否需要设置写半包标识,如果需要则调用 setOpWrite设置写半包标识,代码如图16-18所示。

设置写半包标识就是将 SelectionKey设置成可写的,通过原操作位与SelectionKey.OP_WRITE做按位或操作即可实现如果 SelectionKey的OP_ WRITE被设置,多路复用器会不断轮询对应的 Channel,用于处理没有发送完成的半包消息,直到清除 SelectionKey的OP_WRITE操作位。因此,设置了 OP_WRITE操作位后,就不需要启动独立的 Runnable来负责发送半包消息了。
如果没有设置 OP_WRITE操作位,需要启动独立的 Runnable,将其加入到 EventLoop中执行,由 Runnable负责半包消息的发送。它的实现很简单,就是调用 flush方法来发送缓冲数组中的消息。
消息发送的另一个分支是文件传输,由于它的实现原理与 ByteBuf类似,限于篇幅,在此不再详细说明。

2.5、AbstractNioMessageChannel源码分析

由于 AbstractNioMessageChannel没有自己的成员变量,所以我们直接对其方法进行说明。
它的主要实现方法只有一个: doWrite(ChannelOutboundBuffer in),下面首先看下它的源码,如图16-19所示。
在循环体内对消息进行发送,从 ChannelOutboundBuffer中弹出一条消息进行处理,如果消息为空,说明发送缓冲区为空,所有消息都已经被发送完成。清除写半包标识,退出循环。
与 AbstractNioByteChannel的循环发送类似,利用 writeSpinCount对单条消息进行发送,调用 doWriteMessage(Object msg, ChannelOutboundBuffer in)判断消息是否发送成功,如果成功,则将发送标识done设置为true,退出循环;否则继续执行循环,直到执行
writeSpinCount次。

发送操作完成之后,判断发送结果,如果当前的消息被完全发送出去,则将该消息从缓冲数组中删除;否则设置半包标识,注册 SelectionKey.OP_WRITE到多路复用器上,由多路复用器轮询对应的 Channel重新发送尚未发送完全的半包消息。
通过代码分析我们发现, AbstractNioMessageChannel和 AbstractNioByteChannel的消息发送实现比较相似,不同之处在于:一个发送的是 ByteBuf或者 FileRegion,它们可以直接被发送:另一个发送的则是POJO对象。

2.6、AbstractNioMessageServerChannel源码分析

AbstractNioMessageServerChannel的实现非常简单,它定义了一个 EventLoopGroup类型的 childGroup,用于给新接入的客户端 NioSocketchannel分配 EventLoop,它的源码实现如图16-20所示。

每当服务端接入一个新的客户端连接 NioSocketChannel时,都会调用 childEventLoopGroup方法获取 EventLoopGroup线程组,用于给 NioSocketChannel分配 Reactor线程 EventLoop,相关分配代码如图16-21所示。

2.7、NioServerSocketChannel源码分析

NioServerSocketChannel的实现比较简单,下面我们重点分析主要API的实现,首先看它的成员变量定义和静态方法,如图16-22所示

首先创建了静态的 ChannelMetadata成员变量,然后定义了 ServerSocketChannelConfig用于配置 ServerSocketchannel的TCP参数。静态的 newSocket方法用于通过ServerSocketChannel的open打开新的 ServerSocketChannel通道。
接着我们再看下 ServerSocketChannel相关的接口实现: inActive、 remoteAddress、javaChannel和 doBind,它们的源码如图16-23所示。

通过java.net.ServerSocket的 isBound方法判断服务端监听端口是否处于绑定状态,它的 remoteAddress为空。 javaChannel的实现是java.nio.ServerSocketChannel,服务端在进行端口绑定的时候,可以指定 backlog,也就是允许客户端排队的最大长度。
下面继续看服务端 Channel的 doReadMessages( List<Object> buf)的实现,如图16-25所示。

首先通过 ServerSocketChannel的 accept接收新的客户端连接,如果 SocketChannel不为空,则利用当前的 NioServerSocketChannel、 EventLoop和 SocketChannel创建新的NioSocketchannel,并将其加入到 List<object> buf中,最后返回1,表示服务端消息读取成功。
对于 NioServerSocketChannel,它的读取操作就是接收客户端的连接,创建NioSocketChannel对象。
最后看下与服务端 Channel无关的接口定义,由于这些方法是客户端 Channel相关的,因此,对于服务端 Channel无须实现。如果这些方法被误调,则返回 UnsupportedOperationException异常,这些方法的源码如图16-26所示。

2.8、NioSocketChannel源码分析

2.8.1、连接操作
我们重点分析与客户端连接相关的API实现,首先看连接方法的实现,如图16-27所示。


判断本地 Socket地址是否为空,如果不为空则调用 java.nio.channels.SocketChannel.socket(). bind()方法绑定本地地址。如果绑定成功,则继续调用java.nio.channels.SocketChannel.connect(SocketAddress remote)发起TCP连接。对连接结果进行判断,连接结果有以下三种可能。
(1)连接成功,返回true;
(2)暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回 false;
(3)连接失败,直接抛出I/O异常

如果是结果(2),需要将NioSocketChannel中的 selectionKey设置为 OP_CONNECT,监听连接网络操作位。如果抛出了IO异常,说明客户端的TCP握手请求直接被REST或者被拒绝,此时需要关闭客户端连接,代码如图16-28所示。

2.8.2、写半包

分析完连接操作之后,继续分析写操作,由于它的实现比较复杂,所以仍然需要将其拆分后分段进行分析,代码如图16-29所示。
获取待发送的 Byte Buf个数,如果小于等于1,则调用父类 AbstractNioByteChannel的 doWrite方法,操作完成之后退出。
在批量发送缓冲区的消息之前,先对一系列的局部变量进行赋值,首先,获取需要发送的 ByteBuffer数组个数 nioBufferCnt,然后,从 ChannelOutboundBuffer中获取需要发送的总字节数,从 NioSocketChannel中获取NO的 Socketchannel,将是否发送完成标识设
置为 false,将是否有写半包标识设置为 false。如图16-30所示。

继续分析循环发送的代码,代码如图16-31所示。

就像循环读一样,我们需要对一次 Selector轮询的写操作次数进行上限控制,因为如果TCP的发送缓冲区满,TCP处于KEEP- ALIVE状态,消息会无法发送出去,如果不对上限进行控制,就会长时间地处于发送状态, Reactor线程无法及时读取其他消息和执行排队的Task。所以,我们必须对循环次数上限做控制。
调用 NIOSocketChannel的 write方法,它有三个参数:第一个是需要发送的 ByteBuffer数组,第二个是数组的偏移量,第三个参数是发送的 ByteBuffer个数。返回值是写入SocketChannel的字节个数。
下面对写入的字节进行判断,如果为0,说明TCP发送缓冲区已满,很有可能无法再写进去,因此从循环中跳出,同时将写半包标识设置为true,用于向多路复用器注册写操作位,告诉多路复用器有没发完的半包消息,需要轮询出就绪的 SocketChanne继续发送。
代码如图16-32所示。

发送操作完成后进行两个计算:需要发送的字节数要减去已经发送的字节数;发送的字节总数+已经发送的字节数。更新完这两个变量后,判断缓冲区中所有的消息是否已经发送完成。如果是,则把发送完成标识设置为true同时退出循环;如果没有发送完成,则继续循环。从循环发送中退出之后,首先对发送完成标识done进行判断,如果发送完成,则循环释放己经发送的消息。环形数组的发送缓冲区释放完成后,取消半包标识,告诉多路复用器消息已经全部发送完成。代码如图16-33所示。

当缓冲区中的消息没有发送完成,甚至某个 ByteBuffer只发送了几个字节,出现了所谓的“写半包”时,该怎么办?下面我们继续看看Netty是如何处理“写半包”的,如图16-34所示。

首先,循环遍历发送缓冲区,对消息的发送结果进行判断,下面具体展开进行说明。
(1)从 ChannelOutboundBuffer弹出第一条发送的 ByteBuf,然后获取该 ByteBuf的读索引和可读字节数。
(2)对可读字节数和发送的总字节数进行比较,如果发送的字节数大于可读的字节数说明当前的 Byte Buf己经被完全发送出去,更新 ChannelOutboundBuffer的发送进度信息,将已经发送的 Byte Buf删除,释放相关资源。最后,发送的字节数要减去第一条发送的字
节数,得到后续消息发送的总字节数,然后继续循环判断第二条消息、第三条消息……
(3)如果可读的消息大于已经发送的总字节数,说明这条消息没有被完整地发送出去,仅仅发送了部分数据报,也就是出现了所谓的“写半包”问题。此时,需要更新可读的索引为当前索引+已经发送的总字节数,然后更新 ChannelOutboundBuffer的发送进度信息,退出循环。
(4)如果可读字节数等于已经发送的总字节数,则说明最后一次发送的消息是个整包消息,没有剩余的半包消息待发送。更新发送进度信息,将最后一条已发送的消息从缓冲区中删除,最后退出循环。循环发送操作完成之后,更新 SocketChannel的操作位为 OP_WRITE,由多路复用器在下一次轮询中触发 Socketchannel,继续处理没有发送完成的半包消息。

2.8.3、读写操作

NioSocketChannel的读写操作实际上是基于NlO的 SocketChanne和Netty的 ByteBuf封装而成,下面我们首先分析从 SocketChannel中读取数据报,如图16-35所示。

它有两个参数,说明如下

java.nio.channels.SocketChannel: JDK NIO 的SocketChannel
length: ByteBuf的可写最大字节数。
实际上就是从 SocketChannel中读取L个字节到 ByteBuf中,L为 ByteBuf可写的字节数,下面我们看下 ByteBuf writeBytes方法的实现,如图16-36所示。

首先分析 setBytes(int index, ScatteringByteChannel in, int length)在 UnpooledHeapByteBuf中的实现,如图16-37所示。

从 SocketChannel中读取字节数组到缓冲区 java.nio.ByteBuffer中,它的起始 position为 writelndex, limit为 writelndex+ length。

4、Unsafe源码分析

实际的网络I操作基本都是由 Unsafe功能类负责实现的,下面我们一起看下它的主要功能子类和重要的API实现。

4.1、Unsafe继承关系类图

首先看下如图16-39所示 Unsafe接口的类继承关系图。

4.2、AbstractUnsafe源码分析

4.2.1、register方法

register方法主要用于将当前 Unsafe对应的 Channel注册到 EventLoop的多路复用器上,然后调用 DefaultChannelPipeline的 fireChannelRegistered方法。如果 Channe被激活,则调用 DefaultChannelPipeline的 fireChannelActive方法。源码如图16-40所示。
首先判断当前所在的线程是否是 Channel对应的 NioEventLoop线程,如果是同一个线程,则不存在多线程并发操作问题,直接调用 register进行注册;如果是由用户线程或者其他线程发起的注册操作,则将注册操作封装成 Runnable,放到 NioEventLoop任务队列中执行。注意:如果直接执行 register方法,会存在多线程并发操作 Channel的问题。

下面继续看 register方法的实现,代码如图16-41所示首先调用 ensureOpen方法判断当前 Channel是否打开,如果没有打开则无法注册,直接返回。校验通过后调用 deRegister方法,它由 AbstractNioUnsafe对应的 AbstractNioChannel实现,代码如图16-42所示。

该方法在前面的 AbstractNioChannel源码分析中已经介绍过,此处不再赘述。如果deRegister方法没有抛出异常,则说明 Channel注册成功。将 ChannelPromise的结果设置为成功,调用 ChannelPipeline的 fireChannelRegistered方法,判断当前的 Channel是否已经被激活,如果已经被激活,则调用 ChannelPipeline的 fireChannelActive方法。
如果注册过程中发生了异常,则强制关闭连接,将异常堆栈信息设置到 ChannelPromise中。

4.2.2、bind方法

bind方法主要用于绑定指定的端口,对于服务端,用于绑定监听端口,可以设置 backlog参数;对于客户端,主要用于指定客户端 Channel的本地绑定 Socket地址。代码实现如图16-43所示。

调用 doBind方法,对于 NioSocketChannel和 NioServerSocketChannel有不同的实现,客户端的实现代码如图16-44所示。

服务端的 doBind方法实现如图16-45所示

如果绑定本地端口发生异常,则将异常设置到ChannelPromise中用于通知ChannelFuture,随后调用 closelfClosed方法来关闭Channel。

4.2.3、disconnect方法

disconnect用于客户端或者服务端主动关闭连接,它的代码如图16-46所示。

4.2.4、close方法

在链路关闭之前需要首先判断是否处于刷新状态,如果处于刷新状态说明还有消息尚未发送出去,需要等到所有消息发送完成再关闭链路,因此,将关闭操作封装成 Runnable稍后再执行。如图16-47所示。

如果链路没有处于刷新状态,需要从 closeFuture中判断关闭操作是否完成,如果已经完成,不需要重复关闭链路,设置 ChannelPromise的操作结果为成功并返回执行关闭操作,将消息发送缓冲数组设置为空,通知JVM进行内存回收。调用抽象方法 doClose关闭链路。源码如图16-48所示。

如果关闭操作成功,设置 ChannelPromise结果为成功。如果操作失败,则设置异常对象到ChannelPromise中调用ChannelOutboundBuffer的 close方法释放缓冲区的消息,随后构造链路关闭通知Runnable放到 NioEventLoop中执行。源码如图16-49所示。

最后,调用 deregister方法,将 Channel从多路复用器上取消注册,代码实现如图16-50所示。

NioEventLoop的 cancel方法实际将 selectionKey对应的 Channel从多路复用器上去注册, NioEventLoop的相关代码如图16-51所示。

4.2.5、write方法

write方法实际上将消息添加到环形发送数组中,并不是真正的写 Channel,它的代码如图16-52所示。

如果 Channel没有处于激活状态,说明TCP链路还没有真正建立成功,当前 Channel存在以下两种状态。
(1) Channel打开,但是TCP链路尚未建立成功: NOT_YET_CONNECTED_EXCEPTION;
(2) Channel已经关闭: CLOSED_CHANNEL_EXCEPTION。
对链路状态进行判断,给 ChannelPromise设置对应的异常,然后调用 ReferenceCountUtil的 release方法释放发送的msg对象。
如果链路状态正常,则将需要发送的msg和 promise放入发送缓冲区中(环形数组)。

4.2.6、fush方法

flush方法负责将发送缓冲区中待发送的消息全部写入到 Channel中,并发送给通信对方。它的代码如图16-53所示。

首先将发送环形数组的 unflushed指针修改为tail,标识本次要发送消息的缓冲区范围然后调用fush0进行发送,由于fush0代码非常简单,我们重点分析 doWrite方法,代码如图16-54所示。

首先计算需要发送的消息个数(unflushed-flush),如果只有1个消息需要发送,则调用父类的写操作,我们分析 AbstractNioByteChannel的 doWrite方法,代码如图16-55所示。

因为只有一条消息需要发送,所以直接从 ChannelOutboundBuffer中获取当前需要发送的消息,代码如图16-56所示。

首先,获取需要发送的消息,如果消息为ByteBuf且它分配的是JDK的非堆内存,则直接返回。对返回的消息进行判断,如果为空,说明该消息已经发送完成并被回收,然后执行清空 OP_WRITE操作位的 clearOpWrite方法,代码如图16-57所示。

继续向下分析,如果需要发送的 ByteBuf已经没有可写的字节了,则说明已经发送完成,将该消息从环形队列中删除,然后继续循环,代码如图16-58所示。

下面我们分析下 ChannelOutboundBuffer的 remove方法,如图16-59所示。

首先判断环形队列中是否还有需要发送的消息,如果没有,则直接返回。如果非空,则首先获取 Entry,然后对其进行资源释放,同时对需要发送的索引 flushed进行更新。所有操作执行完之后,调用 decrementPendingOutboundBytes减去已经发送的字节数,该方法跟 incrementPendingOutboundBytes类似,会进行发送低水位的判断和事件通知,此处不再赘述。
我们接着继续对消息的发送进行分析,代码如图16-60所示。

首先将半包标识设置为 false,从 DefaultSocketChannelConfig中获取循环发送的次数,进行循环发送,对发送方法 doWriteBytes展开分析,如图16-61所示。

ByteBuf的 readBytes()方法的功能是将当前 ByteBuf中的可写字节数组写入到指定的Channel中。方法的第一个参数是 Channel,此处就是 SocketChannel,第二个参数是写入的字节数组长度,它等于 ByteBuf的可读字节数,返回值是写入的字节个数。由于我们将
SocketChannel设置为异步非阻塞模式,所以写操作不会阻塞。
从写操作中返回,需要对写入的字节数进行判断,如果为0,说明TCP发送缓冲区已满,不能继续再向里面写入消息,因此,将写半包标识设置为true,然后退出循环,执行后续排队的其他任务或者读操作,等待下一次 selector的轮询继续触发写操作。
对写入的字节数进行累加,判断当前的 ByteBuf中是否还有没有发送的字节,如果没有可发送的字节,则将done设置为true,退出循环
从循环发送状态退出后,首先根据实际发送的字节数更新发送进度,实际就是发送的字节数和需要发送的字节数的一个比值。执行完进度更新后,判断本轮循环是否将需要发送的消息全部发送完成,如果发送完成则将该消息从循环队列中删除;否则,设置多路复用器的 OP_WRITE操作位,用于通知 Reactor线程还有半包消息需要继续发送。

4.3、AbstractNioUnsafe源码分析

AbstractNioUnsafe是AbsstractUnsafe类的NIO实现,它主要实现了connect、finishConnect等方法,下面我们对重点API实现进行源码分析。

4.3.1、connect方法

首先获取当前的连接状态进行缓存,然后发起连接操作,代码如图16-62所示。


需要指出的是, Socketchannel执行 connect()操作有三种可能的结果。

  1. 连接成功,返回true
  2. 暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回 false;
  3. 连接失败,直接抛出IO异常

如果是第(2)种结果,需要将 NioSocketchannel中的 selectionKey设置为OP_ CONNECT,监听连接应答消息。
异步连接返回之后,需要判断连接结果,如果连接成功,则触发 ChannelActive事件,代码如图16-63所示。

这里对 ChannelActive事件处理不再进行详细说明,它最终会将 NioSocketChannel中的 selectionKey设置为 SelectionKey.OP_READ,用于监听网络读操作位。如果没有立即连接上服务端,则执行如图16-64所示分支。

上面的操作有两个目的
(1)根据连接超时时间设置定时任务,超时时间到之后触发校验,如果发现连接并没有完成,则关闭连接句柄,释放资源,设置异常堆栈并发起去注册。
(2)设置连接结果监听器,如果接收到连接完成通知则判断连接是否被取消,如果被取消则关闭连接句柄,释放资源,发起取消注册操作。

4.3.2、finishConnect方法
客户端接收到服务端的TCP握手应答消息,通过 Socketchannel的 finishConnect方法对连接结果进行判断,代码如图16-65所示。

首先缓存连接状态,当前返回 false,然后执行 doFinishConnect方法判断连接结果,代码如图16-66所示。

通过 Socketchannel的 finishConnect方法判断连接结果,执行该方法返回三种可能结果。
◎连接成功返回true;
◎连接失败返回 false;
◎发生链路被关闭、链路中断等异常,连接失败
只要连接失败,就抛出Eror(),由调用方执行句柄关闭等资源释放操作,如果返回成功,则执行 fulfillConnectPromise方法,它负责将 SocketChannel修改为监听读操作位,
用来监听网络的读事件,代码如图16-67所示。

最后对连接超时进行判断:如果连接超时时仍然没有接收到服务端的ACK应答消息,则由定时任务关闭客户端连接,将 Socket channel从 Reactor线程的多路复用器上摘除,释放资源,代码如图16-68所示。

4.4、NioByteUnsafe源码分析

我们重点分析它的read方法,源码如图16-69所示。

首先,获取 NioSocketchannel的 SocketChannelConfig,它主要用于设置客户端连接的TCP参数,接口如图16-70所示。


继续看 allocHandle的初始化。如果是首次调用,从 SocketChannelConfig的RecvByteBufAllocator中创建 Handle。下面我们对 RecvByteBufAllocator进行简单地代码分析: RecvByteBufAllocator默认有两种实现,分别是 AdaptiveRecvByteBufAllocator和
FixedRecvByteBufAllocator。由于 FixedRecvByteBufAllocator的实现比较简单,我们重点分析 AdaptiveRecvByteBufAllocator的实现。如图16-71所示。顾名思义, AdaptiveRecvByteBufAllocator指的是缓冲区大小可以动态调整的 ByteBuf分配器。它的成员变量定义如图16-72所示。

它分别定义了三个系统默认值:最小缓冲区长度64字节、初始容量1024字节、最大容量65536字节。还定义了两个动态调整容量时的步进参数:扩张的步进索引为4、收缩的步进索引为1。
最后,定义了长度的向量表 SIZE_TABLE并初始化它,初始值如图16-73所示。

向量数组的每个值都对应一个 Buffer容量,当容量小于512的时候,由于缓冲区已经比较小,需要降低步进值,容量每次下调的幅度要小些;当大于512时,说明需要解码的消息码流比较大,这时采用调大步进幅度的方式减少动态扩张的频率,所以它采用512的倍数进行扩张。
接下来我们重点分析下 AdaptiveRecvByteBufAllocator的方法。
方法1: getSizeTablelndex(final int size),代码如图16-74所示。

根据容量Size査找容量向量表对应的索引—一这是个典型的二分查找法,由于它的算法非常经典,也比较简单,此处不再赘述。
下面我们分析下它的内部静态类 Handlelmpl,首先,还是看下它的成员变量,如图16-75所示。

它有5个成员变量,分别是:对应向量表的最小索引、最大索引、当前索引、下一次预分配的Buffer大小和是否立即执行容量收缩操作。
我们重点分析它的 record(int actualReadBytes)方法:当 NioSocketChannel执行完读操作后,会计算获得本次轮询读取的总字节数,它就是参数 actualReadBytes,执行 record方法,根据实际读取的字节数对 ByteBuf进行动态伸缩和扩张,代码如图16-76所示

首先,对当前索引做步进缩减,然后获取收缩后索引对应的容量,与实际读取的字节数进行比对,如果发现小于收缩后的容量,则重新对当前索引进行赋值,取收缩后的索引和最小索引中的较大者作为最新的索引。然后,为下一次缓冲区容量分配赋值——新的索引对应容量向量表中的容量。相反,如果当前实际读取的字节数大于之前预分配的初始容量,则说明实际分配的容量不足,需要动态扩张。重新计算索引,选取当前索引+扩张步进和最大索引中的较小作为当前索引值,然后对下次缓冲区的容量值进行重新分配,完成缓冲区容量的动态扩张。
通过上述分析我们得知, AdaptiveRecvByteBufAllocator就是根据本次读取的实际字节数对下次接收缓冲区的容量进行动态调整使用动态缓冲区分配器的优点如下。
(1) Netty作为一个通用的NlO框架,并不对用户的应用场景进行假设,可以使用它做流媒体传输,也可以用它做聊天工具。不同的应用场景,传输的码流大小千差万别,无论初始化分配的是32KB还是IMB,都会随着应用场景的变化而变得不适应。因此,Netty根据上次实际读取的码流大小对下次的接收 Buffer缓冲区进行预测和调整,能够最大限度地满足不同行业的应用场景。
(2)性能更高,容量过大会导致内存占用开销增加,后续的 Buffer处理性能会下降容量过小时需要频繁地内存扩张来接收大的请求消息,同样会导致性能下降。
(3)更节约内存。假如通常情况下请求消息平均值为1MB左右,接收缓冲区大小为12MB突然某个客户发送了一个10MB的流媒体附件,接收缓冲区扩张为10MB以接纳该附件如果缓冲区不能收缩,每次缓冲区创建都会分配10MB的内存,但是后续所有的消息都是1MB左右,这样会导致内存的浪费,如果并发客户端过多,可能会发生内存溢出,最终宕机。
看完了 AdaptiveRecvByteBufAllocator,我们继续分析读操作。
首先通过接收缓冲区分配器的 Handler计算获得下次预分配的缓冲区容量byteBufCapacity,如图16-77所示。紧接着根据缓冲区容量进行缓冲区分配,Netty的缓冲区种类很多,此处重点介绍的是消息的读取,因此对缓冲区不展开说明。

接收缓冲区 ByteBuf分配完成后,进行消息的异步读取,代码如图16-78所示。

它是个抽象方法,具体实现在 NioSocketChannel中,代码如图16-79所示。

其中 javaChannel()返回的是 SocketChannel,代码如图16-80所示。

byteBuf.writableBytes返回本次可读的最大长度,我们继续展开看最终是如何从Channel中读取码流的,代码如图16-81所示。

对 setBytes方法展开分析如图16-82所示。

由于 SocketChanne的read方法参数是 Java NIO的 ByteBuffer,所以,需要先将 Netty的 ByteBuf转换成JDK的 ByteBuffer,随后调用 ByteBuffer的 clear方法对指针进行重置用于新消息的读取,随后将 position指针指到初始读 index,读取的上限设置为 index+读取的长度。最后调用read方法将 Socketchannel中就绪的码流读取到 ByteBuffer中,完成消息的读取,返回读取的字节数完成消息的异步读取后,需要对本次读取的字节数进行判断,有以下三种可能:
(1)返回0,表示没有就绪的消息可读
(2)返回值大于0,读到了消息
(3)返回值-1,表示发生了IO异常,读取失败。
下面我们继续看Nety的后续处理,首先对读取的字节数进行判断,如果等于或者小于0,表示没有就绪的消息可读或者发生了IO异常,此时需要释放接收缓冲区:如果读取的字节数小于0,则需要将 close状态位置位,用于关闭连接,释放句柄资源。置位完成之后,退出循环。源码如图16-83所示。

完成一次异步读之后,就会触发一次 ChannelRead事件,这里要特别提醒大家的是:
完成一次读操作,并不意味着读到了一条完整的消息,因为TCP底层存在组包和粘包,所以,一次读操作可能包含多条消息,也可能是一条不完整的消息。因此不要把它跟读取的消息个数等同起来。在没有做任何半包处理的情况下,以 ChannelRead的触发次数做计数器来进行性能分析和统计,是完全错误的。当然,如果你使用了半包解码器或者处理了半包,就能够实现一次 ChannelRead对应一条完整的消息触发和完成 ChannelRead事件调用之后,将接收缓冲区释放,代码如图16-84所示。

因为一次读操作未必能够完成TCP缓冲区的全部读取工作,所以,读操作在循环体中进行,每次读取操作完成之后,会对读取的字节数进行累加,代码如图16-85所示。

在累加之前,需要对长度上限做保护,如果累计读取的字节数已经发生溢出,则将读取到的字节数设置为整型的最大值,然后退出循环。原因是本次循环已经读取过多的字节,需要退出,否则会影响后面排队的Task任务和写操作的执行。如果没有溢出,则执行累加操作。代码如图16-86所示。

最后,对本次读取的字节数进行判断,如果小于缓冲区可写的容量,说明TCP缓冲区已经没有就绪的字节可读,读取操作已经完成,需要退出循环。如果仍然有未读的消息则继续执行读操作。连续的读操作会阻塞排在后面的任务队列中待执行的Task,以及写操作,所以,要对连续读操作做上限控制,默认值为16次,无论TCP缓冲区有多少码流需要读取,只要连续16次没有读完,都需要强制退出,等待下次 selector轮询周期再执行。如图16-87所示。

完成多路复用器本轮读操作之后,触发 ChannelReadComplete事件,随后调用接收缓冲区容量分配器的 Hanlder的记录方法,将本次读取的总字节数传入到 record方法中进行缓冲区的动态分配,为下一次读取选取更加合适的缓冲区容量,代码如图16-88所示。

上面我们提到,如果读到的返回值为-1,表明发生了IO异常,需要关闭连接,释放资源,代码如图16-89所示。

至此,请求消息的异步读取源码我们已经分析完成。
 

猜你喜欢

转载自blog.csdn.net/shenchaohao12321/article/details/89298302