源码调试技巧:从基本概念出手,寻找突破点;观察线程栈,寻找关键字眼
简单回顾。netty 是如何启动的
上一节回顾 netty 是如何启动的
服务启动的时候会创建ServerSocketChannel ,并将之与 ChannelPipeline 、 EventLoop 、 Selector 、 Java 原生 Channel 进行绑定,同时通过 Java 原生 Channel 绑定到一个本地地址(port端口)。
那么,如果有新连接进来,服务是如何接收(或接受, Accept )的呢?
服务启动的时候会往 NioServerSocketChannel 对应的 ChannelPipeline 后面添加一个叫做 ServerBootstrapAcceptor 的 ChannelHandler ,那么,问题是:
- ServerBootstrapAcceptor 的作用是什么?
- 接收连接的过程是否也要跟 Java 原生 Channel 打交道?
- Selector 又是在哪里使用到的?
源码解读,大胆假设法。顾名思义,连接连接。肯定是客户端需要读取数据,inbound 之类的啦,所以
我们知道, Netty 中将 ChannelHandler 分成 inbound 和 outbound 两种类型,既然是接收新连接,那肯定是inbound ,所以,我们先从 ChannelInboundHandler 这个接口出发,分析它有哪些方法。
简单瞄一眼,大致发现有三种可能跟接收新连接有关系的:
channelRegistered ()/channelUnregistered () ,服务启动的时候有看到 Register 相关的调用
channelActive ()/channelInactive () ,服务启动绑定完地址的时候有看到 Active 相关的调用
channelRead ()/channelReadComplete () ,暂未看到在哪里使用的。
大胆猜测应该是 channelRead ()/channelReadComplete ()
io.netty.channel.nio.NioEventLoop#processSelectedKeysOptimized
private void processSelectedKeysOptimized() {
// 遍历SelectionKey
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
// 优化GC
selectedKeys.keys[i] = null;
// 取出SelectionKey中的附件,还记得附件是什么吗?
// 上一节,服务启动的时候把Selector、Java原生Channel、Netty的Channel绑定在一起了
// 其中,Netty的Channel是通过attachment绑定到SelectionKey中的
// 所以,针对新连接建立的过程,这里取出来的就是Netty中的NioServerSocketChannel
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 处理之
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
// null out entries in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
// If the channel implementation throws an exception because there is no event loop, we ignore this
// because we are only trying to determine if ch is registered to this event loop and thus has authority
// to close ch.
return;
}
// Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
// still healthy and should not be closed.
// See https://github.com/netty/netty/issues/5125
if (eventLoop == this) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
}
return;
}
try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
// 如果是Connect事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// 如果是Write事件
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// 如果是Read事件或者Accept事件
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
可以看到,这里的写法也是跟我们的 NIO 编程保持一致的,针对不同的事件使用不同的逻辑进行处理,不同的是, Netty 中具体的处理逻辑交给了 Channel 的 unsafe 来处理,对于接收新连接的过程,这里的 unsafe 无疑就是NioServerSocketChannel 中的 unsafe 了。
private final class NioMessageUnsafe extends AbstractNioUnsafe {
private final List<Object> readBuf = new ArrayList<Object>();
@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 {
// key1 ,读取消息
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (continueReading(allocHandle));
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
// key2 ,触发 channelRead()
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();
}
}
}
}
1 doReadMessages (readBuf) ,读取消息,如何读取?读取的内容是什么?
2 pipeline.fireChannelRead (readBuf.get (i)) ,触发 ChannelHandler 的channelRead () 方法,最终由谁处理?
doReadMessages (readBuf)
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// key1 ,看起来跟 Java 原生 Channel 有关系
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// key2 ,构造了一个 Netty 的 NioSocketChannel ,并把 Java 原生 SocketChannel 传入
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;
}
// io.netty.util.internal.SocketUtils#accept
public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
@Override
public SocketChannel run() throws IOException {
// 调用 Java 原生的 aacept() 方法创建一个 SocketChannel
return serverSocketChannel.accept();
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getCause();
}
}
可以,
Netty 最终还是调用的 Java 原生的 SeverSocketChannel 的 accept () 方法来创建一个 SocketChannel ,并把这个 SocketChannel 绑定到 Netty 自己的 NioSocketChannel 中,还记得上一节创建 NioServerSocketChannel 的过程吗
NioSocketChannel 的创建过程也是一样的,我就不贴源码了,简单再回顾一下:
1 将 Java 原生 Channel 配置成非阻塞 ch.configureBlocking(false) ;
2 设置感兴趣的事件为 Read 事件, NioServerSocketChannel 感兴趣的事件为 Accept 事件;
3 分配 id ;创建 unsafe ;创建 ChannelPipeline ;
好了,到这里 SocketChannel 就创建好了,但是它的 pipeline 中还是只有 head 和 tail 两个 Handler ,还无法处理消息,那么, ChannelPipeline 的初始化在哪里呢?
这 就 轮 到 pipeline.fireChannelRead(readBuf.get(i)) 这行代码来发挥作用了,这里的 pipeline 实际上是NioServerSocketChannel 对应的 ChannelPipeline ,通过上一节的分析我们知道,服务启动完成后这个ChannelPipeline 中的双向链表为 head<=>LoggingHandler<=>ServerBootstrapAcceptor<=>tail ,很显然,只有
ServerBootstrapAcceptor 这个 ChannelHandler 会往子 ChannelPipeline 中添加子 ChannelHandler ,所以,我们直接看 ServerBootstrapAcceptor 的 channelRead () 方法即可。
// io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs;
private final Runnable enableAutoReadTask;
ServerBootstrapAcceptor(
final Channel channel, EventLoopGroup childGroup, ChannelHandler childHandler,
Entry<ChannelOption<?>, Object>[] childOptions, Entry<AttributeKey<?>, Object>[] childAttrs) {
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;
// Task which is scheduled to re-enable auto-read.
// It's important to create this Runnable before we try to submit it as otherwise the URLClassLoader may
// not be able to load the class because of the file limit it already reached.
//
// See https://github.com/netty/netty/issues/1328
enableAutoReadTask = new Runnable() {
@Override
public void run() {
channel.config().setAutoRead(true);
}
};
}
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 这里的 msg 就是上面的 readBuf.get(i) ,也就是 NioSocketChannel ,也就是子 Channel
final Channel child = (Channel) msg;
// 添加子 ChannelHandler ,这里同样也是以 ChannelInitializer 的形式添加的
child.pipeline().addLast(childHandler);
// 设置子 Channel 的配置等信息
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
try {
// 将子 Channel 注册到 workerGroup 中的一个 EventLoop 上
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);
}
}
我们来总结一下服务接收新连接的过程
- Netty 中轮询的方法是写在 NioEventLooop 中的;
- Netty 底层也是通过 Java 原生 ServerSocketChannel 来接收新连接的;(有点代理的味道了啊。增强一个原生的功能)
- Netty 将接收到的 SocketChannel 包装成了 NioSocketChannel ,并给它分配 ChannelPipeline 等元素;
- Netty 中通过 ServerBootstrapAcceptor 这个 ChannelHandler 来初始化 NioSocketChannel 的配置并将其注册到 EventLoop 中;
- 注册到 EventLoop 中同样也会与这个 EventLoop 中的 Selector 绑定, Netty 的 NioSocketChannel 同样地也是以附件的形式绑定在 SelectionKey 中
对 Java 原生NIO 的增强
至此,一个 SocketChannel 才算真正建立完成,也就可以接收消息了。
通过本节的剖析,我们知道了 Netty 中接收新连接的过程跟 Java 原生 NIO 是同出一辙,或者说是对 Java 原生NIO 的增强,那么,既然连接已经就绪,如何接收消息呢
Netty服务如何接收新的连接
服务接收新连接过程:
1在NioEventLoop中轮询select ,
2调用java原生ServerSocketChannel的accept()方法接收新连接
3.包装java原生SocketChannel,对其进行增强
服务启动的时候会创建 ServerSocketChannel,并将之与
ChannelPipeline、EventLoop、Selector、Java 原生 Channel 进行绑定,
同时通过 Java 原生 Channel 绑定到一个本地地址。
至此,一个 SocketChannel 才算真正建立完成,也就可以接收消息了。
为了照顾大部分同学,我们再回顾下 NIO 编程的大致过程:
1 先是启动 ServerSocketChannel,并注册 Accept 事件;
2 轮询调用 Selector 的 select () 方法,select () 方法还有两个兄弟 ——selectNow (),select (timeout);
3 调用 Selector 的 selectedKeys () 方法,拿到轮询到的 SelectionKey;
4 遍历这些 selectedKeys,处理它们感兴趣到的事件;
5 如果是 Accept 事件,则从 SelectionKey 中取出 ServerSocketChannel,并 accept () 出来一个 SocketChannel;
6 把这个 SocketChannel 也注册到 Selector 上,并注册 Read 事件;
所以,这里我们大胆猜测,Netty 底层接收新连接跟 Java 原生 Channel 是一致的,而且它的 select 是在 NioEventLoop 的 run () 方法中,并在获取到 SelectedKeys 之后,调用了 processSelectedKeys () 方法处理这些 SelectionKey。
至于,我们的猜测对不对呢?打开 NioEventLoop 的 run () 方法看一看:
// io.netty.channel.nio.NioEventLoop#run
@Override
protected void run() {
int selectCnt = 0;
// 死循环,还记得NIO中的死循环吗?
for (;;) {
try {
int strategy;
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
// key1,select()相关的方法
strategy = select(curDeadlineNanos);
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
default:
}
} catch (IOException e) {
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
try {
if (strategy > 0) {
// key2,处理SelectionKey
processSelectedKeys();
}
} finally {
ranTasks = runAllTasks();
}
} else if (strategy > 0) {
final long ioStartTime = System.nanoTime();
try {
// key2,处理SelectionKey
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else {
ranTasks = runAllTasks(0);
}
// 省略其他代码
}
}
processSelectedKeysOptimized 果然
private void processSelectedKeysOptimized() {
// 遍历SelectionKey
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
// 优化GC
selectedKeys.keys[i] = null;
// 取出SelectionKey中的附件,还记得附件是什么吗?
// 上一节,服务启动的时候把Selector、Java原生Channel、Netty的Channel绑定在一起了
// 其中,Netty的Channel是通过attachment绑定到SelectionKey中的
// 所以,针对新连接建立的过程,这里取出来的就是Netty中的NioServerSocketChannel
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 处理之
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
// null out entries in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
processSelectedKey
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
// 省略异常处理等其他代码
try {
int readyOps = k.readyOps();
// 如果是Connect事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// 如果是Write事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 如果是Read事件或者Accept事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
今天的源码剖析基本就讲完了,让我们来总结一下服务接收新连接的过程
1 Netty 中轮询的方法是写在 NioEventLooop 中的;
2 Netty 底层也是通过 Java 原生 ServerSocketChannel 来接收新连接的;
3 Netty 将接收到的 SocketChannel 包装成了 NioSocketChannel,并给它分配 ChannelPipeline 等元素;
4 Netty 中通过 ServerBootstrapAcceptor 这个 ChannelHandler 来初始化 NioSocketChannel 的配置并将其注册到 EventLoop 中;
5 注册到 EventLoop 中同样也会与这个 EventLoop 中的 Selector 绑定,Netty 的 NioSocketChannel 同样地也是以附件的形式绑定在 SelectionKey 中;
至此,一个 SocketChannel 才算真正建立完成,也就可以接收消息了。
通过本节的剖析,我们知道了 Netty 中接收新连接的过程跟 Java 原生 NIO 是同出一辙,或者说是对 Java 原生 NIO 的增强,那么,既然连接已经就绪,如何接收消息呢?