本系列Netty源码解析文章基于 4.1.56.Final版本
在上篇文章《聊聊Netty那些事儿之从内核角度看IO模型》中我们花了大量的篇幅来从内核角度详细讲述了五种IO模型
的演进过程以及ReactorIO线程模型
的底层基石IO多路复用技术在内核中的实现原理。
最后我们引出了netty中使用的主从Reactor IO线程模型。
通过上篇文章的介绍,我们已经清楚了在IO调用的过程中内核帮我们搞了哪些事情,那么俗话说的好内核领进门,修行在netty
,netty在用户空间又帮我们搞了哪些事情?
那么从本文开始,笔者将从源码角度来带大家看下上图中的Reactor IO线程模型
在Netty中是如何实现的。
本文作为Reactor在Netty中实现系列文章中的开篇文章,笔者先来为大家介绍Reactor的骨架是如何创建出来的。
在上篇文章中我们提到Netty采用的是主从Reactor多线程
的模型,但是它在实现上又与Doug Lea在Scalable IO in Java论文中提到的经典主从Reactor多线程模型
有所差异。
Netty中的Reactor
是以Group
的形式出现的,主从Reactor
在Netty中就是主从Reactor组
,每个Reactor Group
中会有多个Reactor
用来执行具体的IO任务
。当然在netty中Reactor
不只用来执行IO任务
,这个我们后面再说。
Main Reactor Group
中的Reactor
数量取决于服务端要监听的端口个数,通常我们的服务端程序只会监听一个端口,所以Main Reactor Group
只会有一个Main Reactor
线程来处理最重要的事情:绑定端口地址
,接收客户端连接
,为客户端创建对应的SocketChannel
,将客户端SocketChannel分配给一个固定的Sub Reactor
。也就是上篇文章笔者为大家举的例子,饭店最重要的工作就是先把客人迎接进来。“我家大门常打开,开放怀抱等你,拥抱过就有了默契你会爱上这里......”
Sub Reactor Group
里有多个Reactor
线程,Reactor
线程的个数可以通过系统参数-D io.netty.eventLoopThreads
指定。默认的Reactor
的个数为CPU核数 * 2
。Sub Reactor
线程主要用来轮询客户端SocketChannel上的IO就绪事件
,处理IO就绪事件
,执行异步任务
。Sub Reactor Group
做的事情就是上篇饭店例子中服务员的工作,客人进来了要为客人分配座位,端茶送水,做菜上菜。“不管远近都是客人,请不用客气,相约好了在一起,我们欢迎您......”
一个
客户端SocketChannel
只能分配给一个固定的Sub Reactor
。一个Sub Reactor
负责处理多个客户端SocketChannel
,这样可以将服务端承载的全量客户端连接
分摊到多个Sub Reactor
中处理,同时也能保证客户端SocketChannel上的IO处理的线程安全性
。
由于文章篇幅的关系,作为Reactor在netty中实现的第一篇我们主要来介绍主从Reactor Group
的创建流程,骨架脉络先搭好。
下面我们来看一段Netty服务端代码的编写模板,从代码模板的流程中我们来解析下主从Reactor的创建流程以及在这个过程中所涉及到的Netty核心类。
Netty服务端代码模板
/**
* Echoes back any received data from a client.
*/
public final class EchoServer {
static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
public static void main(String[] args) throws Exception {
// Configure the server.
//创建主从Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//配置主从Reactor
.channel(NioServerSocketChannel.class)//配置主Reactor中的channel类型
.option(ChannelOption.SO_BACKLOG, 100)//设置主Reactor中channel的option选项
.handler(new LoggingHandler(LogLevel.INFO))//设置主Reactor中Channel->pipline->handler
.childHandler(new ChannelInitializer<SocketChannel>() {//设置从Reactor中注册channel的pipeline
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// Start the server. 绑定端口启动服务,开始监听accept事件
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
复制代码
- 首先我们要创建Netty最核心的部分 ->
创建主从Reactor Group
,在Netty中EventLoopGroup
就是Reactor Group
的实现类。对应的EventLoop
就是Reactor
的实现类。
//创建主从Reactor线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
复制代码
- 创建用于
IO处理
的ChannelHandler
,实现相应IO事件
的回调函数,编写对应的IO处理
逻辑。注意这里只是简单示例哈,详细的IO事件处理,笔者会单独开一篇文章专门讲述。
final EchoServerHandler serverHandler = new EchoServerHandler();
/**
* Handler implementation for the echo server.
*/
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
................省略IO处理逻辑................
ctx.write(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
复制代码
-
创建
ServerBootstrap
Netty服务端启动类,并在启动类中配置启动Netty服务端所需要的一些必备信息。-
通过
serverBootstrap.group(bossGroup, workerGroup)
为Netty服务端配置主从Reactor Group
实例。 -
通过
serverBootstrap.channel(NioServerSocketChannel.class)
配置Netty服务端的ServerSocketChannel
用于绑定端口地址
以及创建客户端SocketChannel
。Netty中的NioServerSocketChannel.class
就是对JDK NIO中ServerSocketChannel
的封装。而用于表示客户端连接
的NioSocketChannel
是对JDK NIOSocketChannel
封装。
在上篇文章介绍
Socket内核结构
小节中我们提到,在编写服务端网络程序时,我们首先要创建一个Socket
用于listen和bind
端口地址,我们把这个叫做监听Socket
,这里对应的就是NioServerSocketChannel.class
。当客户端连接完成三次握手,系统调用accept
函数会基于监听Socket
创建出来一个新的Socket
专门用于与客户端之间的网络通信我们称为客户端连接Socket
,这里对应的就是NioSocketChannel.class
-
serverBootstrap.option(ChannelOption.SO_BACKLOG, 100)
设置服务端ServerSocketChannel
中的SocketOption
。关于SocketOption
的选项我们后边的文章再聊,本文主要聚焦在NettyMain Reactor Group
的创建及工作流程。 -
serverBootstrap.handler(....)
设置服务端NioServerSocketChannel
中对应Pipieline
中的ChannelHandler
。
netty有两种
Channel类型
:一种是服务端用于监听绑定端口地址的NioServerSocketChannel
,一种是用于客户端通信的NioSocketChannel
。每种Channel类型实例
都会对应一个PipeLine
用于编排对应channel实例
上的IO事件处理逻辑。PipeLine
中组织的就是ChannelHandler
用于编写特定的IO处理逻辑。注意
serverBootstrap.handler
设置的是服务端NioServerSocketChannel PipeLine
中的ChannelHandler
。serverBootstrap.childHandler(ChannelHandler childHandler)
用于设置客户端NioSocketChannel
中对应Pipieline
中的ChannelHandler
。我们通常配置的编码解码器就是在这里。
ServerBootstrap
启动类方法带有child
前缀的均是设置客户端NioSocketChannel
属性的。ChannelInitializer
是用于当SocketChannel
成功注册到绑定的Reactor
上后,用于初始化该SocketChannel
的Pipeline
。它的initChannel
方法会在注册成功后执行。这里只是捎带提一下,让大家有个初步印象,后面我会专门介绍。 -
-
ChannelFuture f = serverBootstrap.bind(PORT).sync()
这一步会是下篇文章要重点分析的主题Main Reactor Group
的启动,绑定端口地址,开始监听客户端连接事件(OP_ACCEPT
)。本文我们只关注创建流程。 -
f.channel().closeFuture().sync()
等待服务端NioServerSocketChannel
关闭。Netty服务端到这里正式启动,并准备好接受客户端连接的准备。 -
shutdownGracefully
优雅关闭主从Reactor线程组
里的所有Reactor线程
。
Netty对IO模型的支持
在上篇文章中我们介绍了五种IO模型
,Netty中支持BIO
,NIO
,AIO
以及多种操作系统下的IO多路复用技术
实现。
在Netty中切换这几种IO模型
也是非常的方便,下面我们来看下Netty如何对这几种IO模型进行支持的。
首先我们介绍下几个与IO模型
相关的重要接口:
EventLoop
EventLoop
就是Netty中的Reactor
,可以说它就是Netty的引擎,负责Channel上IO就绪事件的监听
,IO就绪事件的处理
,异步任务的执行
驱动着整个Netty的运转。
不同IO模型
下,EventLoop
有着不同的实现,我们只需要切换不同的实现类就可以完成对NettyIO模型
的切换。
BIO | NIO | AIO |
---|---|---|
ThreadPerChannelEventLoop | NioEventLoop | AioEventLoop |
在NIO模型
下Netty会自动
根据操作系统以及版本的不同选择对应的IO多路复用技术实现
。比如Linux 2.6版本以上用的是Epoll
,2.6版本以下用的是Poll
,Mac下采用的是Kqueue
。
其中Linux kernel 在5.1版本引入的异步IO库io_uring正在netty中孵化。
EventLoopGroup
Netty中的Reactor
是以Group
的形式出现的,EventLoopGroup
正是Reactor组
的接口定义,负责管理Reactor
,Netty中的Channel
就是通过EventLoopGroup
注册到具体的Reactor
上的。
Netty的IO线程模型是主从Reactor多线程模型
,主从Reactor线程组
在Netty源码中对应的其实就是两个EventLoopGroup
实例。
不同的IO模型
也有对应的实现:
BIO | NIO | AIO |
---|---|---|
ThreadPerChannelEventLoopGroup | NioEventLoopGroup | AioEventLoopGroup |
ServerSocketChannel
用于Netty服务端使用的ServerSocketChannel
,对应于上篇文章提到的监听Socket
,负责绑定监听端口地址,接收客户端连接并创建用于与客户端通信的SocketChannel
。
不同的IO模型
下的实现:
BIO | NIO | AIO |
---|---|---|
OioServerSocketChannel | NioServerSocketChannel | AioServerSocketChannel |
SocketChannel
用于与客户端通信的SocketChannel
,对应于上篇文章提到的客户端连接Socket
,当客户端完成三次握手后,由系统调用accept
函数根据监听Socket
创建。
不同的IO模型
下的实现:
BIO | NIO | AIO |
---|---|---|
OioSocketChannel | NioSocketChannel | AioSocketChannel |
我们看到在不同IO模型
的实现中,Netty这些围绕IO模型
的核心类只是前缀的不同:
- BIO对应的前缀为
Oio
表示old io
,现在已经废弃不推荐使用。 - NIO对应的前缀为
Nio
,正是Netty推荐也是我们常用的非阻塞IO模型
。 - AIO对应的前缀为
Aio
,由于Linux下的异步IO
机制实现的并不成熟,性能提升表现上也不明显,现已被删除。
我们只需要将IO模型
的这些核心接口对应的实现类