一、入口
在第二节(Netty简单启动过程)中我们介绍了,netty的简单启动,我们知道端口绑定完成之后,就开始等待来自于客户端的请求,以及处理客户端的请求等一系列操作,现在让我们看看netty是怎么操作的?
当我们回顾第二节中channel注册过程源码时可以发现有下边这段代码。
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
AbstractChannel.this.eventLoop = eventLoop;//这里是把当前的EventLoop赋值给
//当前channel的eventLoop属性
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
}
}
从代码可以看出,channel的注册是通过EventLoop(当前的EventLoop是NioEventLoop的一个实例,具体这个实例是怎么分配的,后续再单独说明,可以知道的一点是当前的EventLoop是通过group来分配的)来实现的,execute方法主要就是往任务队列中添加任务,然后启动线程(如果当前线程没有启动)。回到doBind0方法
private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
//这里的eventLoop就是上边注册方法赋值的
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
});
}
具体的端口绑定也是通过EventLoop来完成,此时继续往任务队列中添加一个端口绑定的任务,线程在注册的时候已经启动了,到这里我们就已经可以大致猜出,接收客户端的请求的入口在哪了。
注册和绑定都是通过EventLoop来完成,说明接收客户端的请求也是在这里完成(在注册的时候,我们知道开启了线程),所以我们先看看EventLoop中的run方法是怎么执行的。
protected void run() {
int selectCnt = 0;
for (;;) {
//省略了绝大部分代码
select(curDeadlineNanos);
processSelectedKeys();
}
}
这里提取整个run方法的关键两行代码,第一行就是我们很熟悉的代码select(curDeadlineNanos);也就是调用底层selector的select方法的入口,第二行就是处理具体请求的方法入口processSelectedKeys(),如果我们degug代码的时候,发现最终都会调用processSelectedKey(k, (AbstractNioChannel) a)这个方法。
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
try {
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();//客户端的accept和读都是走这里
}
}
我们看看read方法,从方法来看主要就是读取信息,然后处理信息。
public void read() {
doReadMessages(readBuf);
pipeline.fireChannelRead(readBuf.get(i));
}
查看doReadMessages方法源码可以知道,当前方法是往readBuf集合中添加NioSocketChannel对象(buf.add(new NioSocketChannel(this, ch)),其中ch就是底层的SocketChannel)。pipeline.fireChannelRead(readBuf.get(i))这个方法就是处理当前集合中的所有元素,这里可以看出是通过pipeline来获取一个handler执行,我们在第三节的时候也简单介绍了这个pipeline,回顾第二节中的初始化方法调用init:
void init(Channel channel) {
ChannelPipeline p = channel.pipeline();
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}
源码可以知道,我们在初始化的时候已经向NioServerSocketChannel的pipeline属性中添加了一个ChannelInitializer的handler,其中其initChannel需要我们自己实现,这个initChannel方法在channelRegistered这个方法和handlerAdded方法中调用,对于NioServerSocketChannel来说,第一次会在channelRegistered方法中调用,我们知道channel注册完成之后就会调用pipeline.fireChannelRegistered()方法。所以当注册结束之后,就会调用上边ChannelInitializer的initChannel方法,把ServerBootstrapAcceptor添加到pipeline中。所以当read方法中调用fireChannelRead方法时,就会直接调用到ServerBootstrapAcceptor中的channelRead方法
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;//上边可知这里的channel
//就是NioSocketChannel
child.pipeline().addLast(childHandler);//这里的childHandler就是
//最开始启动的是自定义添加的handler
setChannelOptions(child, childOptions, logger);//设置子参数
setAttributes(child, childAttrs);//设置子属性
try {
childGroup.register(child).addListener(new ChannelFutureListener() {
//通过子group注册channel,这里的channel注册就很简单,就是执行handler的register和active方法
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
从这里我们可以知道,真正接收客户端数据,就是通过childGroup来实现的,也就是从childGroup中分配的EventLoop来处理,处理逻辑同上。
二、细节
第一部分只是简单介绍了数据接收和处理的入口,相信大部分人还是比较懵,逻辑混乱的感觉。所以这部分主要是厘清第一部分的逻辑。
- 回到第一节(初识Netty)的样例,有这样两个group,这两个group非常的关键,从命名来看一个是主group,一个是工作group,这里我们需要记住有这两个group,并且从bootstrap的group方法可以知道,boss赋值给了AbstractBootstrap中的group属性,work赋值给了ServerBootstrap中的childGroup属性。
EventLoopGroup boss=new NioEventLoopGroup();
EventLoopGroup work=new NioEventLoopGroup();
ServerBootstrap b=new ServerBootstrap();
b.group(boss,work)
我们先不用管其功能,先记住上边这个关系。
- 回顾绑定过程,找出在绑定过程当中用到这两个group的地方
1、在初始化init(channel)方法中,childGroup作为构造参数传入到了ServerBootstrapAcceptor中
2、initAndRegister()调用注册的时候config().group().register(channel),先通过配置config得到group,config().group()返回的是group属性也就是上边的boss参数。查看group的register方法,实际调用next().register(channel).
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
next(),方法返回值是一个EventLoop对象(这就是一个具体的线程对象),如果继续跟踪父类的next方法,就会知道,group维护了一个EventLoop 数组(EventExecutor[] executors,数组的初始化是通过group的来实现的,这个具体的是怎么分配后续单独做说明),group通过一定的算法去做具体的分配,这些都将在后续单独分析group源码的时候再进行详细说明。
public EventLoop next() {
return (EventLoop) super.next();
}
结合第一部分,可知channel的注册和端口的绑定都是通过group属性(也就是boss)分配的同一个EventLoop来实现的,以及接收客户端请求也是在这个实例的run方法中。由此可知AbstractBootstrap中的group属性(也就是boss)会根据一定算法分配一个EventLoop实例来处理channel的注册、端口绑定以及客户端请求。
3、由第一部分可知,处理客户端请求在EventLoop实例的processSelectedKeys()方法中,这个方法最终都会调用到如下方法。
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
final Object a = k.attachment();
//这里是通过附件获取到的channel,注册的时候我们已经分析了
//会把当前的NioServerSocketChannel实例通过附件的形式注册到底层channel上
//所以当前的AbstractNioChannel就是一个NioServerSocketChannel实例
processSelectedKey(k, (AbstractNioChannel) a);
}
}
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
//所以这里的实例同样是NioServerSocketChannel实例,由此可知ch.unsafe()方法返回的是
//new NioMessageUnsafe()实例,这在[第三节](https://blog.csdn.net/solayang/article/details/116641699)中已经分析过了
try {
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0
|| readyOps == 0) {
unsafe.read();//客户端的accept和读都是走这里
}
}
所以这里的unsafe.read()走的是NioMessageUnsafe实例中的read方法,这在第一部分也分析过了,最后会调用ServerBootstrapAcceptor的channelRead方法,这个方法主要做以下几件事:
1)往childChannel中添加自定义的childHandler
2)设置子参数和子属性
3)通过childGroup注册childChannel,这里的注册原理和group的注册原理是一样的,唯一不同的一点是当前的channel是NioSocketChannel实例,然后doRegister方法中底层的channel是SocketChannel而不是ServerSocketChannel。这里要注意的是,childGroup和group一样会分配一个EventLoop实例来处理当前的注册并开启当前线程。
到这里我们先整理一下目前的一个处理流程:
- 从bind()开始,先实例化NioServerSocketChannel,然后初始化channel,并把childGroup作为构造参数赋值给ServerBootstrapAcceptor,然后把这个handler添加的channel的pipeline中
- 注册channel,group分配一个EventLoop实例(并赋值给channel),通过这个实例把当前的channel作为附件注册到底层ServerSocketChannel上(这里就涉及到NIO编程),并开启线程
- 端口绑定,端口绑定也是通过channel的eventLoop属性来实现,最后调用底层的端口绑定的方法。
- 接收客户端请求,客服端请求在EventLoop实例的run方法中,先生成一个NioSocketChannel实例对象,调用ServerBootstrapAcceptor的channelRead方法,通过childGroup注册childChannel,等待客户端发送数据
- 通过自定义的handler处理客户端的数据
所以通过上边的反复分析,可以明确group会分配一个EventLoop实例用来绑定端口以及处理来自当前端口的连接请求。group处理完请求之后,childGroup会分配一个EventLoop来处理客户端的读写事件。
所以netty处理请求的大致逻辑就分析到此。
以上,有任何不对的地方,请指正,敬请谅解。