目录
Netty是什么
Netty中的技术
1. 一个Java开源框架,现为Github的独立项目
2. 一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
3. 主要针对TCP协议下,面向Clients端的高并发应用,或者P2P场景下的大量数据持续传输的应用。
4. 本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。
为什么出现Netty
其实Java是有自己的原生NIO库的,不过存在以下问题
1)NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2)需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
3)开发工作量和难度都非常大:例如客户端面临**断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理**等等。
4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。
而Netty是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序; Netty 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了 NIO 的开发过程;Netty 是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。
技术背景
I/O模型
Java中的IO模型:BIO、NIO、AIO。
BIO
其中BIO就是同步阻塞I/O模式。
- 服务器启动一个ServerSocket
- 客户端启动Socket对服务器进行通信,默认情况下服务器需要对每个客户建立一个线程与之通信。
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,再继续进行。
简答的Java demo如下:
public static void main(String[] args) throws IOException {
//线程池机制
/*
思路:
1.创建一个线程池
2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
*/
//线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建一个ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
while(true){
//监听,等待客户端连接
final Socket socket = serverSocket.accept();//阻塞
System.out.println("s连接到一个客户端");
//启动一个线程,与之通信
newCachedThreadPool.execute(()->{
//可以和客户端通信
handler(socket);
});
}
}
//连接后的处理
public static void handler(Socket socket){
try {
byte[] bytes = new byte[1024];
//通过socket 获取一个输入流
InputStream inputStream = socket.getInputStream();
while(true){
int read = inputStream.read(bytes);//阻塞
if(read!=-1){
System.out.println(new String(bytes,0,read));//输出客户端发送的数据
}else{
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
明显BIO有着许多问题。
1)每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
2)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
3)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
NIO
于是就出现了NIO(同步非阻塞IO)。Java中的NIO有三大核心部分:Channel、Buffer、Selector(如下图)。NIO是面向缓冲区的,或者说是面向块编程的——数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- 每个Channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个Channel。
- 上图也反映了有3个Channel注册到该Selector中去。
- 程序切换到哪个Channel是由事件决定的,基于此,Event就是一个重要的概念。
- Selector会根据不同的事件,在各个通道上切换。
- Buffer就是一个内存块,底层是一个数组。
- 数据的读取和写入是通过Buffer,这个和BIO对比:BIO中要么是输入流,要么是输出流,不能双向,但是NIO中的Buffer是可以读也可以写的,需要flip方法转换。
- Channel是双向的,可以返回底层操作系统的情况,比如linux,底层操作系统通道就是双向的。
比较:
处理方式 | 是否阻塞 | 选择器 | |
BIO | 流(字节流和字符流) | 阻塞 | 无 |
NIO | 块(Channel和Buffer) | 非阻塞 | Selector监听多个Channel的事件 |
关于Selector:Selector 能够检测多个注册的通道上是否有事件发生 (注意多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
通过SelectionKey。
- 当客户端连接时,会通过ServerSocketChannel得到SocketChannel。
- Selector进行监听select方法,返回有事件发生的Channel的个数。
- 将SocketChannel注册到Selector上(register),一个selector上可以注册多个SocketChannel。
- 注册后返回一个SelectionKey,会和该Selector关联。
- 这样在有事件发生时能得到各个SelectionKey。
- 通过SelectionKey能反向获取SocketChannel(方法channel)。
- 可以通过得到的channel进行业务处理。
代码demo:
服务器端:
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel ->ServerSocket
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//得到一个Selector对象
Selector selector = Selector.open();
//绑定一个端口6666,在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while(true){
//这里我们等待1s,如果没有事件发生(连接事件)
if(selector.select(1000)==0){ //没有任何事件发生
System.out.println("服务器等待了1s,无连接");
continue;
}
//如果返回的>0,就获取到相关的selectionKeys集合
//1.如果返回的>0,表示已经获取到关注的事件
//2.selector.selectedKeys() 返回关注事件的集合
// 通过selectedKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历集合 Set<SelectionKey>,使用迭代器
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应的处理
if(key.isAcceptable()){//有新的客户端来连接,有新的客户端来连接
//给该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个socketChannel "+socketChannel.hashCode());
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将当前socketChannel 注册到selector,关注事件为OP_READ,同时给该socketChannel
//关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable()){ //发生 OP_READ
//通过key 反向获取对应channel
SocketChannel channel =(SocketChannel) key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer =(ByteBuffer) key.attachment();
channel.read(buffer);//读到channel
System.out.println("from 客户端 "+ new String(buffer.array()));
}
//手动从集合中移除当前的selectionKey,防止重复操作。
keyIterator.remove();
}
}
}
}
可以看出服务器端在一个大循环里面会轮询目前的selectionKeys集合,通过检查每一个key的状态(acceptable或readable)来判断现在是连接是连接事件或读写事件。
客户端
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞模式
socketChannel.configureBlocking(false);
//提供服务器端的ip和端口
InetSocketAddress inetSocketAddress =
new InetSocketAddress("localhost", 6666);
//连接服务器
if(!socketChannel.connect(inetSocketAddress)){
while(!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作");
}
}
//...如果连接成功,就发送数据
String str="hello,尚硅谷~";
//wraps a byte array into a buffer
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将buffer写入channel
socketChannel.write(buffer);
System.in.read();
}
}
AIO
JDK7引入了AIO(异步IO模型),在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。目前AIO还没有广泛应用。
零拷贝技术
零拷贝是网络编程的技术,很多性能优化都离不开这个技术点。在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。
- mmap:通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,可以减少内核空间到用户空间的拷贝次数。
- sendFile:Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
即从操作系统来看,零拷贝就是没有CPU拷贝。
线程模型介绍
不同的线程模型,对程序的性能有很大影响。
目前常用的线程模型有:
- 传统阻塞I/O线程模型
- Reactor模型
而Reactor线程模型根据数量和资源池的数量不同,有3种典型的实现。
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
传统阻塞式I/O服务模型
模型的特点:
- 采用阻塞式I/O模式获取输入的数据
- 每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。
问题:
- 当并发数很大时,就会创建大量的线程,占用很大系统资源。
- 连接创建后,如果当前线程没有数据可读,线程会阻塞在read上,造成线程资源浪费。
因此出现了Reactor线程模型
Reactor模型
- 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象中等待,无需阻塞等待所有连接。当某个连接有新的数据需要处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
- 基于线程池复用线程资源:不必再为每个连接创建独立的线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
整体思路如下图:
Reactor模型中的核心组成:
- Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序对I/O事件作出反应。它就像公司的电话接线员,接听来自客户的电话并将线路转移到适当的联系人。
- Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
单Reactor单线程模型
该模型的大致思路如下:
- Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求。
- Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发。
- 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理。
- 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应。
- Handler 会完成 Read→业务处理→Send 的完整业务流程。
优缺点比较:
优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成。
缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈;可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
使用场景:客户端的数量有限,业务处理非常快速,比如 Redis在业务处理的时间复杂度 O(1) 的情况。
单Reactor多线程
具体业务会分发给线程池来处理。
- Reactor 对象通过select 监控客户端请求事件, 收到事件后,通过dispatch进行分发。
- 如果建立连接请求, 则右Acceptor 通过accept 处理连接请求, 然后创建一个Handler对象处理完成连接后的各种事件。
- 如果不是连接请求,则由reactor分发调用连接对应的handler 来处理。
- handler 只负责响应事件,不做具体的业务处理, 通过read 读取数据后,会分发给后面的worker线程池的某个线程处理业务。
- worker 线程池会分配独立线程完成真正的业务,并将结果返回给handler。
- handler收到响应后,通过send 将结果返回给client。
优缺点比较:
优点:可以充分的利用多核cpu 的处理能力。
缺点:多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈。
主从Reactor多线程
三级模型:主Reactor线程只处理连接,I/O read/send分配给其他多个SubReactor中,然后其中的业务处理又分给各自的Worker线程池中。
- Reactor主线程 MainReactor 对象通过select 监听连接事件, 收到事件后,通过Acceptor 处理连接事件。
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给SubReactor线程。
- subreactor 将连接加入到连接队列进行监听,并创建handler进行各种事件处理。
- 当有新事件发生时, subreactor 就会调用对应的handler处理。
- handler 通过read 读取数据,分发给后面的worker 线程处理。
- worker 线程池分配独立的worker 线程进行业务处理,并返回结果。
- handler 收到响应的结果后,再通过send 将结果返回给client。
- Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor。
优缺点:
优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理;父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。
缺点:编程复杂度较高。
Reactor模式具有的优点:
- 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的。
- 可以最大程度地避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销。
- 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源。
- 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性。
Netty线程模型
基础版:
- BossGroup线程维护Selector,只关注Accept。
- 当收到Accept事件,获取到对应的SocketChannel,封装成NioSocketChannel并注册到Woker线程进行维护。
- 当Worker线程监听到selector中通道发生自己感兴趣的事件后,就进行处理(handler),这里handler已经加入通道。
详细版(重要):
1)Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
2)BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
3)NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
4)NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
5)NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
6)每个Boss NioEventLoop 循环执行的步骤有3步
- 轮询accept 事件
- 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker NIOEventLoop 上的 selector
- 处理任务队列的任务 , 即 runAllTasks
7) 每个 Worker NIOEventLoop 循环执行的步骤
- 轮询read, write 事件
- 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
- 处理任务队列的任务 , 即 runAllTasks
8) 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器(handler)
简介在此就结束,接下来我将就Netty模型就从demo代码开始深入源码剖析Netty模型的方方方面面,大家稍等哈。