netty 是一个网络通信框架,底层是 java 的 NIO。使用 netty 后开发者可以将重点都放在对业务的处理上。而它的启动流程是有固定的模板的。
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 1. 创建负责接收客户端连接的事件循环组(公司的老板)
EventLoopGroup boss = new NioEventLoopGroup(1);
// 2. 创建负责处理客户端发送数据的事件循环组(员工)
EventLoopGroup worker = new NioEventLoopGroup();
// 3. 生成服务端启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
// 4. 设置线程队列中等待连接的个数
.option(ChannelOption.SO_BACKLOG, 128)
// 5. 保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 6. 老板接待客户的地方,走流程签合同
.handler(new BossChannelInboundHandler())
// 7. 员工跟客户谈事情的地方
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new WorkerChannelInboundHandler());
}
});
// 8. 启动服务器
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
}
}
而 Netty 底层实现用到的是 Java NIO。那么一个标准的 NIO 服务器启动模板是这样的。
private Selector selector;
private ServerSocketChannel serverChannel;
public ChatRoomServer() throws IOException {
// 1. 获取 ServerSocketChannel
this.serverChannel = ServerSocketChannel.open();
// 2. 获取 Selector
this.selector = Selector.open();
// 3. 设置非阻塞
serverChannel.configureBlocking(false);
// 4. 注册感兴趣的时间并添加附加对象
SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT,new Object());
sk.
// 5. 可以改变感兴趣的事件
sk.interestOps(SelectionKey.OP_ACCEPT);
// 6. 绑定端口
InetSocketAddress addr = new InetSocketAddress(8888);
serverChannel.socket().bind(addr);
}
所以现在需要找到在 Netty 服务器启动的过程中,在哪里调用了 NIO 的底层 api。
流程分析
EventLoopGroup 是什么
EventLoopGroup 是一个存放 NioEventLoop 的集合,这个看代码里怎么设置参数的。一般 Boss 设置1,Worker 不设置默认就是机器 CPU 数量 * 2。这种设置参数用到的 Reactor 模型就是主从多线程模型,即 Boss 是主,Worker 是从。
NioEventLoop 里面维护了一个 tailTasks 无界任务队列LinkedBlockingQueue
。
Reactor 线程就是开启一个线程,做三件事情:
- 轮询注册事件。
- 处理 IO 事件。
- 处理任务队列里面的任务。
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
}
if (strategy == null) {
throw new NullPointerException("selectStrategy");
}
provider = selectorProvider;
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
unwrappedSelector = selectorTuple.unwrappedSelector;
selectStrategy = strategy;
}
这段 NioEventLoop 的构造器中这段代码final SelectorTuple selectorTuple = openSelector();
就是获取到了一个包装后的 Jdk Selector。那么这就找到了一个调用点。
源码跟进
从serverBootstrap.bind(8888)
这行代码开始跟进。到io.netty.bootstrap.AbstractBootstrap#doBind(final SocketAddress localAddress)
里面可以看到两行重要的代码。
System.out.println("开始初始化和注册");
final ChannelFuture regFuture = initAndRegister();
// 绑定地址
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) {
promise.setFailure(cause);
} else {
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
进入initAndRegister()
方法,这里重点关注三行代码。
channel = channelFactory.newChannel();
init(channel);
ChannelFuture regFuture = config().group().register(channel);
第一行是新建一个 ServerSocketChannel,这里是通过反射的方式生成了一个 NioServerSocketChannel 实例。看看 NioServerSocketChannel 的构造方法。
public NioServerSocketChannel() {
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
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());
}
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
ch.configureBlocking(false);
}
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
System.out.println("创建 pipeline");
pipeline = newChannelPipeline();
}
在 NioServerSocketChannel 的无惨构造器中,发现做了很多事情。
- 通过
newSocket(DEFAULT_SELECTOR_PROVIDER)
获取到了 ServerSocketChannel。这又是一个点。 - 在
AbstractChannel
的构造方法中,创建了 Netty 的核心 pipeline。 - 然后设置
ch.configureBlocking(false);
非阻塞。至此 Nio 方式的前三步已经找到了。
接下来看init(Channel channel)
初始化 Channel 的方法。这里面的核心是加了一个 ChannelInitializer。
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline(); ChannelInitializer<ServerSocketChannel>()
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));
}
});
}
});
这段代码很眼熟,因为我们在写 Netty 的时候,也会自己写一个 ChannelInitializer,然后再里面加上自己的 ChannelHandler。这里也是做这样的事情,在pipeline.addLast(handler);
体现出来的。其实ChannelInitializer
就是一个辅助类。然后加了一个任务到任务队列中,这个任务就是为了添加一个ServerBootstrapAcceptor
,这也是一个 ChannelHandler,要等到后面才真正添加。
然后就是开始注册。 ChannelFuture regFuture = config().group().register(channel);
。一路跟进,找到io.netty.channel.AbstractChannel.AbstractUnsafe#register(EventLoop eventLoop, final ChannelPromise promise)
方法。这里面的核心是register0(promise)
。这里有个判断,如果 Reactor 线程开启了,那么就立即执行,如果没有就等加入任务后面在执行,总之是要执行的。那么看看加入任务的方法。
addTask(task);
if (!inEventLoop) {
startThread();
}
将任务加入任务队列,然后判断当前线程是不是 Reactor 线程,如果不是就开启 Reactor 线程。再次跟进,到SingleThreadEventExecutor.this.run();
,进入。此时找到io.netty.channel.nio.NioEventLoop#run
。看核心代码
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// fall-through to SELECT since the busy-wait is not supported with NIO
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
default:
}
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
这段代码就是之前说的 Reactor 线程做的三件事。首先判断任务队列里有没有任务,如果有任务,那么先去执行selector.selectNow()
看下有没有连接过来,立即返回。如果没有任务,那么就去轮询注册事件(这时候是没有任何 SocketChannel 注册进入 Selector 的),然后处理 IO 事件,然后再去执行任务队列,但是这时候的任务队列是同步执行的,是直接调用run
而不是start
。
现在回顾刚才加入任务队列的任务register0(promise)。核心代码如下:
doRegister();
pipeline.invokeHandlerAddedIfNeeded();
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
一行一行看,先看doRegister();
这里面有一行关键性的代码。
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
将之前的 ServerSocketChannel 注册进入了 Selector,并且附加对象是当前的 NioServerSocketChannel,但是关心的事件是0,就是什么都不关心。
接着看pipeline.fireChannelRegistered();
。这里面是处理之前加入 pipeline 的 ChannelInitializer,通过调用它的initChannel
方法将我们自己定义的 ChannelHandler 加入进去,然后移除掉自己,剩下的就是head--->myhandler--->tail
。然后又通过任务的方式加入ServerBootstrapAcceptor
到 pipeline 中。
safeSetSuccess(promise);
是触发之前的回调监听,这样就可以去执行bind0
方法了。bind0
中主要就是添加一个任务。
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
跟进。
public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
if (localAddress == null) {
throw new NullPointerException("localAddress");
}
if (isNotValidPromise(promise, false)) {
// cancelled
return promise;
}
System.out.println("找出栈处理器,head");
final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
EventExecutor executor = next.executor();
// 判断是否已经开启了 reactor 线程
if (executor.inEventLoop()) {
System.out.println("开启了 reactor 线程,立即执行真正的 bind");
next.invokeBind(localAddress, promise);
} else {
System.out.println("没有开启 reactor 线程,加入执行真正的 bind 的任务,稍后执行");
safeExecute(executor, new Runnable() {
@Override
public void run() {
next.invokeBind(localAddress, promise);
}
}, promise, null);
}
return promise;
}
一路debug跟进,找到了真正的核心代码了。
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
boolean wasActive = isActive();
try {
doBind(localAddress);
} catch (Throwable t) {
safeSetFailure(promise, t);
closeIfClosed();
return;
}
if (!wasActive && isActive()) {
// 这里又加入了一个任务
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
}
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
这段代码是真正的 bind 方法了,但是还缺了一点,就是 selector 还没有感兴趣的时间。可以看到在执行完绑定方法之后,又加入了一个任务pipeline.fireChannelActive();
。后面的同步执行任务的时候,会触发这个方法。这个方法是传播通道活跃事件的,
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.fireChannelActive();
readIfIsAutoRead();
}
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead();
}
// io.netty.channel.nio.AbstractNioChannel#doBeginRead
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);
}
}
从io.netty.channel.DefaultChannelPipeline.HeadContext#channelActive
跟进,找到了selectionKey.interestOps(interestOps | readInterestOp);
。至此,Nio server 的设置已经全部找到了。
pipeline.fireChannelRegistered();
就是调用 pipeline 的channelRegistered
方法了,一层一层传播。
总结
说到底,启动流程就是开启一个 Reactor 线程去做三件事。轮询注册事件、处理 IO 事件和处理任务队列。任务队列里主要有这几件事:
- bind 任务。
- 添加 ServerBootstrapAcceptor 的任务。
- register0 任务。
- 绑定完成后的 pipeline.fireChannelActive() 任务。