文章目录
一、Netty简介
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
二、线程模型
当前存在的线程模型:
- 传统阻塞IO模型
- Reactor模型
根据 Reactor模型中的 Reactor 数量以及处理资源线程池的大小,分3种:
- 单Reactor 单线程
- 单Reactor 多线程
- 主从Reactor 多线程
Netty 主要基于 主从Reactor 多线程模型做出了改进,主从Reactor多线程模型有多个Reactor。
2.1 传统IO
传统IO 问题:
- 一个请求一个线程,并发上不去
- 每个线程都会阻塞在read
解决办法:
- 不能来一个请求创建一个线程,可以搞线程池复用线程。一个线程处理多个连接
- 不能阻塞在read死等,而是要 IO复用。也就是,多个线程复用同一个阻塞对象,应用只在这一个阻塞对象等待,无需等待所有链接。某链接新来数据可以处理时,OS通知应用,线程从阻塞状态返回处理业务。
2.2 Reactor
这就是 Reactor 的线程模型:
这里的 ServiceHandler 就是 Reactor ,可以比作 电话接线员;
这里的 EventHandlers 就像是 接线员转接到的、真正处理任务的人员。
Reactor 的翻译,五花八门,比如:
a. 反应者模式
b. Dispatcher模式( 服务端将传递进来的多个请求,分发到多个 线程处理)
c. Notifier 模式,其实都是一个意思
2.3 单Reactor单线程
这个图在看的时候,要注意 线程和对象的区别(右下方说明)。
这种模型, 只有一个Reactor 线程,也就是说, select 、dispatch、accept、read … 这些操作都要靠Reactor 来搞定,实战根本没法用。
- 这里的select 是 IO复用模型中的 标准API,可以实现程序通过一个阻塞对象监听多路连接请求。
- Reactor对象通过select 监控客户端请求事件,收到事件后通过dispatch分发
- select 的请求中,一种是 建立连接的,由acceptor 处理,然后创建一个handler对象处理连接完成后的后续业务处理。
- 如果不是建立连接的,Reactor 分发调用 handler 对象处理连接完成后的业务
2.4 单Reactor 多线程模型
这里比较重要的是:
- Handler 只负责响应事件,不做具体业务处理, Handler 通过
read
读取数据后,会分发给后面的 worker 线程池的某个线程处理 - worker线程池的线程处理完业务后,会将结果返回给 handler。【这点要十分注意!否则 处理结果怎么成为 响应返回给客户端呢? 如此,逻辑就断层了】
这种模型的性能瓶颈很可能出现在 reactor 主线程的处理能力上,高并发情况下,分发速度可能跟不上。
2.5 主从Reactor多线程
上文提到,一个reactor 单线程可能成为 性能瓶颈,那主从Reactor多线程的示意图中, SubReactor 只有一个线程,会不会也成为瓶颈。
这里需要解释下,实际上, Reactor 子线程是可以有多个的,这里仅仅是篇幅限制所以从简画。
有几个关键点:
- main reactor 、 subreactor 数据是单向流动的,前者分发请求后, subreactor完成后续的所有处理工作,subreactor 无需返回数据给 main reactor
使用场景:
- Nginx主从reactor 多进程模型
- Memcache 主从 Reactor 多线程
- Netty 主从Reactor 多线程模型
2.6 小结一下
- 扩展性怎么理解?
说白了,就是subreactor 可以有很多个,而不是图示中的一个。 - 响应快 ,不为单个同步时间阻塞, 怎么理解?
说白了,多个subcreator 中其中一个处理请求即可,一个在阻塞,另一个线程来处理即可
三、Netty线程模型【重点内容】
这幅图十分重要 ,它描述了Netty 的线程模型整体轮廓。按图示,BossGroup 、WorkGroup 只是个通俗说法,本质就是NioEventLoopGroup对象,用大白话说,就是 装了一定数量 NioEventLoop 对象的 桶。NioEventLoop 就是真正干活的对象。
-
BossGroup 中的每个 NioEventLoop:
- 1、轮询accept事件
- 2、处理accept事件 ,与 client建立连接,生成NioSocketChannel ,并将其注册到 某个worker group中的NioEventLoop 的selector
- 3、处理任务队列的任务 ,即 runAllTasks
-
WorkerGroup中的每个 NioEventLoop
- 1、轮询 read/write
- 2、处理IO事件 (read/write 事件),在对应 NioSocketChannel上处理
- 3、处理队列任务,即 runAllTasks
四、Netty 任务队列
先说下场景是什么呢?
如果我们在server handler 中需要做过长时间的操作,会阻塞当前线程,造成 对客户端响应时间增长。抽象成代码就是:
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("channelRead thread:" + Thread.currentThread().getName());
try {
//模拟长时间操作:阻塞5S
TimeUnit.SECONDS.sleep(5);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
这段代码里的 sleep 会阻塞住当前线程,因此要考虑异步化 【使用任务队列】
下面来说下 任务队列 的常见三种使用场景:
1、用户自定义的普通任务 (任务队列是 字段taskQueue)
2、用户自定义的定时任务 (任务队列是 字段 scheduleQueue)
3、非当前reactor 线程调用 channel
【第三种在推送系统中经常使用到:根据 用户的ID,找到对应的channel引用,然后调用 write 方法向该用户推送消息。最终的write 会提交到任务队列后 被异步消费】
【注意这里说的阻塞,都是针对 ChannelRead() 方法会不会 被阻塞住的!!!】
A: 按方式一改造后的代码:
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("channelRead thread:" + Thread.currentThread().getName());
Runnable runnable = () -> {
//模拟长时间操作:阻塞5S
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("write-and-flush thread :" + Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
e.printStackTrace();
}
};
// 其实这是个伪异步, 由于执行 这个channelRead 、以及两次 runnable 的都是同一个线程,事实上这种方式
// 还是会阻塞 这个 channel 对应的线程。 不过,这种做法可以让 ChannelRead 先执行完,然后再执行 Runnable
// 提交第一次任务
ctx.channel().eventLoop().execute(runnable);
// 提交第二次任务
ctx.channel().eventLoop().execute(runnable);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
B : 按方式二改造代码 【同上,这里channelRead 不会会阻塞住了, schedule 中的任务会在 channelRead 被执行完之后才会被执行】
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("channelRead thread:" + Thread.currentThread().getName() + " now it is: " + CommonUtil
.toCurrentTimeStr());
Runnable runnable = () -> {
//模拟长时间操作:阻塞5S
try {
System.out.println(
"write-and-flush thread :" + Thread.currentThread().getName() + " now it is: " + CommonUtil
.toCurrentTimeStr());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello there".getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
e.printStackTrace();
}
};
// 其实这是个伪异步, 由于执行 这个channelRead 、以及两次 runnable 的都是同一个线程,事实上这种方式
// 还是会阻塞 这个 channel 对应的线程
// 提交第一次任务
ctx.channel().eventLoop().schedule(runnable, 10, TimeUnit.SECONDS);
// 提交第二次任务
ctx.channel().eventLoop().schedule(runnable, 5, TimeUnit.SECONDS);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
C. 方式 三 代码略
其中有个比较简单的做法是在下面代码中获取到 SocketChannel 的引用,然后用Map 缓存起来
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
})