Netty源码阅读系列-Channel创建

178 阅读5分钟

「我正在参与掘金会员专属活动-源码共读第一期,点击参与

本篇详细分析NioServerSocketChannel的创建时机、创建过程。

通过分析构造函数的执行解读Netty的NioServerSocketChannel对JDK ServerSocketChannel的封装, 对JDK channel做了哪些配置。后面部分还会简单聊一下Netty的IO模型。

为了避免混乱, 本文中带有JDK前缀的channel都表示JDK原始NIO相关的类, 本文出现的NioServerSocketChannel或Netty channel都代表Netty中的类。

一、NioServerSocketChannel是何时创建的

在serverBootStrap执行bind操作前, 执行了serverBootStrap.channel(NioServerSocketChannel.class)方法, 该方法并不是直接将NioServerSocketChannel实例new出来。

而是在serverBootStrap内部创建了一个反射工厂(ReflectiveChannelFactory), 通过该反射工厂可以根据传入的class类型的不同创建不同的channel实例。

image.png

image.png 真正创建channel的时机是在调用serverBootStrap.bind方法后的initAndRegister方法中, 反射工厂通过反射的方式, 创建了NioServerSocketChannel的实例, 通过观察调用栈可以看到这点。

image.png

二、NioServerSocketChannel构造函数分析

观察构造函数调用栈可以看到, 从NioServerSocketChannel无参数的构造函数进入, 层层递进的过程中设置了一些默认值, 并做了一些初始化的操作:

  1. 使用DEFAULT_SELECTOR_PROVIDER作为默认的SelectorProvider, 该类用于创建JDK的ServerSocketChannel。

image.png

image.png

  1. 调用NioServerSocketChannel.newChannel方法, 在这里Netty屏蔽了一处JDK的版本差异, 先通过反射查找SelectorProvider在JDK15及以上版本的openServerSocketChannel方法, 该方法需要一个java.net.ProtocolFamily类型的入参, 若存在则使用反射调用该方法。若Java版本低于15, 则直接调用provider.openServerSocketChannel()。

image.png

image.png

  1. 调用父类AbstractNioMessageChannel的构造方法, 注意这里传入的第三个参数SelectionKey.OP_ACCEPT, 这个channel需要关注accept事件。

image.png

  1. AbstractNioMessageChannel调用父类AbstractNioChannel的构造函数。AbstractNioChannel调用父类AbstractNioChannel的构造函数。在AbstractChannel构造函中, 该Netty channel初始化了id、unsafe和pipeline几个成员变量。

image.png

image.png

image.png

  1. 在AbstractNioChannel中, 将JDKserverSocketChannel的阻塞模式设置为非阻塞

image.png

  1. 创建一个NioServerSocketChannelConfig实例, 通过该实例可以对底层的JDk channel进行配置。

image.png

三、为什么将NioServerSocketChannel设置为非阻塞

1.几种IO模型的差异

这里简单聊一下IO模型的差异, 对我们理解Netty源码是有帮助的。

谈论IO模型的差异, 要将读socket分为两阶段:

  • 数据准备阶段 在这一阶段, 等待数据到达

  • 数据拷贝阶段 在这一阶段, 已有数据到达, 但要从内核空间拷贝到进程

(1) 阻塞、非阻塞

我们讨论【阻塞】、【非阻塞】, 都是针对数据准备阶段的。

阻塞

如果IO模型是阻塞的, 那么调用读方法时, 会一直阻塞到数据到来。

Java中传统的BIO就是阻塞IO,当有很多的客户端连接到服务器时, 由于对每个Socket的读操作都有机会进入阻塞状态, 在一个线程中无法完成对多个socket的处理, 服务器要为每一个Socket分配独立线程。

线程分配的太多会消耗很多内存资源, 并且线程上下文切换会造成很大的CPU开销,这也是传统BIO的缺点。

非阻塞

如果IO模型时非阻塞的, 在调用读方法时, 如果没有数据可读会立即返回, 不会阻塞线程。

但是为了即时的读取数据, 线程要不停的询问Socket是否有数据到达。

我们可以将在一个线程中对多个Socket进行轮询(这里可以先不考虑使用IO多路复用器), 这样就达到了节省线程资源消耗的目的, 这也是非阻塞IO的优点。

(2) 同步、异步

谈论【同步】、【异步】时针对的是数据拷贝阶段。

同步

如果IO模型是同步的, 在调用读方法时, 等待数据从内核空间拷贝到用户空间。

异步

如果IO模型是异步的, 准备好的数据会以回调的方式返回给应用。在应用的线程中不会感受到等待数据拷贝的耗时。

2.Netty的IO模型是什么

JDK的NIO是 同步非阻塞IO

Netty是对JDK NIO的包装与增强, Netty的IO模型是 同步非阻塞IO

Netty之所以未使用异步非阻塞IO(AIO)实现, 有一种说法是在linux系统上AIO的底层同NIO一样使用epoll实现, 在性能方面并没有体现出更大的优势。

3.答案自然揭晓

读到这里其实已经知道为什么要将Netty的ServerSocketChannel设置为非阻塞模式了, 如果使用阻塞模式, 就和传统BIO一样要启用大量的线程去处理SocketChannel的读写操作, 这和Netty的设计(Reactor线程模型)是不符的。

四、本篇总结

  • 通过阅读源码可以看到, NioServerSocketChannel创建过程中多处使用了反射的技术(反射工厂、屏蔽JDK版本差异), 给代码增加了灵活性。

  • Netty中NioServerSocketChannel的创建是将JDK的ServerScoketChannel包装了一层, 目的是增强能力和易用性。

  • 本文还探讨了IO模型, 【阻塞非阻塞】是针对【数据准备阶段】, 【同步异步】是针对数【据拷贝阶段】。

  • Netty使用的IO模型是同步非阻塞, 优点是不需要为每一个SocketChannel创建独立的线程以节约线程资源, 所以Netty创建NioServerSocketChannel的实例时将底层的JDK ServerSocketChannel设置为非阻塞模式。