NIO
基本介绍
三大核心
- Selector
- Channel
- Buffer
关系图
graph TD
t[Thread]
s[Selector]
c1[Channel]
c2[Channel]
c3[Channel]
b1[Buffer]
b2[Buffer]
b3[Buffer]
p1[程序]
p2[程序]
p3[程序]
t-->s
s-->c1-->b1-->p1
s-->c2-->b2-->p2
s-->c3-->b3-->p3
%%{init: {'theme':'dark'}}%%
graph TD
t[Thread]
s[Selector]
c1[Channel]
c2[Channel]
c3[Channel]
b1[Buffer]
b2[Buffer]
b3[Buffer]
p1[程序]
p2[程序]
p3[程序]
t-->s
s-->c1-->b1-->p1
s-->c2-->b2-->p2
s-->c3-->b3-->p3
关系描述
- 每个 Channel 都对应一个 Buffer
- Selector 对应一个线程,一个线程对应多个 Channel(连接)
- 该图反应了有 3 个 Channel 注册到了该 Selector
- 程序切换到哪个 Channel,是由事件决定的,Event 是一个很重要的概念
- Selector 会根据不同的事件,在各个通道上切换
- Buffer 就是一个内存块,底层是一个数组
- 数据的读取和写入是通过 Buffer,与 BIO 不同,BIO 是直接用流,BIO 要么是输入流,要么是输出流,不能双向。NIO 是可读可写的,但需要
filp
方法进行切换 - Channel 是双向的,可以反应底层操作系统的情况。Linux 底层 OS 的通道就是双向的。
缓冲区 Buffer
基本介绍
本质上是一个可以读写数据的内存块,可以理解为一个容器对象,提供了一组方法,可以更轻松地使用内存块,内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从网络、文件读取数据的渠道,但读写的数据必须经由 Buffer。
graph LR
n[NIO]
b[Buffer]
f[File]
n== data ===b
b== channel ===f
%%{init: {'theme':'dark'}}%%
graph LR
n[NIO]
b[Buffer]
f[File]
n== data ===b
b== channel ===f
Buffer 类及其子类
Buffer 类的属性
4 个属性提供关于其包含的数据元素的信息
属性 | 描述 |
---|---|
capacity | 容量 |
limit | 缓冲区当前终点,不能对超过极限的数据进行操作 |
position | 位置,下一个要被读写的数据的索引 |
mark | 标记 |
子类
没有 Boolean 类型的 Buffer
- ByteBuffer
- IntBuffer
- FloatBuffer
- DoubleBuffer
- …
Buffer 操作
Channel 通道
基本介绍
-
类似于流,但有所区别
- 通道可以同时进行读写,流只能“半双工”
- 通道可以实现异步读写数据
- 通道可以从缓冲区读数据,也可以写数据到缓冲
-
BIO 是单向的,只能进行读取操作
-
Channel 在 NIO 中是一个接口
-
常用的 Channel 的实现
- FileChannel 用于文件的读写
- DatagramChannel 用于 UDP 数据读写
- ServerSocketChannel, SocketChannel 用于 TCP 数据读写
样例
文件通道
写入
graph LR
s[""hello""]
b[ByteBuffer]
subgraph Java输入流对象
n[NIOFileChannel]
end
f{{文件}}
s-->b-->n-->f
%%{init: {'theme':'dark'}}%%
graph LR
s[""hello""]
b[ByteBuffer]
subgraph Java输入流对象
n[NIOFileChannel]
end
f{{文件}}
s-->b-->n-->f
读取
graph LR
s[""hello""]
b[ByteBuffer]
subgraph Java输入流对象
n[NIOFileChannel]
end
f{{文件}}
f-->n-->b-->s
%%{init: {'theme':'dark'}}%%
graph LR
s[""hello""]
b[ByteBuffer]
subgraph Java输入流对象
n[NIOFileChannel]
end
f{{文件}}
f-->n-->b-->s
拷贝
graph LR
b[ByteBuffer]
subgraph Java输入流对象1
n1[NIOFileChannel]
end
subgraph Java输入流对象2
n2[NIOFileChannel]
end
f1{{1.txt}}
f2{{2.txt}}
f1-->n1-->b-->n2-->f2
%%{init: {'theme':'dark'}}%%
graph LR
b[ByteBuffer]
subgraph Java输入流对象1
n1[NIOFileChannel]
end
subgraph Java输入流对象2
n2[NIOFileChannel]
end
f1{{1.txt}}
f2{{2.txt}}
f1-->n1-->b-->n2-->f2
注意事项和细节
- ByteBuffer 支持类型化的 put 和 get,放入什么类型,就需要以什么类型取出,否则有可能发生异常,或者数据出现错误
- 可以将一个普通 Buffer 转为只读 Buffer
- NIO 提供 MappedByteBuffer,可以让文件直接在内存中进行修改,而如何同步到文件由 NIO 完成
- NIO 还支持通过多个 Buffer 完成读写操作
- Scattering 将数据写入到 buffer 时,可以采用 buffer 数组,依次写入(分散)
- Gathering 将数据从 buffer 读取时,可以采用 buffer 数组,依次读取
NIO 传统模式编程
阻塞模式
在没有数据可读时,包括数据复制过程中,线程必须阻塞,不会占用 CPU,但线程相当于闲置。此处服务器有两处会造成阻塞。
非阻塞模式
- 在某个 Channel 没有可读事件时,线程不必阻塞,它可以去处理其他有可读事件的 Channel
- 数据复制过程中,线程实际还是阻塞的
注意:这种非阻塞方式容易导致线程空转,CPU 利用率一直很高,因此几乎不怎么用
Selector 选择器
基本介绍
- 用非阻塞的 IO 方式,可以使用一个线程,处理多个客户端连接
- Selector 能够检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector),如果由事件发生,便获取事件然后针对每个事件进行相应处理
- 只有在连接真正由读写时间发生时,才会进行读写,大大减少了系统的开销,并且不必为每个连接都创建一个线程,不用维护多个线程
- 避免了多线程之间的上下文切换导致的开销
注意事项
- NIO 中的 ServerSocketChannel 功能类似 ServerSocket,SocketChanel 功能类似 Socket
- Selector 相关方法
- open() 得到一个选择器对象
- select() 阻塞
- int select(Long timeout) 阻塞一点时间,在超时后返回,对应 SelectionKey 加入到内部集合中并返回
- Set<SelectionKey> selectedKeys() 从内部稽核中得到所有的 SelectionKey
- wakeup() 唤醒
- selectNow() 不阻塞,立刻返回
分析图
graph TD
t[Thread]
s[Selector 实例]
t --- s
sk{{SelectionKey}}
s --- sk
sk --- reg1[注册]
sk --- reg2[注册]
sk --- reg3[注册]
sk --- reg4[注册]
reg1-->client1[Client]
reg2-->client2[Client]
reg3-->client3[Client]
reg4-->client4[Client]
client1-->reg1
client2-->reg2
client3-->reg3
client4-->reg4
server([服务器ServerSocketChannel
1. 监听端口
2. 获得和客户端连接的通道SocketChannel
3. 每个客户端都会生成对应通道SocketChannel]) server-->reg4 server-->s
1. 监听端口
2. 获得和客户端连接的通道SocketChannel
3. 每个客户端都会生成对应通道SocketChannel]) server-->reg4 server-->s
%%{init: {'theme':'dark'}}%%
graph TD
t[Thread]
s[Selector 实例]
t --- s
sk{{SelectionKey}}
s --- sk
sk --- reg1[注册]
sk --- reg2[注册]
sk --- reg3[注册]
sk --- reg4[注册]
reg1-->client1[Client]
reg2-->client2[Client]
reg3-->client3[Client]
reg4-->client4[Client]
client1-->reg1
client2-->reg2
client3-->reg3
client4-->reg4
server([服务器ServerSocketChannel
1. 监听端口
2. 获得和客户端连接的通道SocketChannel
3. 每个客户端都会生成对应通道SocketChannel]) server-->reg4 server-->s
1. 监听端口
2. 获得和客户端连接的通道SocketChannel
3. 每个客户端都会生成对应通道SocketChannel]) server-->reg4 server-->s
- 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
- 将 SocketChannel 注册到 Selector 上,
register(Selector sel, int ops)
,一个 Selector 上可以注册多个 SocketChannel - 注册后返回 SelectionKey,会和该 Selector 关联(集合)
- Selector 进行监听,
select
方法返回有事件发生的通道的个数 - 进而得到各个有事件发生的 SelectionKey
- 再通过 SelectionKey 反向获取 SocketChannel,方法
channel()
- 通过得到的 channel,完成业务处理
代码实例
服务器端(读客户端)
服务器(写客户端)
客户端
NIO 样例:聊天室
服务端
客户端
关于 ByteBuffer 的说明
- 每个 Channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer(具体体现在
key.attach(buffer)
) - ByteBuffer 不能太大,当连接数量为海量的话,需要的内存非常庞大。因此需要设计大小可变的 ByteBuffer(netty 的 ByteBuf)
- 先分配较小的 buffer,如果发现大小不够,再进行扩容,这样保证了数据的连续性,但是涉及到数据的拷贝
- 用多个 buffer 组成的数组构成 buffer,避免了频繁的拷贝,但是不保证数据的连续性