服务器启动后肯定是要接收客户端请求并返回客户端想要的数据的。那么Netty服务端在启动之后是如何接收客户端请求的呢?
服务端启动之后,最终会注册一个 Accept 事件等待客户端的连接。NioServerSocketChannel 将自己注册到了 bossGroup线程池(reactor线程)上,也就是EventLoop(实际类型为NioEventLoop)上。在上一节《Netty启动过程分析》的末尾,我们有提到 NioEventLoop,其中有个run()方法,源码如下:
@Override
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
// fall through
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
EventLoop 是一个死循环,在这个循环中做了 3 件事情:
①有条件的等待 NIO 事件;
②处理 NIO 事件;
③处理消息队列中的任务;
上面的run()方法中调用了processSelectedKeys()方法,其源码如下:
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
接下来processSelectedKeysPlain()方法的源码:
private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
...
for (;;) {
final SelectionKey k = i.next();
final Object a = k.attachment();
i.remove();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
...
}
}
接着调用 processSelectedKey(k, (AbstractNioChannel) a)方法:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
...
try {
...
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
当我们启动上一节的服务端,并监听端口8007,然后在浏览器中输入:http://localhost:8007时,就会和服务端建立连接,如果我们把断点打在 unsafe.read() 这一行时,会看到 readyOps 的值为16,即这是一个 Accept事件:
说明客户端(此时为浏览器)的请求已经进来了。
这里的 unsafe 是 bossGroup 线程中 NioServerSocketChannel 的AbstractNioMessageChannel$NioMessageUnsafe 类型的对象。我们进入到其read()方法中:
@Override
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
try {
do {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (exception != null) {
closed = closeOnReadError(exception);
pipeline.fireExceptionCaught(exception);
}
if (closed) {
inputShutdown = true;
if (isOpen()) {
close(voidPromise());
}
}
} finally {
// Check if there is a readPending which was not processed yet.
// This could be for two reasons:
// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
//
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
该方法主要做了以下几件事:
①检查该EventLoop线程是否是当前线程(assert eventLoop().inEventLoop()😉
②执行 doReadMessages(readBuf) 方法,其中的变量 readBuf 是一个List,也就是一个容器,该方法的作用是读取 bossGroup 线程中的 NioServerSocketChannel 接收到的请求,并把这些请求放进容器:
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t);
try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}
说明
:该方法是通过 ServerSocket 的accept()方法获取到一个TCP连接,然后将其封装为Netty的NioSocketChannel对象,最后添加到容器中
public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
@Override
public SocketChannel run() throws IOException {
return serverSocketChannel.accept();
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getCause();
}
}
③循环 readBuf 这个容器中的所有请求,执行 pipeline.fireChannelRead(readBuf.get(i))(即开始执行 在handler()方法中注册的handler的channelRead()),用于处理接收的这些请求或者其他事件,这其实相当于消费 readBuf 中注册的 NioSocketChannel,即处理所有新进来的连接,然后就开始执行管道中注册的 handler 的 channelRead()方法,需要强调的是,这里说的管道中注册的handler是通过handler()方法注册的handler而非通过childHandler()注册的handler:
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 LoggingHandler(LogLevel.INFO));// 将会在每次客户端有事件发生时调用
p.addLast(new EchoServerHandler());
}
});
正如代码中的注释所表述的那样:通过handler()方法注册的handler在每次有客户端连接时会调用,通过childHandler()方法注册的handler则是在客户端连接建立成功之后有读写等操作时触发。
服务端的代码中,我们只显示注册了一个LoggingHandler,其实Netty会帮我们注册另外几个handler,此例中,最终注册的handler有:Head、LoggingHandler、ServerBootstrapAcceptor
、Tail。我们重点看下ServerBootstrapAcceptor:因为正是**ServerBootstrapAcceptor完成了将连接转交给workerGroup,同时为每个SocketChannel注册我们在childHandler()方法中指定的handler的工作
**
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
// 将客户端连接注册到workerGroup的线程池中
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
该方法做了以下几件事情:
①将msg强转为Channel,实际上是NioSocketChannel
②为①中的NioSocketChannel的pipeline添加handler,注意这里添加的handler是在childHandler()方法中指定的那些handler
③设置NioSocketChannel的各种属性
④将该NioSocketChannel注册到childGroup中的一个EventLoop上,并添加一个监听器,这个childGroup就是我们创建服务端时的workerGroup
一路跟踪代码,会跟到AbstractNioChannel的doBeginRead()方法:
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
至此,针对这个客户端的连接就完成了,接下来就可以监听读事件了。
总体流程:接收连接->创建一个新的NioSocketChannel->将NioSocketChannel注册到workerGroup的EventLoop->监听selector read事件。
结合源代码来说,其流程主要有以下4步:
①服务端启动时创建的NioEventLoop的对象会通过死循环的方式轮询监听有没有新的客户端连接进来;
②监听到有新的客户端连接(即Accept事件)时,就创建一个NioSocketChannel,并将其置于一个容器中;
③遍历上述的容器,执行fireChannelRead()方法,使每一个新进来的客户端连接都经过我们在handler()方法中指定的handler的处理(即执行这些handler的channelRead()方法),需要特别注意的是,netty会隐式地为我们注册Head、ServerBootstrapAcceptor和Tail这三个handler,因此这三个handle地channelRead()方法也会得到执行;
④在执行ServerBootstrapAcceptor地channelRead()方法时,会将②中创建的NioSocketChannel转交给workerGroup(将其注册到workerGroup中的EventLoop中,监听读写等事件),同时为这些NioSocketChannel绑定我们在childHandler()中指定的handler;