浅谈Netty中ServerBootstrap服务端源码(含bind全流程)

Posted 默辨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Netty中ServerBootstrap服务端源码(含bind全流程)相关的知识,希望对你有一定的参考价值。

文章目录



一、梳理Java中NIO代码

Java中的NIO其本质是网络层面定义中的多路复用IO模型(一定要和NIO模型区分开)。NIO代码主要分为下列几步:

  1. 初始ServerSocketChannel
  2. 初始化Selector,
  3. 完成Selector和Channel的绑定,并且注册对应的事件
  4. 用一个死循环遍历selector监控的事件对应的IO请求
  5. 处理监控到的对应事件的数据信息
public class 基础SelectorChannel绑定 
	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()

  1. MultithreadEventExecutorGroup

在实例化NioEventLoopGroup类的时候,会先去实例化它的父类MultithreadEventLoopGroup,最后会实例到它父类的父类——MultithreadEventExecutorGroup

根据已有的代码,我们不难发现,其底层会创建一个叫children的名字的线程组,其大小为指定的线程数量。并且它还会依次调用newChild方法,完成对线程组的赋值。

补充:上图中的nThreads的值为

nThreads == 0 ? 
(Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2))) 
:
nThreads



  1. 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);



  1. NioEventLoop

实例化NioEventLoop,到这一步我们不难发现一个关联点,那就是我们最开始是实例化一个NioEventLoopGroup,其底层逻辑是在实例化一个一个的NioEventLoop



  1. 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

  1. 获取传入类的构造方法

那这里获取到的无参构造方法必然就是NioServerSocketChannel类里面的构造方法



  1. 把前面创建出来的无参构造方法封装为一个工厂类,并且完成赋值




4、NioServerSocketChannel.class

第三步的group方法存放了NioServerSocketChannel类的无参构造方法,虽然目前不知道有什么用,但是我们可以肯定的是,这个无参构造方法后面一定会用到,那不然存它干嘛?所以我们来看看它的无参构造方法。

  1. 调用newSocket方法

在NioServerSocketChannel类的无参构造方法中,第一步就是根据SelectorProvider.provider()这样一个静态常量去new一个Socket。在该方法中,会调用对应的openServerSocketChannel方法。

我们不妨联想一个NIO的代码,我们NIO代码第一步就是初始化ServerSocketChannel,即调用对应的open方法。既然说到Netty是对Java中NIO的封装,你是否能联想到什么?其实下面的newSocket方法完成的逻辑,和最开始的ServerSocketChannel.open方法完成的逻辑是一样,换句话说open方法的底层就是下面的逻辑。好了Java中创建NIO的第一步代码位置找到了



  1. ch.configureBlocking(false)

点击对应的super方法,就会跟到下面的代码中。类比Java中的NIO代码,这一步就是完成非阻塞状态的设置,同样对应Java中NIO的第一步



  1. newChannelPipeline()

继续向下点击super方法,我可以发现在实例化的过程中,还会创建出对应的pipeline。即我们可以理解为一个 Channel中包含了一个 ChannelPipeline,而 ChannelPipeline中又维护了一个由ChannelHandlerContext(TailContext和HeadContext实现了ChannelHandlerContext接口)组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。

该属性与我们ch.pipeline().addLast(new xxxHandler)相关,此处不再展开,你只需要记住在这里创建了一个这个pipeline对象




5、childHandler

该方法符合我们最初最链式编程的设想,仅仅完成了childHandler这个成员变量的赋值操作。即我们明白真正的核心逻辑在后面的ChannelInitializer类中,这就是我们真正的处理业务逻辑的地方,这一个点我放在最后进行讲解。




6、bind

  1. doBind方法

链式编程的最后一个方法一定是关键,根据代码逻辑,我们会进到下面的这个doBind方法,一看名字就是重点,该方法中主要的核心方法是initAndRegister和doBind0



  1. 调用NioServerSocketChannel的无参构造方法

看到channelFactory就可以直接跳转到第二章的第4节,即实例化对应的对象,此时你就可以直接跳转到第二章的第4节,在回忆一遍,看看查看创建了哪些对象



  1. init方法

紧接着上面的实例化对应的无参构造,紧接着又执行init方法。

在该方法中,它会先去获取我们第4步创建出来的pipeline(ChannelPipeline p = channel.pipeline();),然后向pipeline中添加handler,是否感觉这个代码的逻辑似曾相识,没错,这个和我们自己编写的netty的业务逻辑部分的代码相同,即向pipelin中添加对应的handler处理事件。



  1. register方法

register方法包含逻辑较多,底层的第一次分岔路口如下图,两条路下面又有分支。故这里的执行逻辑,可直接参考最前面给出了的整体的一个流程图。



  1. doBind0方法

这里可以类比第一章中Java中NIO的第1点,即channel.bind(new InetSocketAddress(9090)),完成对应端口的绑定

以上是关于浅谈Netty中ServerBootstrap服务端源码(含bind全流程)的主要内容,如果未能解决你的问题,请参考以下文章

Netty搭建TCP服务实践

学习 java netty -- ServerBootstrap

学习 java netty -- ServerBootstrap

转netty线程模型

netty的好处

Netty 核心源码解读 —— ServerBootstrap 篇