上文深入研究netty框架之Channel和Unsafe主要介绍了Channel和Unsafe功能,本文主要结合源码分析Netty Channel的nio实现。通过从客户端的connect开始,并围绕客户端NioSocketChannel,详细分析Channel的初始化和注册过程。其中涉及到Channel的源码将会做深入分析,涉及Netty线程模型的部分源码不会做深入探究,这部分内容会在后续文章中体现。
客户端connect
创建Netty客户端时,需首先创建引导类Bootstrap的实例,并配置EventLoopGroup实例、Channel类型、TCP参数、初始化handler实例等等,最后调用Bootstrap的connect方法开始连接服务端,连接过程中将会执行很多初始化操作,如创建Channel实例,Channel的初始化和注册等等。下面先看看常见客户端实现的代码:
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect(HOST, PORT).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
由这段代码即可知,Bootstrap采用了典型的Builder模式构造对象,首先创建一个空实例,然后调用方法设置Bootstrap的必要属性。需注意,创建Bootstrap空实例后,必须设置以下属性:
- 必须调用group方法设置EventLoopGroup实例group,用于后期注册Channel到EventLoop;
- 调用channel方法设置Channel类型,该方法会初始化ChannelFactory实例,用于后期创建Channel实例;
- 调用handler设置ChannelInitializer实例,用于添加自定义Handler。
下面从connect方法开始分析:
public ChannelFuture connect(SocketAddress remoteAddress) {
if (remoteAddress == null) {
throw new NullPointerException("remoteAddress");
}
validate();
return doResolveAndConnect(remoteAddress, config.localAddress());
}
读者可查阅validate源码实现,validate会校验Bootstrap中设置的几个必要属性,若validate校验通过,调用doResolveAndConnect进行解析并创建连接,下面看实现:
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.isDone()) {
if (!regFuture.isSuccess()) {
return regFuture;
}
return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
//future.cause()会读取result对象,该对象被声明为volatile,这里读出来缓存在局部变量中,
//主要是为了防止重复读取volatile对象导致性能损失
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
}
}
});
return promise;
}
}
doResolveAndConnect主要分为以下几步:
- doResolveAndConnect首先调用initAndRegister完成Channel的初始化和注册;
- 若ChannelFuture的isDone返回true,且isSuccess返回true,调用doResolveAndConnect0执行实际的地址解析和连接操作;isSuccess返回false则直接返回regFuture;
- 若ChannelFuture的isDone返回false,表明注册过程未完成,创建promise,并在regFuture上添加监听器ChannelFutureListener,待完成后回调。若注册过程中无异常抛出,说明initAndRegister执行成功,在回调中调用doResolveAndConnect0。
注意点:
当ChannelFuture的isDone返回false时,表示Channel的初始化与注册过程还未完成,不能直接调用doResolveAndConnect0执行地址解析和连接操作,需要等待Channel初始化和注册完成;这时进入else代码块,else中创建了PendingRegistrationPromise的实例,同时在regFuture中添加了监听器用于完成后回调。那么为什么需要创建一个PendingRegistrationPromise的实例呢?作用是什么?鉴于篇幅,关于该问题读者可移步Netty源码之ChannelPromise回调通知问题,其中有详细的解答。
无论执行if还是else,都会创建一个新的Promise实例用于建立连接后通知客户端,if中使用channel.newPromise()创建DefaultChannelPromise的实例,由于这里调用DefaultChannelPromise的构造方法没有传入EventExecutor实例,因此默认会使用I/O线程作为executor。
Chanenl初始化与注册
先看initAndRegister源码:
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
channel.unsafe().closeForcibly();
}
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
/** 如果程序到这里,说明promise没有失败,可能发生以下情况之一
* 1) 如果尝试将Channel注册到EventLoop,且此时注册已经完成;inEventLoop返回true,channel已经成功注
* 册,可以安全调用bind() or connect()
* 2) 如果尝试注册到另一个线程,即inEventLoop返回false,则此时register请求已成功添加到事件循环的任务队
* 列中,现在同样可以尝试bind()或connect(),因为bind()或connect()会被调度在执行register
* Task之后执行, 因为register(),bind()和connect()都被绑定到同一个I/O线程。
*/
return regFuture;
}
- 首先使用工厂类ChannelFactory的newChannel通过反射创建Channel实例;
- 调用init方法执行初始化操作;
- 若在创建实例和初始化期间抛出异常,创建DefaultChannelPromise实例,写入异常并返回;
- 调用EventLoopGroup的register方法,完成注册操作
- regFuture返回失败,则需要关闭channel。
注意点:
- 调用newChannel和init抛出异常时,需要判断channel是否为空,因为当调用newChannel崩溃时,channel可能为null,主要原因可能是打开文件描述符fd过多,如抛出异常SocketException("too many open files");
- 调用newChannel和init抛出异常时,由于Channel还未注册到特定的EventLoop,因此创建DefaultChannelPromise实例时,需要强制使用GlobalEventExecutor作为promise的执行器。GlobalEventExecutor是一种单线程单例执行器,它会自动启动,并且当任务队列中没有任务时挂起1秒钟后停止,该执行者无法调度大量任务,更多细节这里不再展开。
下面分步介绍initAndRegister:
创建Channel实例
主要通过反射的手段,使用ChannelFactory的newChannel调用指定Channel的默认构造方法创建Channel实例,由于客户端Bootstrap实例调用channel方法时传入的Channel类型是NioSocketChannel,因此这里将调用NioSocketChannel的默认构造方法,先看看构造方法调用链路:
图中仅表述各构造方法的调用顺序,执行顺序正好相反,最先从AbstractChannel构造方法开始执行。构造方法调用顺序说明:
- 首先,默认构造方法中传入默认的SelectorProvider实例调用带provider参数的构造方法;
- NioSocketChannel(SelectorProvider provider)会调用newSocket来打开一个新的Java NIO SocketChannel,底层依赖java NIO的实现,然后使用SocketChannel实例作为参数调用带SocketChannel参数的构造方法;
- NioSocketChannel(SocketChannel socket)中使用null作为当前Channe的parent,调用带parent和SocketChannel参数的构造方法;
- NioSocketChannel(Channel parent, SocketChannel socket)构造方法首先会调用父类构造方法AbstractNioByteChannel(Channel parent, SelectableChannel ch)->AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)->AbstractChannel(Channel parent)。
最终调用的最上层构造器是AbstractChannel,因此创建Channel对象由此开始:
- AbstractChannel(Channel parent)首先初始化Channel的parent,id属性;然后创建unsafe实例,并初始化Channel的unsafe属性,这里newUnsafe的创建的是NioSocketChannelUnsafe的实例,主要用于后续执行I/O相关的操作;最后创建每个Channel关联的pipeline实例;
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
- 然后进入AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)构造方法。AbstractNioChannel中持有SelectableChannel的实例,主要用于将Netty中NioSocketChannel相关操作转发给Java NIO中SocketChannel相关的操作,因此这里将NioSocketChannel构造方法中创建的SocketChannel实例赋值给ch。SelectableChannel是java实现NIO的默认Channel类型,它一个可通过选择器复用的Channel,由于SocketChannel同时继承自SelectableChannel,因此SocketChannel的实例即是SelectableChannel的实例。此外,当前构造方法中还会设置当前Channel感兴趣的事件类型readInterestOp, 同时配置channel为非阻塞模式。Java NIO实现中,必须配置Channel为非阻塞模式。
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
if (logger.isWarnEnabled()) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
- 最后进入AbstractNioByteChannel(Channel parent, SelectableChannel ch)的构造方法中,传入的readInterestOp为SelectionKey.OP_READ,表示选择器需要关注Channel的可读事件。最后,进入NioSocketChannel构造方法中。
- super返回后,最后在NioSocketChannel(Channel parent, SocketChannel socket)中初始化Channel的config实例,到此Channel创建完成。
Channel初始化
创建为Channel实例后,将调用Bootstrap的init方法初始化Channel,下面看实现:
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
p.addLast(config.handler());
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
}
}
由init实现可知,首先会添加ChannelInitializer实例到pipeline中,这里由于Channel还未注册完成,因此不会真正添加到pipeline,需等待Channel注册完成后通知,ChannelInitializer的添加过程可阅读文章Netty源码之ChannelPipeline和ChannelHandlerContext;然后配置Channel的可选参数,options0()方法会返回通过Bootstrap的option方法配置的参数,主要包含一些TCP相关参数,这些参数将通过Channel的Config实例配置到底层Socket上;最后,添加Channel的可选attr属性,attrs0方法会返回通过Bootstrap的attr方法配置的参数,其中key的类型为AttributeKey。由于Channel支持并发访问,因此配置Channel属性时使用synchronized保证线程安全。
Channel注册
创建Channel实例和初始化完成之后,需要调用register注册Channel。过程如下:
首先调用group返回初始化Boostrap创建的NioEventLoopGroup实例,调用它的register方法,实现在父类MultithreadEventLoopGroup中,看源码:
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next()用于从MultithreadEventLoopGroup中选择一个单线程的NioEventLoop实例,并调用它的register方法,(这里不对next做深入分析,后续在Netty线程模型中介绍),进入SingleThreadEventLoop register源码:
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}
由源码可知,最终调用了unsafe中的register方法,默认实现参考AbstractUnsafe源码:
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
}
if (isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
return;
}
if (!isCompatible(eventLoop)) {
promise.setFailure(
new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
return;
}
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();//最终调用NioSocketChannel中的doClose方法关闭channel
closeFuture.setClosed();//表明Channel已被关闭
safeSetFailure(promise, t);
}
}
}
- 首先,参数校验,检查eventLoop是否为空、当前Channel是否已经注册过、当前eventLoop是否为NioEventLoop的实例。不同I/O类型使用不同的EventLoop,如Nio使用NioEventLoop,Oio使用ThreadPerChannelEventLoop;
- 然后将eventLoop绑定到当前Channel,以后Channel相关的操作都将由该eventLoop完成;
- 最后在eventLoop中执行register0,如果当前线程不是eventLoop对应的线程,则将register0封装在Runnable任务中,提交到NioEventLoop任务队列中交由eventLoop对应的线程执行;调用register0需传入promise实例,用于操作成功后通知结果。
- 若提交register0任务失败,说明eventLoop不接受当前Channel的注册任务,此时需要强制关闭当前Channel,并写入结果到promise。
下面继续看register0的实现:
private void register0(ChannelPromise promise) {
try {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;
pipeline.invokeHandlerAddedIfNeeded();
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
} catch (Throwable t) {
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
下面分析register0的整个过程:
- 首先调用setUncancellable将当前promise设置为不可取消,或者promise已经成功完成而不能被取消,两种情况都将返回true,否则,promise已经被取消,退出register0;setUncancellable返回true则调用ensureOpen确保Channel仍然打开,如果Channel已经关闭,退出register0;
- 如果promise未取消且Channel仍然打开,执行doRegister将Channel注册到EventLoop的多路复用器Selector上,该方法在AbstractNioChannel中实现;
- Channel注册到多路复用器后,调用pipeline.invokeHandlerAddedIfNeeded(),保证在Channel注册之后立刻回调handlerAdded,以保证handler被添加到pipeline上。
- 调用调用pipeline的fireChannelRegistered触发Channel成功注册事件;
- 如果Channel处于活动状态,且Channel是第一次注册,调用pipeline.fireChannelActive()触发Channel处于活动状态事件,同一个Channel只有在第一次注册的情况下才可能触发该事件;这样是为了防止多次触发Channel Active事件,如Channel注册后,调用deregister,再次调用register;
- 如果Channel处于活动状态,且设置了autoRead,则发起read操作;
- 整个过程中若抛出异常,需强制关闭Channel,防止fd泄漏。
注意点:
- 将当前Channel注册到多路复用器之前,必须确保Channel仍然打开,因为当register0进入任务队列中等待I/O线程执行时,Channel可能被关闭了;
- 调用safeSetSuccess通知promise之前需首先调用pipeline.invokeHandlerAddedIfNeeded()确保handler已经被添加到pipeline上,这是非常有必要的。假设先调用safeSetSuccess,即通知promise通道成功注册到多路复用器上,promise将调用notifyListeners通知所有监听者,若客户端已经在ChannelFutureListener中通过pipeline触发事件,用户自定义handler可能就会丢失该事件。回顾一下,context将事件提交给handler处理时,都会先调用invokeHandler()检查handler的状态,对于需要按顺序处理事件的context,只有handler的状态为ADD_COMPLETE,invokeHandler()才会将事件交给对应的handler处理,否则,只负责传递事件。而只有当Channel注册完成之后,handler的状态才会被设置为ADD_COMPLETE。因此需要首先调用invokeHandlerAddedIfNeeded确保ChannelInitializer的实例被添加到pipeline中,然后将自定义的handler添加到pipeline,这样可保证handler不会丢失事件。
- 每个Channel只会触发一次Channel活动事件,因此这里通过firstRegistration标志位来确保只有Channel第一次注册时才调用fireChannelActive触发事件,防止Channel注册后,调用deregister之后再次调用register,从而多次触发该事件。
下面看看doRegister将Channel注册到多路复用器的实现:
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
eventLoop().selectNow();
selected = true;
} else {
throw e;
}
}
}
}
javaChannel用于返回调用AbstractNioChannel构造方法初始化的SelectableChannel 实例,eventLoop().unwrappedSelector()将返回构造NioEventLoop实例时绑定的Selector对象,这样通过SelectableChannel的register方法将当前Channel注册到EventLoop的多路选择器上。
AbstractNioChannel注册Selector的事件类型为0,表示对任何事件都不感兴趣,仅仅是为了完成注册操作。调用SelectableChannel 的register方法时,将AbstractNioChannel的子类实例(this)作为附加对象传入,这样使得后续Channel接收到网络事件通知时可以从SelectionKey中重新获取之前的附加对象进行处理,即这里可通过SelectionKey可以从多路复用器中获取Channel对象。
如果当前注册返回的SelectionKey已经被取消,则抛出CancelledKeyException 异常;如果是第一次处理该异常,调用多路复用器的selectNow方法将已经取消的SelectionKey从多路复用器删除;操作成功后,selected设为true,说明失效的SelectionKey已经删除;继续进行下一次注册,如果仍然抛出CancelledKeyException异常,说明无法删除已经注册的SelectionKey,按照JDK官方说明,不应该出现这种意外。如果发生这种问题,直接抛出异常到上层处理,说明NIO相关类库可能存在不可恢复的BUG。
到此Channel的初始化和注册操作完成。
欢迎指出本文有误的地方,转载请注明原文出处https://my.oschina.net/7001/blog/1421893