浅谈Netty中ServerBootstrap服务端源码(含bind全流程)
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Netty中ServerBootstrap服务端源码(含bind全流程)相关的知识,希望对你有一定的参考价值。
文章目录
一、梳理Java中NIO代码
Java中的NIO其本质是网络层面定义中的多路复用IO模型(一定要和NIO模型区分开)。NIO代码主要分为下列几步:
- 初始ServerSocketChannel
- 初始化Selector,
- 完成Selector和Channel的绑定,并且注册对应的事件
- 用一个死循环遍历selector监控的事件对应的IO请求
- 处理监控到的对应事件的数据信息
public class 基础Selector和Channel绑定
public static void main(String[] args) throws IOException
// 初始化channel
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(9090));
// 初始化selector,并完成两者的绑定
Selector boss = Selector.open();
SelectionKey selectionKey = channel.register(boss, 0, null);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
while (true)
// selector监听事件,可传入对应的超时时间
boss.select();
// 遍历所有接收到的事件
Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
if (iterator.hasNext())
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable())
// 触发对应事件后,获取channel中的数据
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
// 其他业务处理
上述的NIO代码你可以不需要明白每一步的代码写法,但是你一定要明白每一步的具体含义。如果你对含义都没有弄清楚,那我是不建议你继续向下看的,因为Netty底层的代码逻辑就是完成对上述代码的再次封装。理解了上述代码的执行逻辑,对于后期理解Netty的源码启动流程源码起到事半功倍的效果。
二、Netty服务端代码
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioserverSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>()
@Override
protected void initChannel(NioSocketChannel ch) throws Exception
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter()
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
// xxx
);
)
.bind(8099);
先上一张整体的流程图,仅包含bind后的执行逻辑。在阅读源码的时候,你可以对着这个流程图,一步一步进行比对查看。理解完本文,也就理解了这张流程图。
1、new NioEventLoopGroup()
- MultithreadEventExecutorGroup
在实例化NioEventLoopGroup类的时候,会先去实例化它的父类MultithreadEventLoopGroup,最后会实例到它父类的父类——MultithreadEventExecutorGroup
根据已有的代码,我们不难发现,其底层会创建一个叫children的名字的线程组,其大小为指定的线程数量。并且它还会依次调用newChild方法,完成对线程组的赋值。
补充:上图中的nThreads的值为
nThreads == 0 ?
(Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)))
:
nThreads
- newChild
由于我们是实例化的NioEventLoopGroup类,所以会跳转到NioEventLoopGroup中的newChild方法
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception
EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
- NioEventLoop
实例化NioEventLoop,到这一步我们不难发现一个关联点,那就是我们最开始是实例化一个NioEventLoopGroup,其底层逻辑是在实例化一个一个的NioEventLoop
- TaskQueue 和 selector
调用newTaskQueue(queueFactory)方法,创建一个队列用于后期存放消息
创建selector,这里可以类比Java里NIO中代码的写法,即第一节中第二点初始化Selector(Selector.open()),它们两者底层代码是一样的
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0)
unsafe.read();
2、group
熟悉链式编程的小伙伴应该都知道,主对象的链式方法一般都是一个执行一个赋值操作,真正的对这些数据进行逻辑处理的,都在最后一个方法。基于对链式编程的这种常识性的了解之后,我们对group方法就有一个基本的认识了
group方法主要是完成对this.group和this.childGroup两个参数的赋值
这里需要补充一个额外的知识点:
group和childGroup可以理解为是两组线程池,从名字也能看出来前者是老大,后者是child。老大线程池中的线程专门负责接收客户端的连接,child线程池中的线程专门负责网络的读写。如果我们在编写服务端代码的时候,没有去分别指定对应的EventLoopGroup,那么它们两个就会使用同一个EventLoopGroup。一个EventLoopGroup底层又会去创建很多个NioEventLoop,并且其内部含有对应的Selector…这就是第二章第一节的内容。
调用父类的group方法,完成老大NioEventLoopGroup对象的赋值操作
3、channel
- 获取传入类的构造方法
那这里获取到的无参构造方法必然就是NioServerSocketChannel类里面的构造方法
- 把前面创建出来的无参构造方法封装为一个工厂类,并且完成赋值
4、NioServerSocketChannel.class
第三步的group方法存放了NioServerSocketChannel类的无参构造方法,虽然目前不知道有什么用,但是我们可以肯定的是,这个无参构造方法后面一定会用到,那不然存它干嘛?所以我们来看看它的无参构造方法。
- 调用newSocket方法
在NioServerSocketChannel类的无参构造方法中,第一步就是根据SelectorProvider.provider()这样一个静态常量去new一个Socket。在该方法中,会调用对应的openServerSocketChannel方法。
我们不妨联想一个NIO的代码,我们NIO代码第一步就是初始化ServerSocketChannel,即调用对应的open方法。既然说到Netty是对Java中NIO的封装,你是否能联想到什么?其实下面的newSocket方法完成的逻辑,和最开始的ServerSocketChannel.open方法完成的逻辑是一样,换句话说open方法的底层就是下面的逻辑。好了Java中创建NIO的第一步代码位置找到了
- ch.configureBlocking(false)
点击对应的super方法,就会跟到下面的代码中。类比Java中的NIO代码,这一步就是完成非阻塞状态的设置,同样对应Java中NIO的第一步
- newChannelPipeline()
继续向下点击super方法,我可以发现在实例化的过程中,还会创建出对应的pipeline。即我们可以理解为一个 Channel中包含了一个 ChannelPipeline,而 ChannelPipeline中又维护了一个由ChannelHandlerContext(TailContext和HeadContext实现了ChannelHandlerContext接口)组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
该属性与我们ch.pipeline().addLast(new xxxHandler)相关,此处不再展开,你只需要记住在这里创建了一个这个pipeline对象
5、childHandler
该方法符合我们最初最链式编程的设想,仅仅完成了childHandler这个成员变量的赋值操作。即我们明白真正的核心逻辑在后面的ChannelInitializer类中,这就是我们真正的处理业务逻辑的地方,这一个点我放在最后进行讲解。
6、bind
- doBind方法
链式编程的最后一个方法一定是关键,根据代码逻辑,我们会进到下面的这个doBind方法,一看名字就是重点,该方法中主要的核心方法是initAndRegister和doBind0
- 调用NioServerSocketChannel的无参构造方法
看到channelFactory就可以直接跳转到第二章的第4节,即实例化对应的对象,此时你就可以直接跳转到第二章的第4节,在回忆一遍,看看查看创建了哪些对象
- init方法
紧接着上面的实例化对应的无参构造,紧接着又执行init方法。
在该方法中,它会先去获取我们第4步创建出来的pipeline(ChannelPipeline p = channel.pipeline();),然后向pipeline中添加handler,是否感觉这个代码的逻辑似曾相识,没错,这个和我们自己编写的netty的业务逻辑部分的代码相同,即向pipelin中添加对应的handler处理事件。
- register方法
register方法包含逻辑较多,底层的第一次分岔路口如下图,两条路下面又有分支。故这里的执行逻辑,可直接参考最前面给出了的整体的一个流程图。
- doBind0方法
这里可以类比第一章中Java中NIO的第1点,即channel.bind(new InetSocketAddress(9090)),完成对应端口的绑定
以上是关于浅谈Netty中ServerBootstrap服务端源码(含bind全流程)的主要内容,如果未能解决你的问题,请参考以下文章
学习 java netty -- ServerBootstrap