目录
Netty是什么就不过多介绍了,源码分析的代码来自于netty源码包下的案例。
今天分析的代码就是上面这一段,现在我们就开始分析服务端启动的源码流程。(在看源码之前希望大家能够了解Nio、三种Reactor模型等前置知识,这些可以帮助你进行源码理解。源码这个东西自己看的越多就越容易理解,大家坚持下去就一定能掌握)
1.创建EventLoopGroup
1.1 关于线程数的设置
我们Netty服务端需要创建两个EventLoopGroup(事件循环组),分别用来监听Accept事件和其他事件,因为创建流程基本大同小异,这里以创建bossGroup()的流程为例。
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
构造器,一路的this(…)、this(…)一直到MultithreadEventLoopGroup类的构造方法时对我们参数中的线程数有了操作。
判断我们的传入的线程数是否为0(没传入默认是0),如果是0的话,那么采用常量DEFAULT_EVENT_LOOP_THREADS的值,否则就是我们自己设置的值。那么DEFAULT_EVENT_LOOP_THREADS的值是多少呢,我们看到了是在类的构造器中进行初始化的。
DEFAULT_EVENT_LOOP_THREADS的值是 1 与NettyRuntime.availableProcessors() * 2这个表达式的较大者,我们一直跟进这个方法,发现其实是通过Runtime类获取我们操作系统可用的处理器内核数,那么说明如果我们不设置线程数的话,默认是我们操作系统内核数的两倍。比如8核的cpu对应默认就是16个线程。
1.2 构造方法主体内容
上面分析完了线程数的设定,我们继续回到我们的构造方法,继续this(…)、super(…)的跟进,最终来到了一个有真正内容的方法。
将方法的主题内容复制在下面,为了节省篇幅,一些验证性的代码就不贴了。
第一部分代码
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
if (!success) {
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}
for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}
第二部分代码
chooser = chooserFactory.newChooser(children);
第三部分代码
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};
for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
我将上述代码按作用分为了三个部分,我们一个部分一个部分进行解读。
第一部分:初始化事件循环组
children = new EventExecutor[nThreads];
首先创建了一个EventExecutor的数组,EventExecutor是我们事件循环NioEventLoop的父接口,这个操作说明了我们底层用了数组存放我们的EventLoop。
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
}
然后在循环体内有这么一段代码,我们调用newChild()方法给每一个数组元素进行赋值,我们看看我们是如何创建出NioEventLoop的。
返回的是直接new的NioEventLoop对象,并且在里面对我们的任务队列,选择器等进行了初始化,这里就部继续跟下去了。
try语句里面的看完了,因为我们是循环创建的,所以创建中出现异常,我们会将之前创建的那些事件循环进行关闭,这里没什么好说的,我们看第二部分代码。
第二部分:创建选择器
chooser = chooserFactory.newChooser(children);
第二部分代码只有一句话,就是创建出我们事件循环组工作时候的选择选择器,主要想说的是这个选择器如何选择我们的EventLoop的算法算得上是奇淫巧计。
跟进newChooser()方法,发现创建的是PowerOfTwoEventExecutorChooser类型的选择器,我们看看这个选择器的next()方法。
相信有些同学看到这里就有些蒙了,这是怎么选择的,首先我们将变量 idx看作是一个int 类型的变量 index ,然后这个next实际上可以翻译为下面这条语句
return executors[++index & executors.length - 1];
那么 ++index & executors.length - 1到底有什么用呢,这就涉及到我们与操作的特殊含义了,如果两个数相与,表面上是两个数按位与,同一为1,不同为0,实际上与A & B的另一个含义是,二者中的较大者对另一个数+1取模。
假设 A >= B ,那么 A & B = A%(B + 1)。不信的同学可以验证一下 ,假设 A = 7 (111)
B = 3(011) 结果为 res = 011(3)实际上就是 7 % 4。有能力的同学可以在数学上证明一下这个结论,这里就不过多叙述了。然后我们选取下一个EventLoop的原理实际上是索引一直自增,然后对数组长度取模,类似于轮询地进行选取。
第三部分:收尾操作
第三部分主要做的就是给EventLoop加上关闭监听,创建只读集合等等,没什么好说的,不是核心源码,大家有兴趣的可以看一下。
2.创建ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(new EchoServerHandler());
}
});
第一行是一个空参构造器没什么好说的我们就不看了,大家可以自行点进去看看。其实这部分内容也很少,主要就是给我们的bossGroup和workerGroup进行一些参数的设置,需要注意的一点是workerGroup的主体内容大都保存在ServerBootstrap本类的属性中,bossGroup的主体内容都保存在ServerBootstrap父类AbstractBootstrap的属性中。
3.服务器启动
ChannelFuture f = b.bind(PORT).sync();
重头戏来了,bind()方法中的内容决定了我们的服务器是如何启动起来的。sync()方法只是同步等待创建结果的而已,我们跟进bind()方法。
验证一下EventLoop等组件的正确性后,我们进入doBind()方法。这部分代码核心的有两个initAndRegister()和doBind0(),我们接下来对他们进行展开讲解。
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
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) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
3.1 initAndRegister()
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
// channel can be null if newChannel crashed (eg SocketException("too many open files"))
channel.unsafe().closeForcibly();
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
}
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
跟进initAndRegister()方法,我们发现主要做了三件事情
- channel = channelFactory.newChannel();
- init(channel);
- ChannelFuture regFuture = config().group().register(channel);
我们一个一个来看
1.channel = channelFactory.newChannel();
跟进方法内部发现是通过反射创建clazz对象实例,clazz实际上是在我们第二步中channel方法进行指定的。newChannel()方法的作用就是就是创建出我们的NioServerSocketChannel实例对象。
2.init(channel);
void init(Channel channel) throws Exception {
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()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
这个方法前大半部分的操作都是将我们给ServerBootStrap()注册的那些属性(options、attrs等等)赋值给我们的ServerSocketChannel,但是最后部分有一段代码很重要。
该部分的initChannel在我们的initChannel连接初始化时会执行,往我们的pipeline管道里加入了一个ServerBootstrapAcceptor,这个类是我们bossGroup实现监听到accpect事件,获取对应的socketChannel然后注册在workerGroup上的主要实现类。我们可以先看一看这个类的对应代码
在 channelRead方法中,将我们传入的channel注册到了childGroup(workerGroup)中,这部分具体执行流程在我们下一篇服务器接收请求的源码分析中会详细分析,这里就不细说了。
综上init()方法除了对一些TCP参数的设置之外,还将我们的ServerBootstrapAcceptor加入到了管道之中。
3.config().group().register(channel);
这条语句前两个方法返回值实际上是我们的BossGroup,然后调用了对应的register方法,我们跟进register()方法中去。
调用了我们的next()的register,选择一个EventLoop进行注册,因为我们的bossGroup一般只有一个EventLoop,所以这里不用太过在意,继续跟进方法。
最终调到了这个方法,我们直接跟进第二行的register()方法
来到了下面的方法。
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();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}
前三个判断都是一些验证性的操作,我们无需在意,我们直接看下面这段代码
eventLoop.inEventLoop()的方法实际上是判断当前的eventLoop绑定的线程是不是正在执行的线程
注意我们的bind()方法是主方法调用的,所以肯定不是我们bossGroup绑定的线程。我们进入else的判断。
这段代码很重要,很重要
我们跟进eventLoop的execute()方法
依旧是判断当前线程是不是绑定的线程,**不是的话就startThread();!startThread();!startThread();!**重要的事情说三遍,这就是我们服务器线程的开启代码,我们跟进去看看。
防止多线程同时操作的问题,使用CAS算法控制同步,调用了doStartThread();方法,继续跟进去。
executor相当于线程池,对应的excute方法完成的操作就是开启线程。
然后在run方法内调用了我们的 **SingleThreadEventExecutor.this.run();**这是我们的eventLoop的核心代码,一个死循环,不断执行以下三步
- selector.select() 监听channel事件
- processSelectedKeys(); 处理事件
- runAllTasks(); 执行任务队列中的任务
这部分内容属于我们下一篇服务器接收客户端请求的核心分析代码,这里就不反客为主了,大家在当前流程需要知道的就是,服务器在register()代码中开启了我们的线程,并将我们的注册任务放到了任务队列中。至此我们的initAndRegister()方法就分析完毕了。
3.2 doBind0()
上一步我们的服务器线程启动完毕之后,下一步需要做的就是端口绑定了。
如果我们的异步执行已经做完了,我们就直接调用doBind0(),否则我们就将doBind0()加入到任务队列中去。
doBind0实际上就是给我们所有的channel绑定好端口地址,然后注册关闭的监听事件。
至此我们的doBInd方法就执行完毕了,我们的服务器也真正的启动起来了。
后续应该还会更新三篇netty的源码,应该分别是服务器接收请求源码分析、handler调用链源码分析和netty心跳源码分析。(应该会更新吧、、、、如果我不是太懒的话,不过写博客真的很耗时间。。。。。尤其是分析源码的博客,动不动就三四个小时,已经分析的很简略了,但是还是需要花很多时间,希望能对大家有帮助。。)