netty 服务端启动流程案例解析
为什么选择netty?
- netty底层基于jdk的NIO,我们为什么不直接基于jdk的nio或者其他nio框架?下面是我总结出来的原因
- 使用jdk自带的nio需要了解太多的概念,编程复杂,并且在Java原生的IO中只能通过重新实现的方式去切换io并且根据需求变更时,改动也比较大,而netty底层IO模型随意切换,而这一切只需要做微小的改动。
- netty自带的拆包解包,异常检测等机制让你从nio的繁重细节中脱离出来,让你只需要关心业务逻辑。
- netty解决了jdk的很多包括空轮训在内的bug。
- netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程做到非常高效的并发处理
- 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
- netty社区活跃,遇到问题随时邮件列表或者issue
- netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大
netty的启动–一个入场案例
public final class SimpleServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new SimpleServerHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
这个启动服务端的案例我们再也熟悉不过了,这次我们就着这个例子开始分析服务端netty的启动过程,通过案例可以知道服务端的服务端口绑定在8888,使用nio模式,下面讲下每一个步骤的处理细节:
EventLoopGroup :已经在我的其他文章中详细剖析过,说白了,就是一个死循环,不停地检测IO事件,处理IO事件,执行任务
ServerBootstrap :是服务端的一个启动辅助类,通过给他设置一系列参数来绑定端口启动服务
group(bossGroup, workerGroup) :我们需要两种类型的人干活,一个是老板,一个是工人,老板负责从外面接活,接到的活分配给工人干,放到这里,bossGroup的作用就是不断地accept到新的连接,将新的连接丢给workerGroup来处理。
channel(NioServerSocketChannel.class): 表示服务端启动的是nio相关的channel,channel在netty里面是一大核心概念,可以理解为一条channel就是一个连接或者一个服务端bind动作。
handler(new SimpleServerHandler() :表示服务器启动过程中,需要经过哪些流程,这里SimpleServerHandler最终的顶层接口为ChannelHander,是netty的一大核心概念,表示数据流经过的处理器,可以理解为流水线上的每一道关卡,同时也可以理解为这个流水线上的每一个处理器。
childHandler(new ChannelInitializer<SocketChannel>) :表示一条新的连接进来之后,该怎么处理,也就是上面所说的,老板如何给工人配活。
ChannelFuture f = b.bind(8888).sync(); :这里就是真正的启动过程了,绑定8888端口,等待服务器启动完毕。
f.channel().closeFuture().sync(); :等待服务端关闭socket。
bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); 关闭两组死循环。
逐个击破
首先,上面说到程序是从bind方法进入的:
b.bind(8888).sync();
追踪可以发现,其实bind方法有很多实现,但以上面的案例为主线的话其实调用的是下面的方法:
/**
* Create a new {@link Channel} and bind it.
*/
public ChannelFuture bind(int inetPort) {
return bind(new InetSocketAddress(inetPort));
}
本质上是传递进入一个网络socket地址,熟悉socket编程的并不陌生,其实这里也是基于这个地址,也是里面的innetPort指定的端口的位置,建立了一个socket连接。
public ChannelFuture bind(SocketAddress localAddress) {
validate();
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
return doBind(localAddress);
}
这个方法的流程是先去校验,如果没有地址这直接抛出空指针异常,否则则建立链接,需要说明的一点是,在很多地方doxxx方法一般是具体的干活的方法,例如在java servlet中的doget,dopost,或者在spring启动过程中的doService方法等等,所以这也是我们读源码得到的一种经验吧:
public B validate() {
if (group == null) {
throw new IllegalStateException("group not set");
}
if (channelFactory == null) {
throw new IllegalStateException("channel or channelFactory not set");
}
return (B) this;
}
对于校验,没什么好说的,见到代码就知道含义了,你可能对group和channelfactory怎么得到的比较疑惑,别急,看下面的代码:
bootstrap.group(bossGroup,workerGroup)......
|
\|/
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (childGroup == null) {
throw new NullPointerException("childGroup");
}
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
|
\|/
AbstractBootStrap 的group方法:
public B group(EventLoopGroup group) {
if (group == null) {
throw new NullPointerException("group");
}
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return (B) this;
}
也就是说我们开始建立的worker线程组和boss线程组是为了初始化bootStrap实例对象中对应的变量,完成初始化之后才能够使得校验方法能够正确的执行。再追踪下doBind方法:
private ChannelFuture doBind(final SocketAddress localAddress) {
//...
final ChannelFuture regFuture = initAndRegister();
//...
final Channel channel = regFuture.channel();
//...
doBind0(regFuture, channel, localAddress, promise);
//...
return promise;
}
这段代码中的具体的实现部分进行了屏蔽,屏蔽后来看下方法的主要的执行流程,看过之后可以发现,真正起作用的就是initAndRegister方法和doBind0方法。正如见到名字则可以猜想其动作,前者应该是注册某些东西,那让他我们看下到底注册了什么:
// 首先基于之前初始化的channelFactory新建了一个channel
channel = channelFactory.newChannel();
// 之后通过init方法初始化这个channel
init(channel);
// 其实本质上就是对于这个channel的设置,对应的其实就是设置通道的option
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
}
//然后将这对象注册
final Channel channel = regFuture.channel();
ChannelPromise promise = channel.newPromise();
// 然后跟踪进入doBind0方法
逐步看下上面的方法到底做了什么:
1. channel是怎么来的?
我们发现这条channel是通过一个 channelFactory new出来的,channelFactory 的接口很简单。
public interface ChannelFactory<T extends Channel> extends io.netty.bootstrap.ChannelFactory<T> {
/**
* Creates a new channel.
*/
@Override
T newChannel();
}
- 怎么被初始化的?
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 (B) this;
}
// 在这里被赋值,我们层层回溯,查看该函数被调用的地方,发现最终是在这个函数中,ChannelFactory被new出
public B channel(Class<? extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
// 其实本质上是这么一个逻辑:demo程序调用channel(channelClass)方法的时候,将channelClass作为ReflectiveChannelFactory的构造函数创建出一个ReflectiveChannelFactory
// 然后去开始执行newChannel方法,本质上是个工厂方法,所以可以推断其执行的是ReflectiveChannelFactory的channelClass类。
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {
private final Class<? extends T> clazz;
public ReflectiveChannelFactory(Class<? extends T> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
this.clazz = clazz;
}
@Override
public T newChannel() {
try {
***return clazz.newInstance();***
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}
}
// 上面那句newInstance可以看出是通过反射的方式完成对象的创建,而这个class就是我们在ServerBootstrap中传入的NioServerSocketChannel.class,本质上其实类似于直接去new一个新的对象。newInstance方法是调用不带参数的构造方法,那再追溯一下其构造方法:
private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
这里提一下,读源码细节,有两种读的方式,一种是回溯,比如用到某个对象的时候可以逐层追溯,一定会找到该对象的最开始被创建的代码区块,还有一种方式就是自顶向下,逐层分析,一般用在分析某个具体的方法,庖丁解牛,最后拼接出完整的流程。
2. Channel等核心组件是如何生成的?
private static ServerSocketChannel newSocket(SelectorProvider provider) {
//...
return provider.openServerSocketChannel();
}
|
\|/
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
第二行new了一个NioServerSocketChannelConfig类的对象,顶层的接口则是ChannelConfig,基本可以判定,ChannelConfig 也是netty里面的一大核心模块,初次看源码,看到这里,我们大可不必深挖这个对象,而是在用到的时候再回来深究,只要记住,这个对象在创建NioServerSocketChannel对象的时候被创建即可。在上面的代码中可以看到,super方法调用了父类的构造方法,接着往上追溯,最后会找到AbstractNioChannel类的构造方法:
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
//...
ch.configureBlocking(false);
//...
}
这里,简单地将前面 provider.openServerSocketChannel(); 创建出来的 ServerSocketChannel 保存到成员变量,然后调用ch.configureBlocking(false);设置该channel为非阻塞模式,标准的jdk nio编程的写法。这里的 readInterestOp 即前面层层传入的 SelectionKey.OP_ACCEPT,接下来重点分析 super(parent);(这里的parent其实是null,由前面写死传入)
接着再往上追溯则可以到AbstractChannel类的构造方法中:
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
可从代码中看到,新new出来了几个对象,id为channel的唯一标识,unsafe是I/O线程反射调用使用的,而不是用户代码直接使用的,对于pipline,是与上面的unsafe有关的,其实newUnsafe方法最终的执行部分是NioServerSocketChannel中的对应方法:
pipeline = newChannelPipeline();
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
追溯DefaultChannelPipeline 是干嘛用的,我们仍然使用上面的方式,查看顶层接口ChannelPipeline的定义:
A list of ChannelHandlers which handles or intercepts inbound events and outbound operations of a Channel
相信都已经明白了,这其实就是一个有关于事件的handles和intercepts的集合。简单的总结一下就是,用户调用方法 Bootstrap.bind(port) 第一步就是通过反射的方式new一个NioServerSocketChannel对象,并且在new的过程中创建了一系列的核心组件,仅此而已,并无他,真正的启动我们还需要继续跟。其中新生成的组件有:
- Channel
- ChannelConfig
- ChannelId
- Unsafe
- Pipeline
- ChannelHander
3. channel是如何初始化的?
到了这里,回忆一下第一步newChannel完毕,这里就对这个channel做init,init方法具体干啥:
@Override
void init(Channel channel) throws Exception {
// 设置options
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
channel.config().setOptions(options);
}
// 设置attrs
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());
}
}
// 然后将得到的options和attrs注入到channelConfig或者channel中
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(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(
// 到了最后一步,p.addLast()向serverChannel的流水线处理器中加入了一个 ServerBootstrapAcceptor,从名字上
// 就可以看出来,这是一个接入器,专门接受新请求,把新的请求扔给某个事件循环器,我们先不做过多分析
currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
通过上面的分析可以知道,本质上并没有初始化对象,其实也是设置了启动之前需要的很多的内容,一些基本的配置和属性,同时还在pipline上加上了接入器,对此继续深入可以知:
ChannelFuture regFuture = config().group().register(channel);
|
\|/
@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()返回的应该是什么对象吗?不记得的话可以看下前面关于unsafe的描述,或者最快的方式就是debug到这边,跟到register方法里面,看看是哪种类型的unsafe:
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// ...
AbstractChannel.this.eventLoop = eventLoop;
// ...
register0(promise);
}
可以发现,在netty里面,后缀为0的也是真正执行操作的方法:
private void register0(ChannelPromise promise) {
try {
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);
}
}
@Override
public boolean isActive() {
return javaChannel().socket().isBound();
}
这里isBound()返回false,但是从目前我们跟下来的流程看,我们并没有将一个ServerSocket绑定到一个address,所以 isActive() 返回false,我们没有成功进入到pipeline.fireChannelActive();方法。而近的发起端是一个线程Runnable的run方法,那么就在提交Runnable对象方法的地方打一个断点,去掉其他断点,重新debug,比如我们首次debug发现调用栈中的最近的一个Runnable如下:
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
我们停在了这一行pipeline.fireChannelActive();, 我们想看最初始的调用,就得跳出来,断点打到 if (!wasActive && isActive()),因为netty里面很多任务执行都是异步线程即reactor线程调用的,如果我们要查看最先发起的方法调用,我们必须得查看Runnable被提交的地方,逐次递归下去,就能找到那行"消失的代码",最终,通过这种方式,终于找到了 pipeline.fireChannelActive(); 的发起调用的代码,不巧,刚好就是下面的doBind0()方法。
下面看下真正干活的doBind0方法:
private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
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());
}
}
});
}
我们发现,在调用doBind0(…)方法的时候,是通过包装一个Runnable进行异步化的,接下来我们进入到channel.bind()方法:
@Override
public ChannelFuture bind(SocketAddress localAddress) {
return pipeline.bind(localAddress);
}
|
\|/
@Override
public final ChannelFuture bind(SocketAddress localAddress) {
return tail.bind(localAddress);
}
|
\|/
// 这里的unsafe就是前面提到的 AbstractUnsafe, 准确点,应该是 NioMessageUnsafe
@Override
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
unsafe.bind(localAddress, promise);
}
|
\|/
@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
// ...
boolean wasActive = isActive();
// ...
doBind(localAddress);
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
safeSetSuccess(promise);
}
显然按照正常流程,我们前面已经分析到 isActive(); 方法返回false,进入到 doBind()之后,如果channel被激活了,就发起pipeline.fireChannelActive();
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
//noinspection Since15
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
最终调到了jdk里面的bind方法,这行代码过后,正常情况下,就真正进行了端口的绑定。另外,通过自顶向下的方式分析,在调用pipeline.fireChannelActive();方法的时候,会调用到如下方法:
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
|
\|/
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read();
}
}
|
\|/
private volatile int autoRead = 1;
public boolean isAutoRead() {
return autoRead == 1;
}
|
\|/
这里的this.selectionKey就是我们在前面register步骤返回的对象,前面我们在register的时候,注册测ops是0
public Channel read() {
pipeline.read();
return this;
}
// 最终调用到AbstractNioUnsafe
protected void doBeginRead() throws Exception {
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
回忆一下注册 AbstractNioChannel
selectionKey = javaChannel().register(eventLoop().selector, 0, this)
而这里相当于取出来:
selectionKey.interestOps(interestOps | readInterestOp);
而这里的 readInterestOp 就是前面newChannel的时候传入的SelectionKey.OP_ACCEPT,又是标准的jdk nio的玩法,到此,你需要了解的细节基本已经差不多了。
整体流程总结:
最后,我们来做下总结,netty启动一个服务所经过的流程
- 设置启动类参数,最重要的就是设置channel。
- 创建server对应的channel,创建各大组件,包括ChannelConfig,ChannelId,ChannelPipeline,ChannelHandler,Unsafe等。
- 初始化server对应的channel,设置一些attr,option,以及设置子channel的attr,option,给server的channel添加新channel接入器,并触发addHandler,register等事件。
- 调用到jdk底层做端口绑定,并触发active事件,active触发的时候,真正做服务端口绑定。