NIO
1. Channel
在NIO
中,基本所有的IO操作都是从Channel
开始的,Channel
通过Buffer(缓冲区)
进行读写操作。
read()
表示读取通道中数据到缓冲区,write()
表示把缓冲区数据写入到通道。
Channel
有好多实现类,这里有三个最常用:
-
SocketChannel
:一个客户端发起TCP连接的Channel -
ServerSocketChannel
:一个服务端监听新连接的TCP Channel,对于每一个新的Client连接,都会建立一个对应的SocketChannel -
FileChannel
:从文件中读写数据
其中SocketChannel
和ServerSocketChannel
是网络编程中最常用的,一会在最后的示例代码中会有讲解到具体用法。
2. Buffer
Buffer
也被成为内存缓冲区,本质上就是内存中的一块,我们可以将数据写入这块内存,之后从这块内存中读取数据。也可以将这块内存封装成NIO Buffer
对象,并提供一组常用的方法,方便我们对该块内存进行读写操作。我们可以将Buffer
理解为一个数组的封装,我们最常用的ByteBuffer
对应的数据结构就是byte[]。
Buffer
中有4个非常重要的属性:capacity、limit、position、mark
-
capacity
属性:容量,Buffer能够容纳的数据元素的最大值,在Buffer初始化创建的时候被赋值,而且不能被修改。
上图中,初始化Buffer的容量为8(图中从0~7,共8个元素),所以capacity = 8
-
limit
属性:代表Buffer可读可写的上限。-
写模式下:
limit
代表能写入数据的上限位置,这个时候limit = capacity
读模式下:在Buffer
完成所有数据写入后,通过调用flip()
方法,切换到读模式,此时limit
等于Buffer
中实际已经写入的数据大小。因为Buffer
可能没有被写满,所以limit<=capacity
-
-
position
属性:代表读取或者写入Buffer
的位置。默认为0。-
写模式下:每往
Buffer
中写入一个值,position
就会自动加1,代表下一次写入的位置。 -
读模式下:每往
Buffer
中读取一个值,position
就自动加1,代表下一次读取的位置。
-
从上图就能很清晰看出,读写模式下capacity、limit、position的关系了。
-
mark
属性:代表标记,通过mark()方法,记录当前position值,将position值赋值给mark,在后续的写入或读取过程中,可以通过reset()方法恢复当前position为mark记录的值。
创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.wrap("hello world".getBytes());
3. Selector
Selector
是NIO中最为重要的组件之一,我们常常说的多路复用器
就是指的Selector
组件。Selector
组件用于轮询一个或多个NIO Channel
的状态是否处于可读、可写。通过轮询的机制就可以管理多个Channel,也就是说可以管理多个网络连接。
轮询机制
-
首先,需要将Channel注册到Selector上,这样Selector才知道需要管理哪些Channel
-
接着Selector会不断轮询其上注册的Channel,如果某个Channel发生了读或写的时间,这个Channel就会被Selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的IO操作。
1.创建Selector
Selector selector = Selector.open();
2.注册Channel到Selector中
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
-
Connect事件
:连接完成事件( TCP 连接 ),仅适用于客户端,对应 SelectionKey.OP_CONNECT。 -
Accept事件
:接受新连接事件,仅适用于服务端,对应 SelectionKey.OP_ACCEPT 。 -
Read事件
:读事件,适用于两端,对应 SelectionKey.OP_READ ,表示 Buffer 可读。 -
Write事件
:写时间,适用于两端,对应 SelectionKey.OP_WRITE ,表示 Buffer 可写。
4.总结
回顾一下使用 NIO
开发服务端程序的步骤:
-
创建
ServerSocketChannel
和业务处理线程池。 -
绑定监听端口,并配置为非阻塞模式。
-
创建
Selector
,将之前创建的ServerSocketChannel
注册到Selector
上,监听SelectionKey.OP_ACCEPT
。 -
循环执行
Selector.select() 方法,轮询就绪的
Channel`。 -
轮询就绪的
Channel
时,如果是处于OP_ACCEPT
状态,说明是新的客户端接入,调用ServerSocketChannel.accept
接收新的客户端。 -
设置新接入的
SocketChannel
为非阻塞模式,并注册到Selector
上,监听OP_READ
。 -
如果轮询的
Channel
状态是OP_READ
,说明有新的就绪数据包需要读取,则构造ByteBuffer
对象,读取数据。
NIO 原生 API 的弊端 :
① NIO 组件复杂 : 使用原生 NIO
开发服务器端与客户端 , 需要涉及到 服务器套接字通道 ( ServerSocketChannel
) , 套接字通道 ( SocketChannel
) , 选择器 ( Selector
) , 缓冲区 ( ByteBuffer
) 等组件 , 这些组件的原理 和API 都要熟悉 , 才能进行 NIO
的开发与调试 , 之后还需要针对应用进行调试优化
② NIO 开发基础 : NIO
门槛略高 , 需要开发者掌握多线程、网络编程等才能开发并且优化 NIO
网络通信的应用程序
③ 原生 API 开发网络通信模块的基本的传输处理 : 网络传输不光是实现服务器端和客户端的数据传输功能 , 还要处理各种异常情况 , 如 连接断开重连机制 , 网络堵塞处理 , 异常处理 , 粘包处理 , 拆包处理 , 缓存机制 等方面的问题 , 这是所有成熟的网络应用程序都要具有的功能 , 否则只能说是入门级的 Demo
④ NIO BUG : NIO
本身存在一些 BUG , 如 Epoll
, 导致 选择器 ( Selector
) 空轮询 , 在 JDK 1.7 中还没有解决