一、Netty介绍
1.1 简介
在官网开头,有这样一句话来介绍Netty
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
意为 Netty是一个异步、事件驱动的网络应用程序框架,用于快速开发高性能协议的服务端和客户端。
如何理解这句话?
异步是由于Netty中几乎所有操作都是异步的。
而所谓事件驱动,简单理解的话就比如我们使用电脑,你点什么按钮(即产生什么事件),电脑执行什么操作(即调用什么函数),当然事件不仅限于用户的操作,而在网络中,事件可以是建立连接、读写数据等等。
1.2 Netty的架构
Netty的架构图如下,摘自官网
核心部分:
- 支持零拷贝的升级版ByteBuffer(对零拷贝概念不懂的童鞋可自行查阅资料)
- 统一交互API
- 可拓展的事件模型
传输服务:
- Socket & Datagram,对应于TCP和UDP
- HTTP隧道
- JVM内部管道传输
协议支持:还有对众多协议的支持,包括HTTP、WebSocket,SSL、google protobuf等协议
1.3 Netty的主要特性
- 基于NIO实现,具有高吞吐、低延迟特性(没有学习NIO的童鞋需要先学习一下NIO)
- 对众多协议的支持,开箱即用
- 各种传输类型的统一API
- 可定制的线程模型(与接下来要说的Reactor模型有关)
1.4 Netty中的组件
这里先介绍一下Netty中的组件,先眼熟一下,后续再进行详细介绍
- ByteBuf:缓冲区,包括堆内存和直接内存
- ChannelPromise:异步操作相关
- EventLoop和EventLoopGroup:EventLoop意为事件循环,在单个线程中执行各种IO事件,一个EventLoopGroup对应多个EventLoop
- Channel:每个Channel都会注册到一个EventLoop上,一个EventLoop可以对应多个Channel,每个Channel包含了一个ChannelPipeline流水线
- ChannelPipeline和ChannelHandler:ChannelPipeline流水线是一个双向链表,由一连串的ChannelHandler组成
- Bootstrap:启动相关
在上面Netty的主要特性中,看到Netty具有可定制的线程模型的特性,那么这是何意?
其实是Netty可以很轻松的在Reactor的几种线程模型中进行切换,所以我们需要先了解一下Reactor模式
二、Reactor模式
2.1 什么是Reactor模式
定义如下:
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers
大致意思:Reactor是一种专门用于处理事件的模式,用于处理一个或多个输入源的并发服务请求,Service Handler将传入的服务请求分发给关联的Request Handlers来处理。如下图
那平常所说的Reactor模式是基于IO多路复用 + 非阻塞IO来实现的,与之相对应的是传统多线程 + 阻塞IO的模式(即每个线程处理一个连接)
IO多路复用:通过操作系统提供的selet/poll/epoll等操作来实现单个线程管理多个连接的方式,称为IO多路复用
非阻塞IO:当调用read、write等IO操作时,不阻塞当前线程,称为非阻塞IO
而Reactor模式一般实现方式有三种,分别为单Reactor单线程、单Reactor多线程及主从Reactor多线程,接下来逐一进行介绍
2.2 常见的三种Reactor模型
Reactor译为反应器,也叫分发器,它的作用其实就是监听多个连接,并将监听到的IO事件分发给对应的处理器来处理。
2.2.1 单Reactor单线程
单Reactor单线程是指只有一个Reactor,由该Reactor来注册新连接以及监听连接的IO事件,并在单个线程中来处理这些IO事件,如下图
大致流程:
- Reactor监听客户端的请求事件
- 如果是建立连接事件,将该事件分发给acceptor处理,acceptor会将该新连接注册到Reactor上
- 如果是读或写事件,将该事件分发给对应的handler处理
样例代码
基于Java NIO的样例代码如下,可以使用Telnet localhost 9999来测试,服务器将会打印出接收到的内容,并返回接收到的数量
/**
* Description: 单Reactor单线程
* Created by kamier
*/
public class SingleReactorSingleThread {
public static void main(String[] args) throws IOException {
// 启动服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
// 获取一个Selector
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
int recCount = 1;
while (true) {
int readyNum = selector.select();
if (readyNum <= 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
socketChannel.configureBlocking(false);
// 监听该SocketChannel的读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 打印读取到的内容
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
// 修改对该SocketChannel感兴趣的事件集为读写事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.wrap((":" + ++recCount + "; ").getBytes());
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
// 修改对该SocketChannel感兴趣的事件集为读事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
iterator.remove();
}
}
}
}
优缺点
优点:实现简单,不需要处理并发问题
缺点:单线程无法利用多CPU的处理能力,性能较低
2.2.2 单Reactor多线程
单Reactor多线程也是只有一个Reactor,与单线程的主要区别在于,使用多线程来进行业务逻辑的处理,如下图:
大致流程
- Reactor监听客户端的请求事件
- 如果是建立连接事件,将该事件分发给acceptor处理,acceptor会将该新连接注册到Reactor上
- 如果是读或写事件,将该事件分发给对应的handler处理,对应的handler将事件交由线程池来处理
样例代码
处理事件的业务逻辑和单线程是一样的,只是将事件交由线程池来处理
/**
* Description: 单Reactor多线程
* Created by kamier on 2022/6/13 22:20
*/
public class SingleReactorMultiThread {
public static AtomicInteger recCount = new AtomicInteger();
public static void main(String[] args) throws IOException, InterruptedException {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
// 启动服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
// 获取一个Selector
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int readyNum = selector.select();
if (readyNum <= 0) continue;
Set<SelectionKey> selectionKeys = selector.selectedKeys();
CountDownLatch latch = new CountDownLatch(selectionKeys.size());
for (SelectionKey key : selectionKeys) {
// 提交任务给线程池
threadPoolExecutor.submit(new SelectionKeyTask(key, selector, latch));
}
// 这里等待本轮事件全部处理完成
latch.await();
selectionKeys.clear();
}
}
private static class SelectionKeyTask implements Runnable {
SelectionKey key;
Selector selector;
CountDownLatch latch;
SelectionKeyTask (SelectionKey key, Selector selector, CountDownLatch latch) {
this.key = key;
this.selector = selector;
this.latch = latch;
}
@Override
public void run() {
try {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel1.accept();
socketChannel.configureBlocking(false);
// 监听该SocketChannel的读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 打印读取到的内容
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
// 修改对该SocketChannel感兴趣的事件集为读写事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.wrap((":" + recCount.incrementAndGet() + "; ").getBytes());
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
// 修改对该SocketChannel感兴趣的事件集为读事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}
}
优缺点
优点:
- 充分利用多CPU的处理能力
缺点:
- 实现相对复杂,需要解决多线程处理带来的并发问题
- 单个Reactor处理所有连接的IO事件,在连接数量较多的情况下,可能存在性能问题
2.2.3 主从Reactor多线程
主从Reactor多线程是指有一个MainReactor以及多个SubReactor,由MainReactor来监听建立连接事件,而SubReactor用于监听注册到自身上的连接的读写事件,分工更为明确。如下图:
大致流程
- MainReactor监听客户端的请求事件
- 如果是建立连接事件,将该事件分发给acceptor处理,acceptor会将该新连接注册到某一个SubReactor上(SubReactor可以有多个),并由该SubReactor监听该新连接的读写事件
- SubReactor监听注册到自身上的连接的读写事件
- 如果是读或写事件,将该事件分发给对应的handler处理,对应的handler将事件交由线程池来处理
优缺点
优点:
- 主从Reactor分工明确
- 可拓展性强
缺点:
- 实现复杂
三、总结
本文对Netty进行了一个简单介绍,并对Reactor的三种模型进行了讲解,后续章节会先介绍每个Netty组件的作用以及对其关键代码的解读,最后再通过一个http服务器的样例,来看一下整个服务器的启动流程是什么样的,以及它是如何来处理http请求的?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。