导读
原创文章,转载请注明出处。
本文源码地址:netty-source-code-analysis
本文所使用的netty版本4.1.6.Final:带注释的netty源码
我们在“BIO vs NIO”这篇文章中我们给出了使用jdk原生nio编写的服务端Hello World。还记得其中的关键步骤吗,咱们再来温习一下。
-
创建一个ServerSocketChannel
-
将ServerSocketChannel设置为非阻塞的
-
将ServerSocketChannel绑定到8000端口
-
将ServerSocketChannel注册到selector上
今天我们就以这几个关键步骤为目标来看一下在netty中是怎么做的,以及在这几个步骤的中间netty又多做了哪些工作。
1 服务端引导代码
以下代码引导启动一个服务端,在以下文章中我们以“引导代码”指代这段程序。这段代码很简单,创建两个EventLoopGroup分别为bossGroup和workerGroup。创建一个ServerBootstrap并将bossGroup和workerGroup传入,配置一个handler,该handler为监听端口这条连接所使用的handler。接着又设置了一个childHandler即新连接所使用的handler,本篇文章我们不讲新连接的接入,所以这里的childHandler里什么也没做。
运行这段这段代码将在控制台打出如下结果。
HandlerAdded
ChannelRegistered
ChannelActive
/**
* 欢迎关注公众号“种代码“,获取博主微信深入交流
*
* @author wangjianxin
*/
public class com.zhongdaima.netty.analysis.bootstrap.ServerBoot {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.attr(AttributeKey.valueOf("ChannelName"), "ServerChannel")
.handler(new ChannelInboundHandlerAdapter() {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("ChannelRegistered");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("ChannelActive");
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println("HandlerAdded");
}
}).childHandler(new ChannelInboundHandlerAdapter(){
});
ChannelFuture f = b.bind(8000).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
2 启动过程
我们从ChannelFuture f = b.bind(8000).sync()的bind方法往下跟到AbstractBootStrap的doBind方法,这中间的过程很简单,就是将端口号封装为SocketAddress。
在doBind内的关键代码有第一行的initAddRegister方法,还有后面的doBind0方法。
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
if (regFuture.isDone()) {
doBind0(regFuture, channel, localAddress, promise);
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
} else {
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
进入到initAndRegister方法,initAddResgiter方法中有3个关键步骤,1是channelFactory.newChannel(),2是init(channel),3是config().group().register(channel)。
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//创建一个Channel
channel = channelFactory.newChannel();
//初始化Channel
init(channel);
} catch (Throwable t) {
}
//注册Channel
ChannelFuture regFuture = config().group().register(channel);
...
}
整个doBind方法被分成4个关键步骤,分别是:
channelFacotry.newChannel()init(channel)config().group().register(channel)doBind0
接下来咱们分别来看这4个关键步骤。
2.1 新创建一个Channel
channelFacotry是AbstractBootStrap的一个属性,这个属性在哪里被赋值呢,其实是在我们在启动时调用b.channel(NioServerSocketChannel)时赋的值,这个方法在AbstractBootStrap里,非常简单,我们不再分析。最后的结果是channelFactory被赋值为ReflectiveChannelFactory,顾名思义就是用反射的方法创建Channel,我看们一下其中的newChannel()方法,很简单,clazz.newInstance调用无参构造方法创建实例。
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Class<? extends T> clazz;
@Override
public T newChannel() {
try {
return clazz.newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
}
接下来咱们就看一下NioServerSocketChannel的无参构造方法,其中调用newSocket方法创建了一个jdk的ServerSocketChannel。好了,咱们已经看到了导读中提到的第1步“创建一个ServerSocketChannel”,紧着把这个channel传递给了父类的构造方法,还传递一个参数SelectionKey.OP_ACCEPT,记住这个参数后面会提到。
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
private static ServerSocketChannel newSocket(SelectorProvider provider) {
try {
return provider.openServerSocketChannel();
} catch (IOException e) {
}
}
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
咱们接着跟到父类AbstractNioMessageChannel的构造方法,没什么其他操作,继续调用父类的构造方法。
protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent, ch, readInterestOp);
}
接着跟下去,到了AbstractNioChannel的构造方法,在这里我们看到了ch.configureBlocking(false),至此我们看到了导读中提到的第2步“将Channel设置为非阻塞的”。AbstractNioChannel里又调用了父类的构造方法,接着看下去。
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
//将Channel设置为非阻塞的
ch.configureBlocking(false);
} catch (IOException e) {
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
到了AbstractChannel的构造方法,这里为Channel创建了一个id,一个Unsafe还有一个PipeLine。Unsafe和PipeLine咱们后面再讲。
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
2.2 初始化Channel
我们回到AbstractBootstrap的initAndRegister方法,接着往下看init(channel),这是个抽象方法,实现在ServerBootstrap里。
init方法的主要逻辑是设置Channel参数、属性,并将我们在引导代码中所配置的Handler添加进去,最后又添加了一个ServerBootStrapAccptor,顾名思义这是一个处理新连接接入的Handler。
这个ServerBootStrapAccptor在随后的章节中我们会讲,这里先略过。至于为什么调用ch.eventLoop().execute而不是直接添加,这个我在代码里有简要提示,其实目前的版本,直接添加也是没有问题的。这个我会在出视频教程的时候给大家演示一下,欢迎关注。
void init(Channel channel) throws Exception {
//设置Channel参数,我们在引导代码中通过.option(ChannelOption.TCP_NODELAY, true)所设置的参数
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
channel.config().setOptions(options);
}
//设置Channel属性,我们在引导代码中通过.attr(AttributeKey.valueOf("ChannelName"), "ServerChannel")所设置的属性
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
ChannelPipeline p = channel.pipeline();
//p.addLast是同步调用,不管是不是EventLoop线程在执行,这个匿名的ChannelInitializer被立即添加进PipeLine中
//但是这个匿名的ChannelInitializer的initChannel方法是被channelAdded方法调用的,而channelAdded方法只能被EventLoop线程调用
//此时这个Channel还没绑定EventLoop线程,所以这个匿名的ChannelInitializer的channelAdded方法的调用会被封装成异步任务添加到PipeLine的pendingHandlerCallback链表中
//当Channel绑定EventLoop以后会从pendingHandlerCallback链表中取出任务执行。
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
//添加我们在引导代码中所配置的handler
pipeline.addLast(handler);
}
//有些同学对这个有疑问,为什么不直接pipeline.addLast,可以参考下面的issue,其实现在的版本已经可以直接改成pipeline.addLast
//issue链接https://github.com/netty/netty/issues/5566
//为什么现在的版本可以直接改成pipeline.adLast呢,关键在于ChannelInitializer的handlerAdded方法
//大家可以对比4.0.39.Final版本和4.1.6.Final版本的区别
//添加ServerBootStrapAcceptor
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
2.3 绑定EventLoop并向Selector注册Channel
我们回到AbstractBootstrap的initAndRegister方法,接着往下看到ChannelFuture regFuture = config().group().register(channel);,这里就是注册Channel的地方了,咱们跟进去看看。
config.group()的返回是我们在引导代码中所设置的bossGroup,由于这里只有一个Channel,所以bossEventLoopGroup里面只需要1个EventLoop就够了。
跟到register(channel)方法里看看,这个register方法是抽象的,具体实现在MultithreadEventLoopGroup中,跟进去。
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next()方法调用EventExecutorChooser的next()方法选择一个EventLoop。EventExecutorChooser有两个实现,分别是PowerOfTowEventExecutorChooser和GenericEventExecutorChooser,这两个Chooser用的都是轮询策略,只是轮询算法不一样。如果EventLoopGroup内的EventLoop个数是2的幂,则用PowerOfTowEventExecutorChooser,否则用GenericEventExecutorChooser。
PowerOfTowEventExecutorChooser使用位操作。
@Override
public EventExecutor next() {
return executors[idx.getAndIncrement() & executors.length - 1];
}
而GenericEventExecutorChooser使用取余操作。
@Override
public EventExecutor next() {
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
从EventLoop的选择算法上我们可以看出,netty为了性能,无所不用其极。
chooser属性的赋值在MultithreadEventExecutorGroup的构造方法内通过chooserFactory创建的。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
chooser = chooserFactory.newChooser(children);
}
而chooserFactory的赋值在MultithreadEventExecutorGroup的另一个构造方法内。当我们在引导代码中通过new NioEventLoopGroup(1)创建EventLoopGroup时最终会调用到这个构造方法内,默认值为DefaultEventExecutorChooserFactory.INSTANCE。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}
next()方法选出的EventLoop就是个SingleThreadEventLoop了,我们跟到SingleThreadEventLoop的register方法,最终调用的是unsafe的register方法。
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
@Override
public ChannelFuture register(final ChannelPromise promise) {
ObjectUtil.checkNotNull(promise, "promise");
promise.channel().unsafe().register(this, promise);
return promise;
}
unsafe.register方法在io.netty.channel.AbstractChannel.AbstractUnsafe内,我们跟下去看看。在register方法中最主要的有两件事,一是绑定eventloop,二是调用register0方法。此时的调用线程不是EventLoop线程,会发起一个异步任务。
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
//绑定eventloop
AbstractChannel.this.eventLoop = eventLoop;
if (eventLoop.inEventLoop()) {
register0(promise);
//此时我们不在EventLoop内,也就是当前线程非EventLoop线程,会走到这个分支
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
//调用子类的register0方法
register0(promise);
}
});
} catch (Throwable t) {
}
}
}
register0方法内主要有3步操作。
第1步是doRegister(),这个咱们稍后说。
第2步是pipeline.invokeHandlerAddedIfNeeded()这一步是去完成那些在绑定EventLoop之前触发的添加handler操作,比如我们添加了一个ChannelInitializer,在ChannelInitalizer的initChannel方法中添加的Handler,而initChannel被channelAdded方法调用,channelAdded方法的调用必须在EventLoop内,未绑定EventLoop之前这个调用会被封装成异步任务。
这些操作被放在pipeline中的pendingHandlerCallbackHead中,是个双向链表,具体请参考DefaultChannelPipeLine的addLast(EventExecutorGroup group, String name, ChannelHandler handler)方法。
这一步调用了咱们的引导程序中的 System.out.println("HandlerAdded"),在控制台打出"HandlerAdded"。
第3步触发ChannelRegistered事件。这一步调用了咱们的引导程序中的 System.out.println("ChannelRegistered"),在控制台打出"ChannelRegistered"。
好了,到这里我们已经知道了,为什么我们的引导程会先打出"HandlerAdded"和"ChannelRegistered"。
接着往下isActive()最终调用是的jdk ServerSocket类的isBound方法,咱们不再贴出代码,读者自行查看,很简单,显然这里我们还没有完成端口绑定,所以这个if分支的代码并不会执行。
private void register0(ChannelPromise promise) {
try {
//向Selector注册Channel
doRegister();
//去完成那些在绑定EventLoop之前触发的添加handler操作,这些操作被放在pipeline中的pendingHandlerCallbackHead中,是个链表,具体请参考`DefaultChannelPipeLine`的`addLast(EventExecutorGroup group, String name, ChannelHandler handler)`方法。
pipeline.invokeHandlerAddedIfNeeded();
//将promise设置为成功的
safeSetSuccess(promise);
//触发ChannelRegistered事件
pipeline.fireChannelRegistered();
//这里并没有Active,因为此时还没完成端口绑定,所以这个if分支的代码都不会执行
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
} catch (Throwable t) {
}
}
接下来咱们跟进去doRegister方法,这是个抽象方法,本例中方法实现在AbstractNioChannel中。好了,到这里我们终于看到了导读中提到的第4步“向Selector注册Channel”的操作。
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
return;
} catch (CancelledKeyException e) {
}
}
}
到了这里,我们在导读中说的总共4步操作中,还有第3步没有看到,在哪里呢,接着往下看。
2.4 绑定端口号
前文中我们说过doBind方法内有两个重要调用initAndRegister和doBind0,initAndRegister我们已经分析完了,接下来看doBind0。由于initAndRegister中register是异步操作,当initAndRegister返回时,register操作有可能完成了,也有可能没完成,这里做了判断,如果已经完成则直接调用doBind0,如果未完成,则将doBind0放到regFuture的Listener中,等register操作完成后,由EventLoop线程来回调。
那么什么时候会回调Listener呢,当调用promise的setSuccess或者setFailure时回调。还记得上文中的AbstractUnsafe.register0方法吗,其中有一个调用safeSetSuccess(promise),对,就是这里了,很简单,我们不再赘述。
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
} else {
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
那么又有读者疑问了,在这个if判断完成之后到添加Listener之间的这个时间,promise有可能已经完成了,Listener可能不会回调了, 奥秘在DefaultPromise的addListener(GenericFutureListener<? extends Future<? super V>> listener)方法里,这里注册完Listener之后,如果发现promise已经完成了,那么将直接调用nofityListeners方法向EventLoop提交异步任务(此时已经完成绑定EventLoop),该异步任务即是回调刚刚注册的Listener。
@Override
public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
synchronized (this) {
addListener0(listener);
}
if (isDone()) {
notifyListeners();
}
return this;
}
咱们回归正题,去看doBind0方法,这里调用了channel.bind方法,具体实现在AbstractChannel里。
private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}
AbstractChannel里的bind方法调用了pipeline.bind,还记得一篇“Netty整体架构”文章中的那张图吗,咱们再次放出来。
bind方法会首先调用Tail的bind方法,最终传播到Head的bind方法,具体怎么传播的,咱们讲PipeLine的时候再说。
@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return pipeline.bind(localAddress, promise);
}
这里咱们直接跟到HeadContext的bind方法, 我们看到又调用了unsafe的bind方法,前面我们看到Channel在向Selector注册时最终也调用到了unsafe。这里先跟大家说一下unsafe是netty中最直接跟Channel接触的类,对Channel的所有操作最终都会落到unsafe上,具体详情咱们后面讲unsafe的时候再说。
@Override
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
unsafe.bind(localAddress, promise);
}
具体实现在AbstractUnsafe中,bind方法中两个重要操作,一是调用doBind方法绑定端口,这个稍后说。二是触发ChannelActive事件,这一步有一个isActive判断,到这里我们已经完成了端口绑定,所以是true。这一步调用了咱们引导程序中的System.out.println("ChannelActive")在控制台打印出"ChannelActive"。
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
boolean wasActive = isActive();
try {
doBind(localAddress);
} catch (Throwable t) {
}
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
}
doBind方法的实现在NioServerSocketChannel中,我们一起来看一下,至此导读中提到的第3步操作“绑定端口”,我们已经看到了,服务端启动完成。
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
2.4.1 注册兴趣事件
但是似乎是不是少了点什么,我们在使用jdk api编写的时候,向selector注册的时候,传递了兴趣事件的,为什么我们没有看到这里有兴趣事件的注册呢。我们继续回到AbstractUnsafe的bind方法中,最后调用了pipeline.fireChannelActive(),下面是PipeLine的fireChannleActive方法,调用了AbstractChannelHandlerContext.invokeChannelActive(head),而这个head就是我们的“netty整体架构图”中的HeadContext。
@Override
public final ChannelPipeline fireChannelActive() {
AbstractChannelHandlerContext.invokeChannelActive(head);
return this;
}
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelActive();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelActive();
}
});
}
}
HeadContext中的channelActive方法如下,奥秘在readIfIsAutoRead里,readIfIsAutoRead,最终调用了channel.read。
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}
channel.read方法的实现在AbstractChannel中,调用到了pipeline.read。
@Override
public Channel read() {
pipeline.read();
return this;
}
PipeLine中的read方法如下,调用了tail的read方法,最终这个调用会传播到head的read方法,具体的传播过程,等咱们讲PipeLine的时候再说。咱们直接去看HeadContext的read方法。
@Override
public final ChannelPipeline read() {
tail.read();
return this;
}
HeadContext的read方法又调用到unsafe.beginRead()。
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead();
}
beginRead方法的实现在AbstractUnsafe中,这里调用了doBeginRead。doBeginRead方法的实现在AbstractNioChannel中。
@Override
public final void beginRead() {
try {
doBeginRead();
} catch (final Exception e) {
}
}
doBeginRead方法的实现在AbstractNioChannel中,这里修改了selectionKey的兴趣事件,把已有的兴趣事件interestOps和readInterestOp合并在一起重新设置。
interestOps是现有的兴趣事件,在上文中向Selector注册时的代码里javaChannel().register(eventLoop().selector, 0, this),所以interestOps就是0。
readInterestOp在哪里设置的呢,还记得本篇文章中新创建一个Channel那一小节中吗?
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
在NioServerSocketChannel调用父类的构造方法时传递了一个兴趣事件参数,值为SelectionKey.OP_ACCEPT,至此,真相大白。
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
九曲连环,我们终于找到了这么小小的一个点,为什么流程这么长呢,似乎很难理解,不要紧,继续关注我的文章,咱们讲PipeLine的时候会把这里讲明白。
3 总结
netty服务端启动流程:
-
创建一个
Channel实例,这个过程中将Channel设置为非阻塞的,为Channel创建了PipeLine和Unsafe。 -
初始化
Channel,为Channel设置参数和属性,并添加ServerBootstrapAcceptor这个特殊的Handler。 -
注册
Channel,为Channel绑定一个EventLoop并向Selector注册Channel。 -
绑定端口。
关于作者
王建新,转转架构部资深Java工程师,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。
原创文章,码字不易,别忘了点赞。