作者: LemonNan
原文地址: https://juejin.im/post/6890768117803253768/
距离上一次写 Netty 源码解析已经过去了一年多时间了, 最近这段时间回顾 Netty 的源码,目前看的版本是 4.1.31, 跟最开始看的版本不太一样, 没记错应该是4.1.16(吧), 不过这并不妨碍我们阅读源码, 小版本的源码在主流程上的差别不会太大. 这次分析的源码是用 Netty 启动一个服务端的过程.
从 Netty 官方给的 demo 中入手, 下面的代码在 Netty项目中的 example 模块中的 io.netty.example.echo
包中.
// Configure the server.
// 线程池初始化
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
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();
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// Start the server.
// 等待启动完成
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
// 等待关闭
f.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
这是启动一个简单的输入输出的服务端, 本来代码中还有个 SSL的配置, 在这里去掉了.
在对上面这段服务端启动代码进行解析前, 我们来回想一下用NIO写服务端的代码逻辑.
NIO流程
// 一共下来就是这几步
// 1.创建
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 2.绑定
serverSocketChannel.bind(new InetSocketAddress(port));
// 3.注册事件
this.selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
总的来说NIO的流程就是下面三步:
在清楚以前用NIO写的流程之后, 对 Netty 给的 echo demo
进行分析.
这里在 ChannelFuture f = b.bind(PORT).sync()
之前的代码, 是简单的用链式对服务端一些参数进行初始化, 等到 bind
的时候, 才开始做启动处理.
接下来分析 bind
方法.
首先bind 的时候, 传递了一个端口号进去, 这个大家应该比较熟悉了.
public ChannelFuture bind(int inetPort) {
return bind(new InetSocketAddress(inetPort));
}
接着继续调用
public ChannelFuture bind(SocketAddress localAddress) {
// 对一些初始化参数的校验
validate();
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
return doBind(localAddress);
}
参数校验方法 validate()
public B validate() {
if (group == null) {
throw new IllegalStateException("group not set");
}
if (channelFactory == null) {
throw new IllegalStateException("channel or channelFactory not set");
}
return self();
}
首先是对 boss 线程池的校验, 然后校验 channel工厂的初始化, 这里分别对应的是初始化代码中的 b.group(bossGroup, workerGroup)
和 What is it ?
继续看了下, 它是一个 Channel 的工厂, 应该就是创建 Channel 的, 在初始化参数的时候, 也有对 Channel 信息进行设置的代码 channel(NioServerSocketChannel.class)
channel(Class) 的代码
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
最后是调用下面这个方法
public B channelFactory(ChannelFactory<? extends C> channelFactory) {
if (channelFactory == null) {
throw new NullPointerException("channelFactory");
}
if (this.channelFactory != null) {
throw new IllegalStateException("channelFactory set already");
}
this.channelFactory = channelFactory;
return self();
}
在这里可以看出, 这个channelFactory 是一个 ReflectiveChannelFactory
.
接下来就是bind中调用的 doBind(localAddress)
方法
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
// ... 下面一堆操作后调用 doBind0(), 这里用 doBind0 来简化一下
doBind0(regFuture, channel, localAddress, promise);
}
这里为了代码容易看, 省略了中间的一些逻辑代码处理.
首先从名字上看, 首先是 初始化和注册 initAndRegister()
, 判断是否异常, 接着调用 doBind0
.
final ChannelFuture initAndRegister() {
channel = channelFactory.newChannel();
init(channel);
ChannelFuture regFuture = config().group().register(channel);
return regFuture;
}
在这里同样将不少的代码都省略掉, 抽取主要流程.
创建 channel
这里看到是用 channelFactory.newChannel() 来进行 channel 的创建, 从上面已知道的代码中, 这个 Factory 是 ReflectiveChannelFactory, 查看它的 newChannel() 方法:
@Override
public T newChannel() {
try {
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
这里是根据 class 使用反射进行 Channel 的创建, 也就是我们一开始传进来的 NioServerSocketChannel.class
init 初始化
ServerBootstrap 的 init 方法
void init(Channel channel) throws Exception
个人感觉这个方法, 似乎没什么好讲的…
注册方法
register 这里调用到 SingleThreadEventLoop 的 register 方法, 最后会调用到 AbstractChannel 的 register(EventLoop, ChannelPromise) -> register0(ChannelPromise) -> AbstractNioChannel#doRegister()
在doRegister() 方法中, 最后会调用 java nio 的注册
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
到这里, 注册和初始化已经完成了, 接下来调用后面的 doBind0()
这里有一个流程的问题, 就是最后调用 doRegister() 方法的时候, 它的一些处理流程.
private void register0(ChannelPromise promise) {
doRegister();
pipeline.fireChannelRegistered();
if (firstRegistration) {
pipeline.fireChannelActive();
}else{
beginRead();
}
}
@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return pipeline.bind(localAddress, promise);
}
doBind0() 中调用 AbstractChannel#bind -> pipeline#bind -> tail.bind -> invokeBind
private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) {
try {
// 最后调到这里,
((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
} else {
bind(localAddress, promise);
}
}
如果你有自定义的 Handle 实现了 ChannelOutboundHandler 接口
的话, 最终事件就会传递到自定义 Handle的 bind 方法上.
在这所有 Handler 的 bind 都调用之后, 最终会调用到 Netty 自动为你加上的最外层的 HeadContext
的 bind 方法, 因为它既是 inbound 也是 outbound.
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
unsafe.bind(localAddress, promise);
}
HeadContext 的 bind 方法 -> AbstractChannel#bind -> NioServerSocketChannel.doBind(这是其中一个子类的实现).
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
最终还是调用的 javaChannel() 的 bind
方法.
channel的创建、事件注册、绑定的流程到这里就结束了, 下面说的是一个关于 Netty 中另一个东西: Future
.
上面 bind 后的 sync() 方法还记得吗?
ChannelFuture f = b.bind(PORT).sync();
sync() 的作用是将当前线程阻塞在这里, 直至服务端启动完成, 而服务端启动完成依赖一个东西, 没错就是 Future, 阻塞在这里的实现还算是比较简单的, 这里简单的用了循环判断, 最终调用代码如下:
@Override
public Promise<V> await() throws InterruptedException {
if (isDone()) {
return this;
}
if (Thread.interrupted()) {
throw new InterruptedException(toString());
}
checkDeadLock();
// 无限等待, 直至完成
synchronized (this) {
while (!isDone()) {
incWaiters();
try {
wait();
} finally {
decWaiters();
}
}
}
return this;
}
从这里可以看到如果调用 sync() , 最终调用到 DefaultPromise#await()
会一直阻塞当前调用到线程, DefaultPromise 中存储一个执行的结果 result
, 并且它是用 volatile 来修饰的, 也就是其它线程修改了结果对其可见, 从最开始的 bind 一直往后的调用中, 相信大家也都看到了 Promise 一直在传递, 为的就是将结果设置进 Promise, 最后修改为成功 or 失败, 其它线程都能知道.
这里有一篇之前写的 Future/Promise 的源码解读
Netty 异步 Future 源码解读
本篇关于Netty服务端的启动流程到这里就结束了.