首页 > 编程语言 >浅谈Netty中ServerBootstrap服务端源码(含bind全流程)

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

时间:2023-01-18 22:34:26浏览次数:44  
标签:Netty pipeline 浅谈 代码 源码 线程 channel 方法 服务端


文章目录

  • ​​一、梳理Java中NIO代码​​
  • ​​二、Netty服务端代码​​
  • ​​1、new NioEventLoopGroup()​​
  • ​​2、group​​
  • ​​3、channel​​
  • ​​4、NioServerSocketChannel.class​​
  • ​​5、childHandler​​
  • ​​6、bind(重点)​​




一、梳理Java中NIO代码

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

  1. 初始ServerSocketChannel
  2. 初始化Selector,
  3. 完成Selector和Channel的绑定,并且注册对应的事件
  4. 用一个死循环遍历selector监控的事件对应的IO请求
  5. 处理监控到的对应事件的数据信息
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后的执行逻辑。在阅读源码的时候,你可以对着这个流程图,一步一步进行比对查看。理解完本文,也就理解了这张流程图。

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




1、new NioEventLoopGroup()

  1. MultithreadEventExecutorGroup

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



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

根据已有的代码,我们不难发现,其底层会创建一个叫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



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


  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…这就是第二章第一节的内容。



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


调用父类的group方法,完成老大NioEventLoopGroup对象的赋值操作



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


3、channel

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

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



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


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



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


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的第一步代码位置找到了



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


  1. ch.configureBlocking(false)

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



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


  1. newChannelPipeline()

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

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



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


5、childHandler

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



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


6、bind(重点)

  1. doBind方法

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



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


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

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



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


  1. init方法

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

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



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


  1. register方法(重点)

register方法包含逻辑较多。详细的代码执行流程,可直接参考最前面给出了的整体的一个流程图。接下来我只对流程图中几个关键的节点进行说明。(流程图中特殊颜色标记的位置分别对应下面的讲解代码位置)

1)
这里有两个方法一个execute方法还有一个register0。execute方法可以理解为是和客户端连接的入口,在该方法中会使用死循环去不断的select获取数据,继而达到监听客户端发送给服务端的事件信息,最终完成客户端和服务端之间的交互(该方法可以一直跟到,客户端和服务端事件的交互位置)。register0方法可以理解为将服务端的select和channel的绑定,然后回调pipeline中的initChannel方法,最终处理服务端添加到pipeline中handler中的处理逻辑。



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


2)
这个就是前面提到的会回调pipeline中所有handler的方法入口,底下再调用callHandlerAddedForAllHandlers,然后再顺着双向链表依次调用,代码比较负责,这里不再进行说明



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


3)
在1)中有说明过,在execute方法底下,会调用到如下的代码位置。该代码位于一段死循环内部,也可以理解为是不断的轮询。触发该方法有两个事件场景,SelectionKey.OP_READ和SelectionKey.OP_ACCEPT,即对应的READ事件和ACCEPT事件。当客户端和Netty服务端建立连接之后,就会触发这个连接事件;当客户端发送数据给服务端的时候,会触发READ事件。两个事件的不同,会导致调用的read方法的实现类不一样。

这个read方法有不同的实现类,分别是NioByteUnsafe类和NioMessageUnsafe类。前者是用于处理READ读取消息事件,后者用于ACCEPT连接事件。



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


4)
当服务端启动之后,老大线程组会利用线程去完成服务端的注册,即你前面看到的1)2)3)的代码,这些都是老大线程组里面的线程干的。如果客户端发起了和服务端的连接信息,那么老大线程组的线程就会触发3)中的read方法,然后调用到下图中的代码。该代码可以类比服务端注册NioServerSocketChannel这个类的初始化流程,这里是直接实例化这个对象。

同样类比NioServerSocketChannel这个类,在实例化NioSocketChannel这个方法过程中也会实例化出对应的ChannelPileline(内部为一个含有头尾节点的双向链表,可以不断增加handler)



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


5)
前面第4)步中说到会实例化NioSocketChannel对象,即下图中的第一个框。实例化完对应的对象后,紧接着会调用fireChannelRead方法。在Netty中,但凡看到fire方法开头的,你第一反应就是回调pipeline中的所有的channelRead(去掉fire前缀后的名字)方法。根据我们前面提到的流程图,此时的pipeline肯定是服务端的pipeline,并且pipeline中的类是ServerBootstrapAcceptor对象,即就会回调到这个类的channelRead方法,即出现了下面的第二幅图片中的代码位置



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

这个channelRead方法至关重要,能否正确理解这个方法,就直接决定了你能否理解Netty客户端和服务端之间的连接绑定。明确我们代码的位置,老大线程组中的一个线程,在死循环中接收到了ACCEPT事件,然后会回调server中pipeline中的这个方法。

这里需要明确childGroup和child这两个参数的含义。我们在很早的时候就有说过,我们在初始化服务端的NioEventLoopGroup的时候会初始化两个线程组,第一个就是老大线程组,第二个就是儿子线程组,这里就使用到了这个儿子线程组(如果不指定儿子线程组,儿子线程组就和老大线程组是一样的两个线程组)。child参数为调用这个方法传过来的msg,也即是上一幅图的readBuf.get(i)的值,而再往前走,这个readBuf就是第4)步中的那个含有实例化出NioSocketChannel的list集合。那么这个child就是客户端的channel。

明白了这两个参数的含义,这个方法就可以描述为,当客户端和服务端发生连接的时候,服务端老大线程组中的一个线程,就会负责将要进行连接的客户端中的channel和服务端初始化过程中的另一个线程组(儿子线程组)进行绑定。完成绑定后,已经连接好的客户端发送数据给服务端后,就将由这个儿子线程组分配线程进行消息的处理



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


在大概理顺了上面的逻辑之后,你应该就能明白下面这张图片的流程。(如果不明白,建议多看几遍开头的那张流程图,我觉得我画的很细了)

  1. 客户端发送消息给服务端
  2. 服务端会初始化两个线程组,即Boss Group线程组(老大线程组)和Worker Group线程组(儿子线程组)
  3. Boss Group(老大线程组)中的线程会获取监听事件
  4. 当Boss Group(老大线程组)中的线程监听到ACCEPT事件之后,就会为连接过来的客户端初始化一个SocketChannel
  5. 然后将这个SocketChannel交由Worker Group(儿子线程组),完成两者之间的一个绑定关系
  6. 后续客户端发送消息给服务端之后,Worker Group(儿子线程组)就会分配对应的线程去处理对象的消息,此时就会回调channel中添加的所有handler,最终完成两者之间的数据交互


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


  1. doBind0方法

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



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


标签:Netty,pipeline,浅谈,代码,源码,线程,channel,方法,服务端
From: https://blog.51cto.com/u_15942107/6019536

相关文章

  • 浅谈Redisson底层源码
    Redisson源码分析​​一、加锁时使用lua表达式,执行添加key并设置过期时间​​​​二、加锁成功之后给锁添加对应的事件​​​​三、加锁完成,看门狗自动续命未处理完的线程​......
  • drf快速使用 CBV源码分析 drf之APIView分析 drf之Request对象分析
     目录序列化和反序列化drf介绍和安装使用原生django写接口djangoDRF安装drf快速使用模型序列化类视图路由datagrip使用postman测试接口CBV源码分......
  • 浅谈三种使用Redis实现MQ的方式
    文章目录​​一、消息队列​​​​二、基于List的消息队列​​​​三、基于PubSub的消息队列​​​​四、基于Stream的消息队列​​​​1、基本命令​​​​2、简单使用​​......
  • 浅谈如何使用Redis实现分布式锁
    文章目录​​一、基础版(含自动释放锁)​​​​二、改良版(含过期时间)​​​​三、进阶版(含唯一性验证)​​​​四、单节点版(含Redisson)​​​​五、多节点版(含RedLock)​​写在......
  • 浅谈Redis底层数据结构(sdshdr-redisObject)
    最近看了点Redis底层的源码分析,特作此记录前提共识:Redis是一个默认为16个数据库的key-value内存数据库Redis底层是由C语言实现文章目录​​C语言源码流程​​​​1、server.......
  • 浅谈Zookeeper集群选举Leader节点源码
    写在前面:zookeeper源码比较复杂,本文讲解的重点为各个zookeeper服务节点之间的state选举。至于各个节点之间的数据同步,不在文本的侧重讲解范围内。在没有对zookeeper组件有一......
  • 浅谈Redis基本数据类型底层编码(含C源码)
    文章目录​​一、String​​​​1、int​​​​2、embstr​​​​3、raw​​​​4、bitmap​​​​5、hyperloglog​​​​二、List​​​​1、ziplist​​​​2、quicklist......
  • 浅谈如何设计MySQL索引
    文章目录​​一、索引的代价​​​​二、如何设计索引​​​​1、索引列的类型尽量小​​​​2、索引的选择离散性高的​​​​3、只为用于搜索、排序或分组的列创建索引​......
  • STM32 PLC底层源码 FX2N源码 断电保持/Keil源码
    STM32PLC底层源码FX2N源码断电保持/Keil源码三菱指令编码注释较多,适合初学者,发编译环境:KeilMDK4.7以上的版本,CPU需要:STM32F103--RAM内存不小64K,Flash程序空间不小于256......
  • JS 前端大文件上传源码
    ​对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程......