netty入门(一)
1 Netty是什么
Netty是一个异步的,基于事件驱动的网络应用框架,用于快速开发高性能,高可靠的网络I/O程序。并且Netty主要针对TCP协议下,面对Client端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
Netty本质上是一个NIO框架,使用于服务器通讯相关的多种应用场景。
思考,NIO是一个同步非阻塞模型,而又说Netty是异步的,这不是会矛盾吗
首先,我们要知道,这两者是没有关系的,Netty所说的异步是指异步事件驱动,用户使用Channel进行IO操作,会立即返回。但是IO操作的任务是提交给Netty的NIO底层去进行处理的。打个比方,你给客服打电话请求注销手机卡,但客服可能不会马上帮你注销,会给你一个回复说注销后会发短信通知你(回调函数),接着,这个请求就交由底层相关注销手机卡的人员进行处理,处理完后再告诉客服,客服再发短信。
2 IO模型
IO模型的简单理解:就是用什么样的通道进行数据的发送和接收
Java支持3种网络编程模型I/O模式:BIO,NIO,AIO
BIO:同步阻塞,服务实现模式为一个连接对应一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成性能开销。
NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
AIO:异步非阻塞,AlO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
BIO,NIO,AIO适用场景分析
- BIO方式适用于连接数目比较小且固定的架构
- NIO方式适用于连接数目多且连接比较短的架构,比如聊天服务器,弹幕系统,服务器间通信等
- AIO方式适用于连接数目多且连接比较长的服务,比如相册服务器
BIO
BIO编程简单流程
服务器端启动一个serverSocket
客户端启动socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯
客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
如果有响应,客户端线程会等待请求结束后,在继续执行
NIO
NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
Channel,Buffer,Selector之间的关系
- 每个channel都会对应一个Bufer
- Selectot 对对应一个线程,一个线程对应多个channel(连接)
- 该图反应了有三个channel注册到该selector l程序
- 程序切换到哪个channel是有事件决定的, Event 就是一个重要的概念5)
- Selector会根据不同的事件,在各个通道上切换
- Buffer就是一个内存块,底层是有一个数组
- 数据的读取写入是通过Buffer,这个和BiO, BIO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写,需要flip方法切换
- channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的
BIO VS NIO
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块,I/o的效率比流I/o高很多
BIO是阻塞的,NIO则是非阻塞的
BlO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
3 NIO基本介绍
3.1 Buffer
缓冲区本质是一个可以读写数据的内存块,可以理解是一个容器对象。
在NIO中,Buffer是一个顶层父类,它是一个抽象类
Buffer类定义了所有的缓冲区都具有的四个属性提供关于其所包含的数据元素的信息
3.2 Channel
基本介绍
1)通道可以同时进行读写
2)通道可以实现异步读写数据
3)通道可以从缓冲中读数据,也可以写数据到缓冲
关于Buffer和 Channel的注意事项和细节
- ByteBuffer支持类型化的put和 get, put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有
BufferUnderflowException
异常。 - 可以将一个普通
Buffer
转成只读Buffer
- NIO还提供了
MappedByteBuffer
,可以让文件直接在内存(堆外的内存〉中进行修改,而如何同步到文件由NIO来完成 - 前面我们讲的读写操作,都是通过一个
Buffer
完成的,NIO还支持通过多个Buffer(即 Buffer数组)完成读写操作,即Scattering
和Gathering
3.3 Selector
基本介绍
- Java的 NIO,用非阻塞的I0方式。可以用一个线程,处理多个的客户端连接,就会使用到
selector
(选择器) Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。- 只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
4 零拷贝
4.1 基本介绍
零拷贝是指没有CPU拷贝的技术。
4.2 传统IO数据读写
在没有DMA技术时,数据的传输过程需要CPU的全程参与,这是很消耗性能的
什么是DMA(直接内存访问)技术呢?在进⾏ I/O 设备和内存的数据传输的时候,数据搬运的⼯作全部交给DMA控制器,⽽CPU不再参与任何与数据搬运相关的事情,这样CPU就可以去处理别的事务。
加入该技术,就有了下图
继续分析该读写方式有多糟糕
从上图可以看出,期间发生了4次用户态与内核态的上下文切换,以及4次数据拷贝
由此read
和write
是系统调用,每次都会发生2次上下文切换
而4次数据拷贝是
- 第⼀次拷⻉,把磁盘上的数据拷⻉到操作系统内核的缓冲区⾥,这个拷⻉的过程是通过 DMA 搬运的
- 第⼆次拷⻉,把内核缓冲区的数据拷⻉到⽤户的缓冲区⾥,于是我们应⽤程序就可以使⽤这部分数据了,这个拷⻉到过程是由 CPU 完成的。
- 第三次拷⻉,把刚才拷⻉到⽤户的缓冲区⾥的数据,再拷⻉到内核的 socket 的缓冲区⾥,这个过程依然还是由 CPU 搬运的。
- 第四次拷⻉,把内核的 socket 缓冲区⾥的数据,拷⻉到⽹卡的缓冲区⾥,这个过程⼜是由 DMA 搬运的。
4.3 mmap优化
mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据映射到⽤户空间,这样,操作系统内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。
这里当用户态调用write
时,操作系统会将内核缓冲区的数据拷贝到socket缓冲区中,这发生在内核态,从而减少一次数据拷贝的过程
但这仍然需要4次上下文切换,3次数据拷贝
4.4 sendFile优化
在 Linux 内核版本 2.1 中,提供了⼀个专⻔发送⽂件的系统调⽤函数 sendfile()
。
1 |
|
该系统调用的好处有二:
- 可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。
- 可以直接把内核缓冲区⾥的数据拷⻉到 socket 缓冲区⾥,不再拷⻉到⽤户态,这样就只有 2 次上下⽂切换,和 3 次数据拷⻉。
但这还不是真正零拷贝,可以发现图中②还有个CPU拷贝
只有当网卡支持SG-DMA技术时,才可以实现真正的零拷贝
使用SG-DMA技术如下图
- 第⼀步,通过 DMA 将磁盘上的数据拷⻉到内核缓冲区⾥;
- 第⼆步,缓冲区描述符和数据⻓度传到 socket 缓冲区,这样⽹卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷⻉到⽹卡的缓冲区⾥,此过程不需要将数据从操作系统内核缓冲区拷⻉到 socket缓冲区中,这样就减少了⼀次数据拷⻉;
5 线程模型
5.1 基本介绍
从不同角度来看,对操作系统来说,线程模型往往指的是用户线程和内核线程的对应关系。而在本文中,指的是线程/进程处理连接请求的一种模型。
前文我们知道,当一个线程要处理很多请求时,使用read
不断轮询判断是否有数据是十分低效的,而由此衍生出的IO多路复用
就是解决该问题的方案,它会用一个系统调用函数来监听所有关心的连接,也就是可以在一个监控线程里面监控很多连接。它的检测方法如下:
- 如果没有事件发⽣,线程只需阻塞在这个系统调⽤,⽽⽆需像前⾯的线程池⽅案那样轮训调⽤
read
操作来判断是否有数据。 - 如果有事件发⽣,内核会返回产⽣了事件的连接,线程就会从阻塞状态返回,然后在⽤户态中再处理这些连接对应的业务即可。
但由于传统的IO多路复用编写程序十分繁琐,开发效率不高,由此衍生除了Reactor
模式。
5.2 传统阻塞I/O服务模型
可以看出来,这就是种阻塞IO,每个连接都需要独立的线程去进行处理,当并发数很大时,就会创建大量线程,占用很大系统资源。而且当连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read
操作,造成线程资源浪费。
5.3 Reactor模式
Reactor
模式指的是对事件反应,也就是来了一个事件,Reactor就有相应的反应/响应。即即 I/O 多路复⽤监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程/线程。
Reactor
模式主要由Reactor和处理资源池这两个核⼼部分组成,它俩负责的事情如下:
Reactor
负责监听和分发事件,事件类型包含连接事件、读写事件;处理资源池负责处理事件,如
read
-> 业务逻辑 ->send
;
Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:
Reactor的数量可以只有⼀个,也可以有多个;
处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程
由此,产生了四种方案:
单
Reactor
单进程 / 线程;单
Reactor
多进程 / 线程;多
Reactor
单进程 / 线程;多
Reactor
多进程 / 线程
由于,多Reactor
单进程 / 线程实现⽅案相⽐单Reactor
单进程 / 线程⽅案,不仅复杂⽽且也没有性能优势,因此实际中并没有应⽤。
5.3.1 单Reactor单进程/线程
下面介绍单进程的情况,在c实现中,一般都是这种,因为 C 语编写完的程序,运⾏后就是⼀个独⽴的进程,不需要在进程中再创建线程。而Java 程序是跑在 Java 虚拟机这个进程上⾯的,虚拟机中有很多线程,我们写的 Java 程序只是其中的⼀个线程⽽已。
可以看到进程⾥有 Reactor、Acceptor、Handler这三个对象:
Reactor 对象的作⽤是监听和分发事件;
Acceptor 对象的作⽤是获取连接;
Handler 对象的作⽤是处理业务;
对象⾥的 select
、accept
、read
、send
是系统调⽤函数,dispatch
和 「业务处理」是需要完成的操作,其中 dispatch
是分发事件操作。
流程
Reactor
对象通过select
(IO 多路复⽤接⼝) 监听事件,收到事件后通过dispatch
进⾏分发,具体分发给Acceptor
对象还是Handler
对象,还要看收到的事件类型;- 如果是连接建⽴的事件,则交由
Acceptor
对象进⾏处理,Acceptor
对象会通过accept
⽅法 获取连接,并创建⼀个Handler
对象来处理后续的响应事件; - 如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;
Handler
对象通过read
-> 业务处理 ->send
的流程来完成完整的业务流程。
优缺点
优点:全部⼯作都在同⼀个进程内完成,所以实现起来⽐较简单,不需要考虑进程间通信,也不⽤担⼼多进程竞争。
缺点:
- 第⼀个缺点,因为只有⼀个进程,⽆法充分利⽤ 多核 CPU 的性能;
- 第⼆个缺点,Handler 对象在业务处理时,整个进程是⽆法处理其他连接的事件的,如果业务处理耗
所以单 Reactor 单进程的⽅案不适⽤计算机密集型的场景,只适⽤于业务处理⾮常快速的场景
5.3.2 单Reactor多线程/进程
详细说⼀下这个⽅案:
Reactor
对象通过select
(IO 多路复⽤接⼝) 监听事件,收到事件后通过dispatch
进⾏分发,具体分发给Acceptor
对象还是Handler
对象,还要看收到的事件类型;- 如果是连接建⽴的事件,则交由
Acceptor
对象进⾏处理,Acceptor
对象会通过accept
⽅法 获取连接,并创建⼀个Handler
对象来处理后续的响应事件; - 如果不是连接建⽴事件, 则交由当前连接对应的 Handler 对象来进⾏响应;
上⾯的三个步骤和单 Reactor
单线程⽅案是⼀样的,接下来的步骤就开始不⼀样了:
Handler
对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过read
读取到数据后,会将数据发给⼦线程⾥的 Processor 对象进⾏业务处理;⼦线程⾥的
Processor
对象就进⾏业务处理,处理完后,将结果发给主线程中的Handler
对象,接着由Handler
通过send
⽅法将响应结果发送给client
;
优缺点
优点
能够充分利用多核CPU的处理能力
缺点
- 涉及共享数据的竞争
- 因为⼀个Reactor对象承担所有事件的监听和响应,⽽且只在主线程中运⾏,在⾯对瞬间⾼并发的场景时,容易成为性能的瓶颈的地⽅。
5.3.3 多Reactor多进程/线程
⽅案详细说明如下:
主线程中的
MainReactor
对象通过select
监控连接建⽴事件,收到事件后通过Acceptor
对象中的accept
获取连接,将新的连接分配给某个⼦线程;⼦线程中的
SubReactor
对象将MainReactor
对象分配的连接加⼊select
继续进⾏监听,并创建⼀个Handler
⽤于处理连接的响应事件。如果有新的事件发⽣时,
SubReactor
对象会调⽤当前连接对应的Handler
对象来进⾏响应。Handler
对象通过read
-> 业务处理 ->send
的流程来完成完整的业务流程。
多 Reactor
多线程的⽅案虽然看起来复杂的,但是实际实现时⽐单Reactor
多线程的⽅案要简单的多,原因如下:
主线程和⼦线程分⼯明确,主线程只负责接收新连接,⼦线程负责完成后续的业务处理。
主线程和⼦线程的交互很简单,主线程只需要把新连接传给⼦线程,⼦线程⽆须返回数据,直接就可以在⼦线程将处理结果发送给客户端。
Netty
和Memcache
都采用了多Reactor多线程的方案
总结
生活案例理解
- 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服
- 单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
- 主从 Reactor 多线程,多个前台接待员,多个服务生
参考
【网络编程】Netty采用的NIO为什么是同步非阻塞的?_牛客博客 (nowcoder.net)
《图解系统》——小林coding