Netty学习笔记
Netty源码篇
简述NIO
- 由于 Netty 的底层是 NIO,所以在读 Netty 源码之前,首先了解一下 NIO 网络编程相关知识。
NIO简介
- NIO:New IO、Non-blocking IO,是 JDK1.4 中引入的一种新的 IO 标准,是一种同步非阻塞 IO。NIO 是以块(Buffer)为单位进行数据处理的,当然 Block 的大小是程序员自己指定的。其相对于 BIO 的以字节/字符为单位所进行的阻塞式处理方式,大大提高了读写效率与并发度。
- BIO:Blocking IO,同步阻塞IO
- NIO:Non-blocking IO,同步非阻塞IO JDK1.4
- AIO:异步非阻塞IO,也称NIO2.0 JDK1.7
NIO通信
- 这里使用 NIO 实现一个简单的 C/S 通信:Client 向 Server 发送一个数据,显示在 Server端控制台。
- 定义服务端:
public class NioServer {
public static void main(String[] args) throws Exception {
// 创建一个服务端Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 指定channel采用的是非阻塞模式
serverChannel.configureBlocking(false);
// 指定要监听的端口
serverChannel.bind(new InetSocketAddress(8888));
// 创建一个多路复用器Selector
Selector selector = Selector.open();
// 将channel注册到selector,并告诉selector让其监听“接收Client连接事件”
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// select()是一个阻塞方法,这里设置会阻塞1秒
// 若阻塞时间到了,或在阻塞期间有channel就绪,都会打破阻塞
if (selector.select(1000) == 0) {
System.out.println("当前没有就绪的channel");
continue;
}
// 获取所有就绪channel的key
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历所有就绪的key
for (SelectionKey key : selectionKeys) {
// 若当前的key是OP_ACCEPT,则说明当前channel是可以接收客户端连接的
if (key.isAcceptable()) {
System.out.println("接收到Client的连接");
// 获取连接到Server的客户端channel,其是客户端channel在Server端的代表
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 将客户端channel注册到selector,并告诉selector让其监听这个channel中是否发送读事件
clientChannel.register(selector, SelectionKey.OP_READ);
}
// 若当前的key是OP_READ,则说明当前channel中有客户端发送来的数据
if (key.isReadable()) {
// 创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 根据key获取其对应的channel
SocketChannel clientChannel = (SocketChannel) key.channel();
// 把channel中的数据读取到buffer
clientChannel.read(byteBuffer);
}
// 删除当前处理过的key,以免重复处理
selectionKeys.remove(key);
} // end-for
}
}
}
- 定义客户端:
public class NioClient {
public static void main(String[] args) throws Exception {
// 创建客户端channel
SocketChannel clientChannel = SocketChannel.open();
// 指定channel使用非阻塞模式
clientChannel.configureBlocking(false);
// 指定要连接的Server地址
InetSocketAddress serverAddr = new InetSocketAddress("localhost", 8888);
// 连接Server
if (!clientChannel.connect(serverAddr)) { // 首次连接
while (!clientChannel.finishConnect()) { // 完成重连
continue;
}
}
if (clientChannel.isConnected()) {
// 将消息写入到channel
clientChannel.write(ByteBuffer.wrap("hello".getBytes()));
System.out.println("Client 消息已发送");
}
System.in.read();
}
}
NIO实现群聊功能
- 该工程实现的功能是:只要有 Client 启动、发送消息,及下线,都会广播给所有其它 Client 通知。
- 定义服务端启动类:
public class NioChatServerStarter {
public static void main(String[] args) throws Exception {
// 创建一个服务端Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 指定channel采用的是非阻塞模式
serverChannel.configureBlocking(false);
// 指定要监听的端口
serverChannel.bind(new InetSocketAddress(8888));
// 创建一个多路复用器Selector
Selector selector = Selector.open();
// 将channel注册到selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 创建支持群聊的NIO Server
NioChatServer chatServer = new NioChatServer();
chatServer.start(serverChannel, selector);
}
}
- 定义服务端Server类:
public class NioChatServer {
/**
* 开启Server的群聊功能
*/
public void start(ServerSocketChannel serverChannel, Selector selector) throws Exception {
System.out.println("Chat Server Started.");
do {
if (selector.select(1000) == 0) {
System.out.println("等待连接...");
continue;
}
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理客户端上线
if (key.isAcceptable()) {
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
String msg = clientChannel.getRemoteAddress() + "-上线了";
// 将上线通知广播给所有在线的其它client
sendToOtherOnlineClient(selector, clientChannel, msg);
}
// 处理客户端发送消息
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer);
String msgFromClient = new String(buffer.array()).trim();
if (msgFromClient.length() > 0) {
// 获取到客户端地址
SocketAddress clientAddr = clientChannel.getRemoteAddress();
String msgToSend = clientAddr + " say:" + msgFromClient;
if ("88".equals(msgFromClient)) {
msgToSend = clientAddr + "下线";
// 取消当前key,即放弃其所对应的channel,将其对应的channel从selector中去掉
key.cancel();
}
// 将client消息广播给所有在线的其它client
sendToOtherOnlineClient(selector, clientChannel, msgToSend);
}
}
// 一般不用下面这两个事件,因为其不是外部条件,是自己的主动行为
// if (key.isWritable()){}
// if (key.isConnectable()){}
// 删除当key,防止重复处理
keyIterator.remove();
}
} while (true);
}
private void sendToOtherOnlineClient(Selector selector, SocketChannel self, String msg) throws IOException {
// 遍历所有注册到selector的channel,即所有在线的client
for (SelectionKey key : selector.keys()) {
SelectableChannel channel = key.channel();
// 将消息发送给所有其它client
if (channel instanceof SocketChannel && channel != self) {
((SocketChannel) channel).write(ByteBuffer.wrap(msg.trim().getBytes()));
}
}
}
}
- 定义客户端启动类:
public class NioChatClientStarter {
public static void main(String[] args) throws Exception {
// 创建客户端channel
SocketChannel clientChannel = SocketChannel.open();
// 指定channel使用非阻塞模式
clientChannel.configureBlocking(false);
// 指定要连接的Server地址
InetSocketAddress serverAddr = new InetSocketAddress("localhost", 8888);
// 连接Server
if (!clientChannel.connect(serverAddr)) { // 首次连接
while (!clientChannel.finishConnect()) { // 完成重连
System.out.println("连接不上server,正在尝试连接中。。。");
continue;
}
}
// 创建群聊客户端,启动聊天功能
NioChatClient chatClient = new NioChatClient();
chatClient.start(clientChannel);
}
}
- 定义客户端Client类:
public class NioChatClient {
/**
* 开启Client的群聊功能
*/
public void start(SocketChannel clientChannel) throws Exception {
// 获取client自己的地址
SocketAddress selfAddr = clientChannel.getLocalAddress();
System.out.println(selfAddr + ",你已经成功上线了");
// 创建一个线程用于不间断地接收来自于Server的消息
new Thread(() -> {
do {
try {
if (!clientChannel.isConnected()) {
return;
}
receiveMsgFromServer(clientChannel);
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
System.out.println(e.getMessage());
}
} while (true);
}).start();
/**
* 注意,该方法不能写到前面的创建线程之前,这样会导致无法接收到来自于Server的消息,
* 因为该方法中的Scanner是阻塞的向server发送消息
*/
sendMsgToServer(clientChannel);
}
private void receiveMsgFromServer(SocketChannel clientChannel) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer);
String msg = new String(buffer.array()).trim();
if (msg.length() > 0) {
System.out.println(msg);
}
}
private void sendMsgToServer(SocketChannel clientChannel) throws Exception {
// 接收来自于键盘的输入
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
// 将消息写入到channel,其中有可能是下线请求消息88
clientChannel.write(ByteBuffer.wrap(msg.trim().getBytes()));
// 若消息为88,则表示当前client要下线,则将该channel关闭
if ("88".equals(msg.trim())) {
// 关闭客户端
clientChannel.close();
return;
}
}
}
}
Reactor/Proactor模型
- 在解析 Netty 源码之前,我们首先要搞清楚 Reactor 模型。因为现在的网络通信框架,大多数都是基于 Reactor 模型进行设计和开发的,Netty 也不例外。
Reactor 单线程模型
- Reactor 单线程模型,指的是当前的 Sever 会为每一个通信客户端形成一个 Channel,而所有这些 Channel 都会与一个线程相绑定,该线程用于完成它们间的所有通信处理。
- 该线程需要完成的操作有:
- 若当前为 Server,则该线程需要接收并处理 Client 的连接请求
- 若当前为 Client,则该线程需要向 Server 发起连接
- 读取通信对端的消息
- 向通信对端发送消息
Reactor 线程池模型
- Reactor 单线程模型中使用一个线程处理所有通信对端的所有请求,在高并发场景中会严重影响系统性能。所以,就将单线程模型中的这一个线程替换为了一个线程池。大大提高了系统性能。
Reactor 多线程池模型
- 若请求连接的并发量是数以百万计的,且 IO 操作还比较耗时,此时的 Server 即使采用的是 Reactor 线程池模型,系统性能也会急剧下降。此时,可以将连接操作与 IO 操作分开处理,形成 Reactor 的多线程模型。
- 当客户端通过处理连接请求的 Channel 连接上 Server 后,系统会为该客户端再生成一个子 Channel 专门用于处理该客户端的 IO 请求。这两类不同的 Channel 连接着两类不同的线程池。而线程池中的线程数量,可以根据需求分别设置。提高了系统性能。
Netty-Server 的 Reactor 模型
- Netty-Server 采用了多线程模型。不过线程池是由 EventLoopGroup 充当。EventLoopGroup中的每一个 EventLoop 都绑定着一个线程,用于处理该 Channel 与当前 Server 间的操作。一个 Channel 只能与一个 EventLoop 绑定,但一个 EventLoop 可以绑定多个 Channel。即 Channel 与 EventLoop 间的关系是 n:1。
Netty-Client 的 Reactor 模型
- Netty-Client 采用的是线程池模型。因为其只需要与 Server 连接一次即可,无需区分连接请求与 IO 请求。
Proactor 模型
- 在高性能的网络通信设计中,有两个比较著名的网络通信模型 Reactor 和 Proactor 模式,其中 Reactor 模式属于同步非阻塞 I/O 的网络通信模型,而 Proactor 运属于异步非阻塞 I/O的网络通信模型。
IO模型
概念
- 用户空间
- 内核空间
- IO中的同步/异步
- IO中的阻塞/非阻塞
四种IO模型
- 同步非阻塞IO模型:两个任务由同一个线程完成,IO调用逻辑与IO执行逻辑由同一个线程完成,它们的执行时串行的,即同步的。此时线程一直在做while(true)查看user buffer中的数据是否就绪。若就绪,则执行IO调用后的语句;若没有就绪,则继续while(true)。整个过程线程一直处于运行状态,没有发生阻塞。
- 同步阻塞IO模型:两个任务由同一个线程完成,IO调用逻辑与IO执行逻辑由同一个线程完成,它们的执行时串行的,即同步的。此时该线程首先向user buffer注册监听,监听其数据是否就绪,然后哦阻塞自己。若数据就绪,则唤醒阻塞的线程,继续执行IO调用后的语句。
- 异步非阻塞IO模型:两个任务由两个不同的线程完成,在IO执行线程执行期间,IO调用后的语句也是同时执行。只不过IO调用线程会为IO执行线程添加一个异步监听,监听IO操作是否结束。若结束,则再处理“IO调用后的语句”与Future回调语句的优先级问题:是执行完毕IO调用语句再执行Future回调,还是先把IO调用后语句挂起,然后马上执行Future回调,回调执行完毕后再继续执行IO调用后语句。
- 异步阻塞IO模型:两个任务由两个不同的线程完成,在IO执调用线程调用了IO执行线程后,其首先向这个IO执行线程的Future添加一个监听,监听其IO是否执行完毕,然后阻塞自己。若执行完毕,则唤醒阻塞的IO调用线程。
Reactor 模型的 IO
- 对于 Reactor 模型,其 IO 属于同步非阻塞 IO。下面仍以 channel 发起读操作请求为例来分析整个执行过程。
- 当 channel 的执行线程发起了 read()调用后,其会向 selector 注册了 OPS_READ 事件,然后该线程会不停的查看该事件是否就绪。
- 当 selector 接收到这个注册后,其就会不停的查看该 channel 所关联的网卡缓存中是否具有了数据。当 selector 轮询到该 channel 的网卡缓存中具有了数据后,该读操作就绪。
- 此时该线程就查看到了就绪,会发起 system call,将网卡缓存中的数据读取到 user buffer 中。这些操作完成后,会再执行 read()后面的逻辑。整个执行过程,该线程未发生阻塞。所以 Reactor 模型是“同步非阻塞 IO”模型。
Proactor 模型的 IO
- 对于 Proactor 模型,其 IO 属于异步非阻塞 IO。下面仍以 channel 发起读操作请求为例来分析整个执行过程。
- 当 channel 的执行线程调用了异步的 read()操作后,其会继续执行 read()后的逻辑。而该 read()会将本次操作注册到一个 Proactor 实例中,注册本次操作关注的事件为 Read Complete。
- 一个 channel 的所有 IO 操作共享一个 Proactor 实例。或者说,一个 Proactor 实例处理同一 channel 中所有的 IO 请求。这个 Proactor 实例是在 channel 创建时完成的初始化。每个Proactor 实例具有一个绑定的线程,用于执行相关 IO 操作。
- 当 Proactor 实例接收了 read()操作的注册后,其会为网络缓存注册一个监听。若网卡缓存中有了数据,则马上通过 DMA 控制将数据写入到 user buffer 中。一旦数据写入 user buffer 完成,则该 IO 操作完毕,产生 Read Complete 事件,此时会将该事件写入到 Proactor 所维的一个队列。Proactor 实例会将队列中的事件发送给各个 IO 调用者线程,以使他们触发相应的回调。
- 当前 read()操作的调用线程无需阻塞等待 read()操作的完成,而是直接执行后面的逻辑。由于 read()操作本身是由另外一个线程来执行,所以 Proactor 模型是“异步非阻塞 IO”模型。
- Reactor 中的 selector 是一种“事件分离器”的实现。在 Proactor 中不存在事件分离器,但存在一个 Proactor 实例。该实例会根据不同的 IO 操作,监听不同的内容。例如,本例为网卡缓存注册了监听。
Proactor 优缺点
- Proactor 在处理高耗时 IO 时的性能要高于 Reactor,但对于低耗时 IO 的执行效率提升并不明显。因为 Reactor 的同步性(IO 也是调用线程来处理)导致其处理高耗时 IO 时性能会极低(IO 操作的后续逻辑无法执行)。但对于低耗时 IO,异步也不会比同步快太多。同时异步还需要线程切换等操作,会使异步的优越性无法体现。
- Proactor 的异步性使其并发处理能力要强于 Reactor。
- Proactor 的实现逻辑复杂,编码成本较 Reactor 要高很多。
- Proactor 的异步依赖于操作系统对于异步的支持。若操作系统对异步的支持不好,Proactor 的性能还不如 Reactor。
Netty 与 Proactor
- Netty4 是 NIO 的,其网络通信模型采用的是 Reactor。
- Netty5 是 AIO 的,其网络通信模型采用的是 Proactor。但该版本已经被不再维护。主要原因还是 Linux 目前对于异步的支持不完善,导致其执行效率很低。
Proactor 与 Epoll
Proactor 与 Epoll 是没有可比性的。
- Epoll:是“事件分离器”对就绪事件的发现方式,有 select、poll 与 epoll 三种方式。epoll采用的是回调方式,而不是轮询方式。Reactor 模型中具有“事件分离器”。
- Proactor:是一种网络通信模型,该模型中就不存在“事件分离器”。
Netty 中的 Epoll 多路复用器
以下内容来自于从 Google 查询到的国外的一篇文章,由于没有对 Netty 中的 epoll 模型进行深入研究,所以仅将看到的内容告诉大家,不做进一步讨论。感兴趣的可以自己研究。
epoll 的效率在如下场景中并不一定比 poll 的高。
- 当出现大批量的读/写事件切换时,epoll 的效率会远远低于 poll。因为 epoll 需要进行大量的用户空间到内核空间的切换,而 poll 仅需要在用户空间做简单的位运算即可完成。
- 若Client与Server端有大量的仅用于传递少量数据的短连接,则epoll的效率要低于poll。因为 epoll 下的每个 socket 连接都需要发生两次用户空间与内核空间的转换,而 poll 不需要。
- epoll 完全属于 Linux,虽然其它系统平台也有 epoll 的支持,但并不完全相同。
- 高性能处理的代码编写逻辑 epoll 要比 poll 更复杂,更难调试。特别是边缘触发。如果错过额外的读/写操作,很容易导致死锁。
导入Netty框架源码
Netty 框架中导入外部工程
- Netty 框架源码是直接以 maven 项目的形式导入到 Idea 中的。导入后,在该项目中以model 的形式导入我们自己定义的工程。
- 然后,将自己工程中的 pom 文件删除,替换为 netty-all 模块中的 pom。当然,若原来的 pom 中还有其它依赖,需要将其实依赖导入到当前这个新的 pom 中。然后,修改 artifactId。
添加插件
- 在 netty-common 模板的 pom 文件中添加如下插件配置。
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
重新编译
- 进入到 netty 框架的 netty-common 模板根目录中执行 mvn compile 命令,对该模块进行重新编译。注意,要跳过风格检测。
mvn compile -Dcheckstyle.skip=true
预备知识
Server 端启动的总体分析
- 服务端的启动总体由三部分构成:
- EventLoopGroup 的创建与初始化
- ServerBootstrap 的创建与初始化
- 任务的执行
NioEventLoop是什么
- 是一个 EventExecutor
- 是一个 Executor:这个Executor中的execute方法,是使用新建线程这种方式去执行command任务的。那么这个线程是谁创建的,在哪里创建的?
- 封装着一个 Executor:
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
// ...
// 当前NioEventLoop所封装的executor
private final Executor executor;
// ...
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
boolean addTaskWakesUp, int maxPendingTasks,
RejectedExecutionHandler rejectedHandler) {
super(parent);
this.addTaskWakesUp = addTaskWakesUp;
this.maxPendingTasks = Math.max(16, maxPendingTasks);
// 为了好区分,我们暂且称这个executor为 子executor
// apply()方法中的executor为 总executor
this.executor = ThreadExecutorMap.apply(executor, this);
// 任务队列
taskQueue = newTaskQueue(this.maxPendingTasks);
rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
}
// ...
}
- 拥有两个 execute()方法:一个是EventLoop的execute()方法,另一个是内部封装的Executor的execute()方法
NioEventLoopGroup是什么
- 是一个 ExecutorService 线程池
- 是一个 Executor:这个Executor中的execute方法,是使用线程池中的线程去执行command任务的。
- 封装着一个线程 Executor:
- 拥有两个 execute()方法:
- 继续跟一下总Executor的execute方法
到这里我们可以小结一下:
- ThreadPerTaskExecutor#execute:总的Exector使用DefaultThreadFactory创建了一个线程,并启动线程。
- ThreadExecutorMap#apply:而第①步运行的线程是这里apply的Runnable任务,而这个任务的run方法调用了我们真正任务的run方法。并且这里使用 ThreadLocal 做了线程隔离。
EventLoopGroup的创建与初始化
分析构造器
- 跟到这里我们知道了,在创建NioEventLoopGroup时,若不指定线程数量,则默认会创建当前机器的逻辑内核数量的2倍。
- 接着继续往下跟踪:我们就找到了创建 NioEventLoopGroup 的核心构造方法
- 到这里,构造方法首先创建了一个Executor(总executor)。先整体分析这个构造器方法,后面再详细分析。
线程工厂的创建newDefaultThreadFactory()
- 至此,默认的线程工厂对象创建好了,而且知道了将来创建的线程的名称格式是什么样的了。
创建NioEventLoop
- 里面的细节再跟下,创建了一个selector的二元组实例SelectorTuple:
- ThreadExecutorMap.apply创建子的executor:
创建EventExecutorChooser
ServerBootstrap的创建与初始化
- 使用:
- 类图:
- group() 方法、channel() 方法、options()方法等
任务的执行
- 分析入口:
ChannelFuture future = bootstrap.bind(8888).sync();
整体分析
初始化并注册initAndRegister()
- 该方法整体完成三件事:① 创建parentChannel;② 初始化parentChannel;③ 注册parentChannel。
创建parentChannel
- 继续往下分析代码:
- 生成channel的id的构成是怎样的?
- 至此,我们就创建好了一个NioServerSocketChannel。该Channel是对NIO原生Channel的封装,里面有一个config(NioServerSocketChannelConfig)可以对原生Channel进行配置,并且里面还有 id、底层操作对象unsafe、pipeline等。
初始化parentChannel
- 这里有个疑问,根据代码的逻辑,应该是在注册parentChannel时,才会进行当前channel与eventLoop的绑定,那么这里ch.eventLoop()存在吗?后面会进行分析。
- 将options属性初始化到channel中:
- 再跟踪下ServerBootstrapAcceptpor:
- 这样我们就初始化完成parentChannel了,并将childChannel注册到了selector。
注册parentChannel
- 选择一个eventLoop:chooser在前面已经跟踪过了
- 继续跟踪注册方法 register0():
- 到这里我们就将parentChannel注册到了selector。那么有个问题:这里的selector与前面childChannel注册的selector是不是同一个呢?
答案是否定的,原因是 parentChannel 绑定的selector是从parentGroup中的eventLoop来的,而childChannel绑定的selector是从childGroup中的eventLoop来的,eventLoop对象不是同一个,那么其对于的selector也不是同一个。
// parentGroup中的eventLoop将来是用于与处理客户端连接请求的parentChannel进行绑定的
EventLoopGroup parentGroup = new NioEventLoopGroup();
// childGroup中的eventLoop将来是用于与处理客户端读写请求的childChannel进行绑定的
EventLoopGroup childGroup = new NioEventLoopGroup();
eventLoop绑定线程的创建与执行
- 回过来我们再跟踪下 eventLoop.execute 这个方法的执行:
至此,我们可以梳理一下execute的执行:
-
eventLoop调用自己的execute()方法,去创建并启动线程,其会调用**自己所包含的executor(子executor)**的execute()方法。
-
子executor 其实是ThreadExecutorMap中用匿名内部类创建的,其调用execute()方法就会调用**eventLoopGroup内部的executor(总executor)**的execute()方法。
-
总executor 其实是ThreadPerTaskExecutor,其调用execute()方法就会创建并启动新的线程,执行start()方法,即会调用command的run()方法。
-
这个command就是ThreadExecutorMap.apply()方法中通过线程隔离处理过的command,该command的运行就会最终触发eventLoop调用一个无限循环的for【SingleThreadEventExecutor.this.run()】,去执行我们任务队列里面的任务。
-
源码阅读github地址:https://github.com/shouwangyw/netty/tree/main/netty-netty-4.1.36.Final