本文主要分析服务端新连接的接入过程,主要分为以下 3 各步骤:
- select 操作;
- processSelectedKeys 操作;
- runAllTasks 操作。
1. select 操作
在分析 select 操作前,先要回顾一下 NioEventLoop 的 run()方法及其父类 SingleThreadEventExecutor 的 execute(Runnable task)方法。
1 @Override 2 public void execute(Runnable task) { 3 if (task == null) { 4 throw new NullPointerException("task"); 5 } 6 //判断是否是 netty 线程添加的任务 7 boolean inEventLoop = inEventLoop(); 8 if (inEventLoop) { 9 addTask(task);//是的话直接添加任务到队列 10 } else { 11 //不是的话,可能要创建 netty 线程 12 startThread(); 13 //然后添加任务到队列 14 addTask(task); 15 if (isShutdown() && removeTask(task)) { 16 reject(); 17 } 18 } 19 // 构造方法中将 addTaskWakesUp 置 false 20 // wakesUpForTask(task) 直接返回 true 21 if (!addTaskWakesUp && wakesUpForTask(task)) { 22 // 所以 wakeup()方法肯定会被调用 23 wakeup(inEventLoop); 24 } 25 }
这里的 wakeup(boolean inEventLoop)方法分析 NioEventLoop 中的:
1 @Override 2 protected void wakeup(boolean inEventLoop) { 3 // 如果是非 Netty 线程添加任务,那么 wakenUp.compareAndSet(false ,true) 4 // 成功的话,就会调用 selector 的 wakeup()方法 5 if (!inEventLoop && wakenUp.compareAndSet(false, true)) { 6 // 回顾一下 selector 的 wakeup()方法 7 // 1. 当 selector 在执行 select 操作时,调用它的 wakeup()方法, 8 // 那么当前的 select 操作会立刻返回已就绪的事件集 9 // 2. 如果提前调用 selector 的 wakeup()方法(一次或者多次都是一样的) 10 // 那么下一次的 select 操作会直接返回(应该是没有就绪事件) 11 // 关于 selector 的 wakeup()方法,可以看一下文末的参考资料 12 selector.wakeup(); 13 } 14 }
接下来再 分析以下 NioEventLoop 中的 run()方法:
1 protected void run() { 2 for (;;) { 3 try { 4 // 计算 select 策略,当前有任务时,会进行一次 selectNow 操作返回就绪的 key 个数(大于等于 0) 5 // SelectStrategy.CONTINUE 值是 -2,SelectStrategy.SELECT 的值是 -1 6 // (这里的 SelectStrategy.CONTINUE 感觉不会匹配到) 7 // 显然 switch 中没有匹配项,直接跳出 switch 8 // 无任务时,则直接返回 SelectStrategy.SELECT 9 switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) { 10 case SelectStrategy.CONTINUE: 11 continue; 12 case SelectStrategy.SELECT: 13 //当没有可处理的任务时,直接进行 select 操作 14 // wakenUp.getAndSet(false) 返回的是 oldValue,由于默认值是 false 15 // 所以第一次返回的是 false,需要注意的是,只有在这个地方才将 wakenUp 置为 false 16 select(wakenUp.getAndSet(false)); 17 18 if (wakenUp.get()) { 19 selector.wakeup(); 20 } 21 default: 22 // fallthrough 23 } 24 cancelledKeys = 0; 25 needsToSelectAgain = false; 26 final int ioRatio = this.ioRatio; 27 //根据比例来处理 IO 事件和任务 28 if (ioRatio == 100) { 29 //... 30 } else { 31 final long ioStartTime = System.nanoTime(); 32 try { 33 processSelectedKeys(); 34 } finally { 35 // Ensure we always run tasks. 36 // 计算出处理 IO 事件的时间,然后根据比例算出执行任务的时间 37 final long ioTime = System.nanoTime() - ioStartTime; 38 runAllTasks(ioTime * (100 - ioRatio) / ioRatio); 39 } 40 } 41 } catch (Throwable t) { 42 handleLoopException(t); 43 } 44 //... 45 } 46 }
wakenUp 是用来减少调用 selector 的 wakeup()方法,关于 wakeup()方法的实现细节,参考文末的资料。接下来分析一下 key 的处理:
1 private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) { 2 for (int i = 0;; i ++) { 3 final SelectionKey k = selectedKeys[i]; 4 if (k == null) { 5 break; 6 } 7 // null out entry in the array to allow to have it GC'ed once the Channel close 8 // See https://github.com/netty/netty/issues/2363 9 // 置为 null,参考上面的一行的注释 10 selectedKeys[i] = null; 11 12 // 在前面 AbstractNioChannel 中的 doRegister()方法中,注册的时候传入的参数是 this 13 // 接入连接时,这个 a 就是 AbstractNioChannel 对象 14 final Object a = k.attachment(); 15 16 if (a instanceof AbstractNioChannel) { 17 processSelectedKey(k, (AbstractNioChannel) a); 18 } else { 19 //这里暂时不熟悉 20 @SuppressWarnings("unchecked") 21 NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a; 22 processSelectedKey(k, task); 23 } 24 //... 25 } 26 }
接下来分析一下 processSelectedKey()方法:
1 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { 2 //取出 channel 对应的 unsafe 3 final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); 4 if (!k.isValid()) { 5 //... 当 key 无效时,关闭 channel 等操作 6 } 7 8 try { 9 int readyOps = k.readyOps(); 10 // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise 11 // the NIO JDK channel implementation may throw a NotYetConnectedException. 12 if ((readyOps & SelectionKey.OP_CONNECT) != 0) { 13 // 连接就绪事件只需要处理一次就行了,否则后续的 select()操作会一直立刻返回 14 // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking 15 // See https://github.com/netty/netty/issues/924 16 int ops = k.interestOps(); 17 ops &= ~SelectionKey.OP_CONNECT; 18 k.interestOps(ops); 19 // 最后调用 SocketChannel 的 finishConnect()方法 20 unsafe.finishConnect(); 21 } 22 23 // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. 24 if ((readyOps & SelectionKey.OP_WRITE) != 0) { 25 // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write 26 ch.unsafe().forceFlush(); 27 } 28 29 // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead 30 // to a spin loop 31 if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { 32 // read 和 accept 33 unsafe.read(); 34 if (!ch.isOpen()) { 35 // Connection already closed - no need to handle write. 36 return; 37 } 38 } 39 } catch (CancelledKeyException ignored) { 40 unsafe.close(unsafe.voidPromise()); 41 } 42 }
由于新连接的接入是一个就绪的 ACCEPT 事件,所以分析一下 unsafe.read(),这个 unsafe 对象是创建服务端 Channel 时创建的,是一个 NioMessageUnsafe 对象,它的 read()方法中有一行:
1 int localRead = doReadMessages(readBuf);
该方法的实现选择 NioServerSocketChannel 中的:
1 @Override 2 protected int doReadMessages(List<Object> buf) throws Exception { 3 // 接入连接 4 SocketChannel ch = javaChannel().accept(); 5 try { 6 if (ch != null) { 7 // 向 Object 列表中加入封装后的 NioSocketChannel 对象 8 buf.add(new NioSocketChannel(this, ch)); 9 return 1; 10 } 11 } catch (Throwable t) { 12 logger.warn("Failed to create a new channel from an accepted socket.", t); 13 14 try { 15 ch.close(); 16 } catch (Throwable t2) { 17 logger.warn("Failed to close a socket.", t2); 18 } 19 } 20 21 return 0; 22 }
回到 NioMessageUnsafe 的 read()方法,doReadMessages()方法返回了 NioSocketChannel 列表,接下来的一行:
1 pipeline.fireChannelRead(readBuf.get(i));
就会调用服务端 Channel 中的 handler 链,首先是用户添加的 handler,最后会找到前面说过的 ServerBootstrapAcceptor,它的 channelRead()方法中的 msg 参数实际上是 Channel 对象。
接下来对这个客户端的 Channel 的处理与服务端 Channel 的处理过程基本类似。