1. Netty 工作原理示意图
- Netty 主要基于主从 Reactors 多线程模型做了一定的改进,其中主从 Reactor 多 线程模型有多个 Reactor
1.1 简单版
- 说明 :
- BossGroup 线程维护Selector , 只关注Accecpt
- 当接收到Accept事件,获取到对应的 SocketChannel, 封装成 NIOScoketChannel并注册到 Worker 线程(事件循环), 并进行维护
- 当Worker线程监听到 Selector 中通道发生自己感 兴趣的事件后,就进行处理(就由 Handler 处理), 注意 Handler 已经加入到通道
1.2 进阶版
1.3 详细版
- 说明 :
- Netty 抽象出两组线程池
BossGroup
专门负责接收客户端的连接,WorkerGroup
专门负责网络的读写- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个 NioEventLoop 都有一个 Selector , 用于监听绑定在其上的 Socket 的网络通讯
- NioEventLoopGroup(BossGroup、WorkerGroup) 可以有多个线程, 即可以含有多个 NioEventLoop
- 每个Boss 的 NioEventLoop 循环执行的步骤有3步
- 轮询accept 事件
- 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到 Worker 的 NIOEventLoop 上的 Selector
- 处理任务队列的任务 , 即 runAllTasks
- 每个 Worker 的 NIOEventLoop 循环执行的步骤
- 轮询read, write 事件
- 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
- 处理任务队列的任务 , 即 runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用 Pipeline(管道), Pipeline 中包含了 Channel , 即通过 Pipeline 可以获取到对应通道, 管道中维护了很多的处理器。管道可以使用 Netty 提供的,也可以自定义
2. Netty快速入门实例-TCP服务
- 要求
- Netty 服务器在 6668 端口监听,客户端能发送消息给服务器 “hello, 服务器~”
- 服务器可以回复消息给客户端 “hello, 客户端~”
- 代码实现
-
引入依赖
<!--netty依赖--> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.52.Final</version> </dependency>
-
编写 Netty 的服务端 :
NettyServer
public class NettyServer { public static void main(String[] args) throws InterruptedException { // 创建 BossGroup 和 WorkerGroup /* 说明 1. 创建两个线程组 BossGroup 和 WorkerGroup 2. BossGroup 只处理连接请求 3. WorkerGroup 处理真正客户端的业务 4. 运行时,这两个都是无限循环 */ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); // 进行异常处理,try - catch try { // 创建 服务端 启动对象,并配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); // 使用链式编程进行配置参数 bootstrap.group(bossGroup, workerGroup)// 设置两个线程组 .channel(NioServerSocketChannel.class)// 使用 NioServerSocketChannel 作为服务器的通道实现 .option(ChannelOption.SO_BACKLOG, 128)// 设置线程队列等待连接的个数 .childOption(ChannelOption.SO_KEEPALIVE, true)// 设置连接保持活动连接状态 .childHandler(new ChannelInitializer<SocketChannel>() {// 给 workerGroup 的 NioEventLoop 对应的管道(Pipeline)设置处理器 // 创建一个通道初始化对象 /** * 向 workerGroup 对应的 管道(Pipeline) 设置处理器 * * @param socketChannel * @throws Exception */ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline()// 获得 这个 socketChannel 对应的 Pipeline .addLast(new NettyServerHandler());// 把自定义的 Handler 添加到 管道 } }); System.out.println("服务器准备好了……"); // 绑定一个端口,并且同步。生成了一个 ChannelFuture 对象 // 这里就已经启动了服务器 ChannelFuture channelFuture = bootstrap.bind(6668).sync(); // 对 关闭通道 进行监听 // 这里只是监听,只有关闭通道时才进行处理,这句话不是直接关闭了通道 channelFuture.channel().closeFuture().sync(); }finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
-
编写 自定义的 Netty 服务端处理器 :
NettyServerHandler
/** * 1. 自定义一个 Handler 需要继承 Netty 规定好的某个 处理器适配器 * 2. 这时自定义的 Handler ,才能称为一个 Handler */ public class NettyServerHandler extends ChannelInboundHandlerAdapter { /** 读取数据的事件(可以读取客户端发送的消息) * * @param ctx 上下文对象,包含 管道、通道、地址 * @param msg 客户端发送的消息,默认是 Object 类型 * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("【Server】: ctx" + ctx); // 将 msg 转换成 ByteBuffer /* 说明 : 1. 注意这个是 ByteBuf ,是 io.netty.buffer 包下的,不是 NIO 下的 Buffer 2. ByteBuf 比 Buffer 的性能更高一点 */ ByteBuf buf = (ByteBuf) msg; // 把 buf 转成 UTF8 格式的字符串 System.out.println("客户端发送的 msg :" + buf.toString(CharsetUtil.UTF_8)); System.out.println("客户端地址 :" + ctx.channel().remoteAddress()); } /** * 数据读取完毕后,返回消息给客户端 * @param ctx * @throws Exception */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // 把数据写入缓冲区,并刷新缓冲区 // 一般来说,需要对这个发送的消息进行编码 ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端",CharsetUtil.UTF_8)); } /** * 处理异常 * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // 关闭通道 ctx.channel().close(); } }
-
编写 Netty 的客户端 :
NettyClient
public class NettyClient { public static void main(String[] args) throws InterruptedException { // 客户端需要一个事件循环组 EventLoopGroup group = new NioEventLoopGroup(); try { // 客户端启动对象 —— Bootstrap ,不是 服务端的 ServerBootstrap // 并且是 io.netty.bootstrap 包下的 Bootstrap bootstrap = new Bootstrap(); // 设置相关参数 bootstrap.group(group)// 设置线程组 .channel(NioSocketChannel.class)// 设置客户端通道的实现类 .handler(new ChannelInitializer<SocketChannel>() {// 设置处理器 @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new NettyClientHandler());// 加入自己的处理器 } }); System.out.println("客户端准备好了……"); // 启动客户端连接服务器端 // 这里涉及到一个 Netty 的异步模型,后面详述 ChannelFuture channelFuture = bootstrap.connect("localhost", 6668).sync(); // 对关闭通道进行监听 channelFuture.channel().closeFuture().sync(); }finally { group.shutdownGracefully(); } } }
-
编写 自定义的 Netty 客户端处理器 :
NettyClientHandler
public class NettyClientHandler extends ChannelInboundHandlerAdapter { /** * 当通道就绪时,就会触发该方法,就可以发信息了 * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("【Client】:ctx" + ctx); ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,server", CharsetUtil.UTF_8)); } /** * 当通道有读取事件时 ,会触发 * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println("服务器发送的 msg :" + buf.toString(CharsetUtil.UTF_8)); System.out.println("服务器的地址 :"+ ctx.channel().remoteAddress()); } /** * 异常处理 * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.channel().close(); } }
-
启动测试
先启动 NettyServer,在启动 NettyClient
NettyServer 的控制台:
NettyClient 的控制台:
3. 对‘Netty快速入门实例’的分析
- 通过分析 ‘Netty快速入门实例’ 的执行流程,来熟悉并加深对 Netty模型 的理解
3.1 BossGroup 和 WorkGroup 怎么确定自己有多少个 NIOEventLoop
-
点进
NioEventLoopGroup
-
一直点进
this
,一直进入父类
-
找到这个值的初始化代码
-
所以 BossGroup 和 WorkerGroup 含有的子线程数(NioEventLoop)默认为 CPU 核数*2
-
在此处打一个断点,进行 Debug
-
由源码中的构造方法可知 —— 想要设置线程数只要在参数中输入即可
3.2 WorkerGroup 是如何分配这些进程的
-
设置 BossGroup 进程数为 1 ; WorkerGroup 进程数为 4 ; Client 数位 8
-
设置进程数量
-
输出线程信息,在
NettyServerHandler
中添加
-
启动服务端、和4个客户端
-
启动第五个客户端会分配到哪个线程?
又回到了第一个线程,在默认情况下,WorkerGroup 分配的逻辑就是按顺序循环分配的
3.3 BossGroup 和 WorkerGroup 中的 Selector 和 TaskQueue
- 打断点进行 Debug
- 每个子线程都具有自己的 Selector、TaskQueue……
3.4 CTX 上下文、Channel、Pipeline 之间关系
- 修改
NettyServerHandler
,并添加端点
- 先看 CTX 上下文中的信息
上下文中还有很多其他信息,就不一样列举了 - Pipeline
- Channel
- CTX 上下文、Channel、Pipeline 三者关系示意图
4. TaskQueue 任务队列
4.1 概述
-
如果在某个 Handler 中有个长时间的操作,那势必会造成 Pipeline管道 的阻塞,那么这种情况就需要把这些任务提交到 TaskQueue 进行异步执行
-
TaskQueue 和 Channel 有绑定的关系
-
任务队列中的 Task 有 3 种典型使用场景
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前 Reactor 线程调用 Channel 的各种方法
4.2 体验任务的阻塞
-
编写一个 服务端的 Handler :
NettyServerHandlerTaskQ
public class NettyServerHandlerTaskQ extends ChannelInboundHandlerAdapter { /** 读取数据的事件(可以读取客户端发送的消息) * * @param ctx 上下文对象,包含 管道、通道、地址 * @param msg 客户端发送的消息,默认是 Object 类型 * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 比如这里有一个非常耗时的任务,希望可以异步执行 // 把该任务提交到 Channel 对应的 NIOEventLoop 的 TaskQueue 中 Thread.sleep(10 * 1000); ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务",CharsetUtil.UTF_8)); System.out.println("耗时长的任务执行完毕,继续"); } /** * 数据读取完毕后,返回消息给客户端 * @param ctx * @throws Exception */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端",CharsetUtil.UTF_8)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // 关闭通道 ctx.channel().close(); } }
-
修改 NettyServer 中添加到 Pipeline 的 Handler
-
先这样执行一个查看效果(没有添加到任务队列)
4.3 TaskQueue 使用场景-1
-
用户程序自定义的普通任务
-
上面自定义了一个执行时间长的任务,该任务会阻塞这个 EventLoop 线程,可以使用该方法来解决耗时长的任务的阻塞问题
-
修改
NettyServerHandlerTaskQ
中的channelRead
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 解决方案-1:用户程序自定义的普通任务 ctx.channel().eventLoop().execute(()->{ try { Thread.sleep(10 * 1000); ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务,方案-1",CharsetUtil.UTF_8)); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("耗时长的任务执行完毕,继续"); }
-
运行测试
-
这也就能看出任务队列的异步执行了
-
下断点,查看 TaskQueue
查看
ctx -> pipeline -> channel -> eventloop -> taskqueue
可以看到这里面有刚刚的 Handler
4.4 TaskQueue 使用场景-2
- 用户自定义定时任务
- 修改
NettyServerHandlerTaskQ
中的channelRead
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 解决方案-2:用户自定义定时任务 // 把任务提交到 scheduledTaskQueue // 在和服务端连接成功后 5s 开始异步执行 run 方法 ctx.channel().eventLoop().schedule(()->{ try { Thread.sleep(5 * 1000); ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务,方案-2",CharsetUtil.UTF_8)); } catch (InterruptedException e) { e.printStackTrace(); } }, 5, TimeUnit.SECONDS); System.out.println("耗时长的任务执行完毕,继续"); }
- 运行测试
- 下断点,查看 scheduledTaskQueue 中是否多了任务
查看 ctx -> pipeline -> channel -> eventloop -> scheduledTaskQueue
4.5 TaskQueue 使用场景-3
-
非当前 Reactor 线程调用 Channel 的各种方法
-
例如:在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费
-
该场景就不演示了,只简单说一下思路
在用户连接到服务端的时候,可以获取到该客户对应的 SocketChannel 的 HashCode
可以在服务端维护一个 集合 ,存放所有连接的 SocketChannel
在有消息需要推送的时候,就遍历这个 SocketChannel 集合,通过 HashCode 找到 Channel
通过 Channel 找到 EventLoop
再往后就是上面提到的任务的处理了
5. 总结
-
Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。
-
NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道。
-
NioEventLoop 内部采用串行化设计,从消息的 读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责
• NioEventLoopGroup 下包含多个 NioEventLoop
• 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
• 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
• 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
• 每个 NioChannel 都绑定有一个自己的 ChannelPipeline