Netty-Netty的底层实现:JavaNIO
Netty框架 - 前传
跟着韩顺平老师学完了Netty,深感这种底层框架使用起来必须得有很好的计算机网络的知识。
果然,转码的路上是不能避免学习基础课程的。
Netty简介
总结一句话, Netty是一个异步的,基于事件驱动的网络应用框架,用来开发高性能、高可靠性的网络IO程序。
Netty的应用场景也比较广泛,比如说:
- RPC框架中往往会使用Netty来作为基础的通信组件。Dubbo,Hadoop···
- 游戏行业会使用Netty来搭建服务器之间的通信。
I/O模型
如上文所说,Netty是一种网络IO程序。实际上,Netty的底层就是基于JAVA NIO实现的。
那么 NIO是什么呢?
Java中共支持了3种I/O模型:BIO,NIO,NIO2.0 ( 也称AIO )
BIO
- 即 Blocking IO
- 是传统的阻塞型IO。服务器会为每一个连接创建一个线程。当这个连接不做任何事情时,就会造成线程资源的浪费
NIO
- 即 Non-Blocking IO
- 同步非阻塞。服务器上每一个线程会处理多个请求,这种实现依赖于IO多路复用(Selector)。多路复用器使用轮询的方式处理I/O请求。
AIO
- 即Asynchronized IO
- 异步非阻塞。AIO引入了异步通道的概念,采用Proactor模式。有效的请求才启动线程。适用于连接数较多且连接时间较长的应用。
简单说说NIO
为什么要用NIO,BIO哪不行了?
BIO也就是Blocking-IO,指的是阻塞性IO。在连接数小的时候,BIO结合线程池是可以应付的过来的。但是,当请求数很大的时候,BIO就完全没有办法胜任了。因为在BIO中,每个请求都需要创建一个独立的线程,从而和客户端直接建立Socket连接,然后在这个线程上执行读写操作。当并发数大的时候,大量的线程都被创建用来建立连接,但也不一定马上进行读写操作,而是阻塞在那里,等待客户端发送请求。此时,线程资源就形成了极大的浪费。若线程池中所有线程都被使用,那么新的连接请求也无法完成,导致不可用。
NIO怎么就行了?
NIO是面向缓冲区,或面向块编程的。数据会读取到一个稍后处理的缓冲区中,需要的时候可以在缓冲区中前后移动,这样就提供了处理过程中的灵活性。
NIO是非阻塞的。具体表现就是,客户端与服务端建立的连接,需要从一个通道发送请求或读取数据,但只在通道中有数据或有请求的时候,才安排线程去执行任务。如果没有,线程也不会阻塞在那里,而是可以去做别的事情。那么如此,在NIO中,一个线程处理多个连接的任务就可以实现了,正所谓IO多路复用。当有10000个请求过来,使用NIO可能只需要100个线程就能同时处理,而BIO就一定得分配10000个线程才能同时处理。
比较NIO & BIO
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,NIO 则是非阻塞的。
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
- Buffer和Channel之间的数据流向是双向的
NIO三大组件 - Channel | Selector | Buffer
Buffer
缓冲区(Buffer
):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel
提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
。
- Buffer是一个抽象类,继承该类的有ByteBuffer,LongBuffer等。
- 在Buffer类里定义了4个重要的属性,mark,position,limit,capacity
- mark:标记
- position:下一个要被读或写的元素的索引位置,为下次读写做准备
- limit:表示缓冲区的当前终点,不能超过这个位置进行读写。limit可以修改。
- capacity:可以容纳的最大数据量,缓冲区创建时被设定,不能改变。
Channel
- NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
BIO
中的Stream
是单向的,例如FileInputStream
对象只能进行读取数据的操作,而NIO
中的通道(Channel
)是双向的,可以读操作,也可以写操作。Channel
在NIO
中是一个接口public interface Channel extends Closeable{}
- 常用的
Channel
类有:**FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
**。【ServerSocketChanne
类似ServerSocket
、SocketChannel
类似Socket
】 FileChannel
用于文件的数据读写,DatagramChannel
用于UDP
的数据读写,ServerSocketChannel
和SocketChannel
用于TCP
的数据读写。
Selector
Java
的NIO
,用非阻塞的IO
方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector
(选择器)。Selector
能够检测多个注册的通道上是否有事件发生(注意:多个Channel
以事件的方式可以注册到同一个Selector
),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。- 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销
Netty
的IO
线程NioEventLoop
聚合了Selector
(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。- 当线程从某客户端
Socket
通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。- 线程通常将非阻塞
IO
的空闲时间用于在其他通道上执行IO
操作,所以单独的线程可以管理多个输入和输出通道。- 由于读写操作都是非阻塞的,这就可以充分提升
IO
线程的运行效率,避免由于频繁I/O
阻塞导致的线程挂起。- 一个
I/O
线程可以并发处理N
个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O
一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
NIO网络编程原理图
- 当客户端连接时,会通过
ServerSocketChannel
得到SocketChannel
。Selector
进行监听select
方法,返回有事件发生的通道的个数。- 将
socketChannel
注册到Selector
上,register(Selector sel, int ops)
,一个Selector
上可以注册多个SocketChannel
。- 注册后返回一个
SelectionKey
,会和该Selector
关联(集合)。- 进一步得到各个
SelectionKey
(有事件发生)。- 在通过
SelectionKey
反向获取SocketChannel
,方法channel()
。- 可以通过得到的
channel
,完成业务处理。
实例 - NIO网络编程
Server
1 |
|
Client
1 |
|