概述
关于学习 Netty 的基本前置知识已全部整理完成,从本篇我们正式进入 Netty 框架的学习。学习任何框架最快的途径就是使用它,因此本篇我打算使用 Netty 实现时间服务器,通过代码的方式初步认识 Netty。
Netty 示例
下面我们直接看代码,其中每一行代码的作用我通过注释的方式给出:
Netty 服务端源码:
public class TimeServer {
public static void main(String[] args) {
// 启动服务端,调用初始化方法
new TimeServer().init();
}
private static final int PORT = 8888;
private void init() {
// 创建两个 NIO 线程组,这两个线程组就类似多 Reactor 模式中的 Reactor
// group 线程组主要用于处理客户端连接
EventLoopGroup group = new NioEventLoopGroup();
// workGroup 线程组主要用于管道的读写操作
EventLoopGroup workGroup = new NioEventLoopGroup();
// bootstrap 是 Netty 用于启动 NIO 服务端的辅助类,主要用来简化代码
ServerBootstrap bootstrap = new ServerBootstrap();
// 将两个线程组作为参数传递,初始化 NIO 启动辅助类
bootstrap.group(group, workGroup)
// 设置 Channel 类型为 NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 设置最大连接数为 1024
.option(ChannelOption.SO_BACKLOG, 1024)
// 绑定 I/O 事件处理类
.childHandler(new ChildChannelHandler());
try {
// 调用 bind(PORT) 方法绑定监听端口
// 调用 sync() 方法等待绑定操作完成,操作完成后返回 ChannelFuture 对象
// ChannelFuture 对象就类似 Future,主要用于异步操作的回调通知
ChannelFuture f = bootstrap.bind(PORT).sync();
// 调用 f.channel().closeFuture().sync() 阻塞等待服务端数据链路中断
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 回收资源
group.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 创建 Channel 成功后,在初始化时创建 TimeServerHandler 处理器,并让它绑定 pipeline,处理网络 IO 请求
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
private class TimeServerHandler extends ChannelHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// ByteBuf 相当于 ByteBuffer 的增强版,提供更多的功能
ByteBuf buf = (ByteBuf) msg;
// readableBytes() 方法可以返回缓冲区客户的字节长度
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String message = new String(bytes, "UTF-8");
System.out.println("The time server receive message:" + message);
String result = message.equalsIgnoreCase("QUERY TIME ORDER")
? new Date(System.currentTimeMillis()).toString() : "I don't know";
// 将处理结果转换为 ByteBuf 对象
ByteBuf response = Unpooled.copiedBuffer(result.getBytes());
// 结果返回给客户端
ctx.write(response);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 读取数据时,数据首先被读取到缓冲区,上层应用从缓冲区读取
// 调用 flush() 方法强制清空缓冲区,提高效率
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 如果出现异常,关闭 ChannelHandlerContext ,释放相关资源
ctx.close();
}
}
}
Netty 客户端源码:
public class TimeClient {
public static void main(String[] args) {
// 启动客户端,调用初始化方法
new TimeClient().connect();
}
private static final String ADDRESS = "127.0.0.1";
private static final int PORT = 8888;
private void connect() {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
// 绑定 Channel 类型为 NioSocketChannel
.channel(NioSocketChannel.class)
// 禁止 Nagle 算法,该算法为了减少 TCP 包的数量,会将较小的包组合成大包发送
// 在部分场景下,由于 TCP 延迟,该算法可能导致连续发送两个请求包
.option(ChannelOption.TCP_NODELAY, true)
// 这里使用匿名内部类绑定处理器
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// NioSocketChannel 创建成功初始化时,将 TimeClientHandler 绑定到 pipeline,用于处理网络 IO
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
// 发起异步连接操作
ChannelFuture future = bootstrap.connect(ADDRESS, PORT);
// 阻塞等待客户端关闭链路
try {
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
private class TimeClientHandler extends ChannelHandlerAdapter {
private final ByteBuf buf;
private TimeClientHandler() {
byte[] bytes = "QUERY TIME ORDER".getBytes();
buf = Unpooled.buffer(bytes.length);
buf.writeBytes(bytes);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String message = new String(bytes, "UTF-8");
System.out.println("Client receive message:" + message);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
}
运行结果:
服务端:
The time server receive message:QUERY TIME ORDER
客户端:
Client receive message:Tue Oct 13 17:28:48 CST 2020
示例分析
这里我主要挑选部分 Netty 核心 API 做简单介绍,后面在具体学习源码时详细介绍:
EventLoopGroup
如上述示例代码所示,在启动 Netty 服务端前,首先需要初始化两个 NioEventLoopGroup 对象。这两个对象实际上就是两个独立的 Reactor 线程池。其中它的类图如下所示:
如上图所示,该类实现了 ExecutorService 线程池接口。其中 Netty 根据具体功能分以下两种 Group:
- BossGroup:接受客户端 TCP 连接,初始化 Channel、将链路状态的变更事件通知给 ChannlePipeline;
- WorkerGroup:异步读取数据,异步发送数据,执行系统调用及执行定时任务 Task
在具体使用过程中可以通过调整线程池的线程数、是否共享线程池等方式改变 Reactor 模型的状态。
Reactor 模型主要分为以下几种:单线程 Reactor、多线程 Reactor、多线程多Reactor。其中在多 Reactor 模型下,可以如上面示例所示,一个负责处理 Accept,一个负责处理 I/O,也可以像我上篇博客最后示例中那样,Accept 集中在某一个 Reactor,I/O 操作随机分配。
其中 Netty 通过两个 EventLoopGroup 逻辑隔离 NIO accept 和 I/O 线程操作,分离处理逻辑。
EventLoop
看类名我们就能猜到,EventLoop 就是 EventLoopGroup 中维护的线程对象。一个 EventLoop 绑定一个特定的线程,并且在它的整个生命周期内,绑定的线程都不会发生改变。其中该类的类图如下所示:
这里类结构比较复杂,我们暂时不做深入源码学习,只简单的提一下:抽象类 SingleThreadEventExecutor 中包含 Thread 类型线程对象,该线程实际上就是 EventLoop 内部维护的线程对象,最终该线程会调用到 NioEventLoop 的 run() 方法。
NioEventLoop 主要包含以下功能:
- 作为 IO 线程,NioEventLoop 主要执行和 Channel 相关的 IO 操作,包含调用 select 等待就绪 I/O 事件,读写数据等
- 手动添加任务执行和定时任务
关于 NioEventLoop 的介绍先写到这里,我们暂时只需要知道通过它关联某一个 Channel,并执行和该 Channel 相关的 I/O 操作即可。
ChannelPipeline
上述代码中我们主要通过 socketChannel.pipeline() 获取 ChannelPipeline 对象后调用 addLast() 方法添加具体 Handler,所以我们最后聊聊 ChannelPipeline 对象,其中该对象和 Channel,ChannelHandler,ChannelHandlerContext 关系密切,因此我放在一块介绍。
Channel 即管道的意思,一个 Channel 就表示一个客户端和服务端的连接通道,其中它是全双工的,并且支持异步处理。它主要和 Buffer 缓冲区实现 I/O 常见操作。
一般情况下我们不直接操作 Channel,而是通过 ChannelHandler 对象间接操作,根据具体功能的不同,ChannelHandler 分为 ChannelInboundHandler 和 ChannelOutboundHandler,这两种 Handler 分别处理输入数据和输出数据。根据不同的 I/O 事件,需要不用的 Handler 来处理。
本篇示例代码采用 Netty 5.0.0 版本,这里 API 介绍采用 Netty 4.x 版本。Netty 5.0.0 版本不存在 ChannelInboundHandler 接口,只有 ChannelInboundHandlerAdapter 类,并且该类目前不建议使用。
ChannelPipeline 实际上就是以链表的形式维护了一组 ChannelHandler ,当存在具体的 I/O 事件需要处理时,它负责调用具体的 Handler 对事件进行处理。其中 Channel 和 ChannelPipeline一一对应,两者之间可以通过 pipeline() 、channel() 方法互相获取对象。
ChannelPipeline 也不是直接管理 ChannelHandler 的,而是通过 ChannelHandlerContext 间接管理。ChannelHandlerContext 对象可以获取与之相关的 Channel 和 ChannelHandler,其中它和 ChannelHandler 一一对应。四者间的关系如下图所示:
如上图所示,一个 Channel 对应一个 ChannelPipeline,每一个 ChannelPipeline 包含多个 ChannelHandler,其中 ChannelPipeline 通过 ChannelHandlerContext 间接调用 ChannelHandler。
ChannelPipeline 内部通过双向链表的形式维护 ChannelHandlerContext 链表,并且链表头和表尾两部分创建两个特殊 Handler,我们手动添加的 Handler 都包含在这两个 Handler 之间,具体效果如下:
这里需要特别的注意的一点是:Inbound 是顺序执行的,Outbound 是逆序执行的。举个例子,假设此时我们调用 write() 方法处理输出流 I/O 事件,下面我列举出具体处理过程,这里暂时省略源码,直接给出结果:
- 首先会调用 tail 的 write() 方法,即从链表尾部开始执行
- tail write() 会沿着 context 链向前寻找,直至找到一个 OutBound 类型的 Context
- 调用该 Context 对应 Handler 的 write() 方法
- 默认实现的 write() 方法不做任何处理,继续向前传播
其中输入流的处理正好相反,这里我们省略其过程。由此可见,Pipeline 、Context,Handler 三者相互配合实现 pipeline 的事件传播。
Netty 和 Reactor 模式
最后我们来简单聊聊 Netty 是如何使用 Reactor 模式的,这里我们和官方 Reactor 模型结构进行类比。
从结构层面:
- Handle:事件资源对应 SelectionKey
- Synchronous Event Demultiplexer:同步事件分离器即 NIO 中的 Selector,Netty 中 Selector 的细节维护在 LoopGroup中,因此也可以把事件分离器看做 EventLoopGroup。
- EventHandler:事件处理器即 ChannelHandler
- Concrete Event Handler:开发者主动实现的 ChannelHandler
- Initiation Dispatcher:分发器即 EventLoop
至此,Netty 的初步认识基本结束!