共计 9819 个字符,预计需要花费 25 分钟才能阅读完成。
前言
后面说过 Java 中的 IO 操作,然而传统的 IO 是阻塞模式的,在高并发的零碎中必定是不可行的,这次咱们来介绍 Java 中提供的另外一种 IO 操作 –NIO。
首先,咱们要晓得应用程序中的 IO 和操作系统的 IO 是有区别的,应用程序的 IO 最终都须要依附操作系统的 IO 来实现最终的操作。这时候就须要抉择适合的 IO 模型,常见的 IO 模型有四种:
- 同步阻塞 IO(Blocking IO)
阻塞 IO 指的是须要内核 IO 操作彻底实现后,能力继续执行上面的操作。Java 中传统的 IO 和 socket 默认都是同步阻塞 IO
- 同步非阻塞 IO(Non-blocking IO)
非阻塞 IO 指的是不须要期待内核 IO 执行完,能够立刻返回用户空间,持续上面的操作,此时内核会给用户一个状态值。很多人认为 Java 中的 NIO 就是 Non-blocking IO 的缩写,也就是同步非阻塞 IO,其实不是的,Java 中 NIO 是 new IO,指的是上面 IO 多路复用模型。
- IO 多路复用
IO 多路复用指的是一个线程能够监督多个文件描述符,一旦某个文件描述符就绪(可读 / 可写),内核就会将就绪状态返回给应用程序,应用程序依据就绪状态,进行相应的 IO 操作。
- 异步 IO
异步 IO 相似于回调模式,用户空间向内核空间注册了各种 IO 事件的回调函数,由内核去被动调用。
四种 IO 模式总结:
同步阻塞 IO 模型是 Java 中传统 IO 默认应用的,个别零碎并发不高,应用此模型还是能够的;同步非阻塞 IO 模型听起来由原来的同步转成了异步,然而此模型须要用户线程始终去询问内核是否就绪,这就会占用大量的 CPU 工夫,在高并发的状况下此模型显然是不可用的;IO 多路复用模型,是当初支流的高并发下的 IO 模型;异步 IO 模型,实践上是性能最高的一种 IO 模型,然而很多操作系统底层还不欠缺,因而在性能上没有显著的劣势。
Java NIO
当初很多支流的框架和中间件都采纳了 Java 的 NIO,比方 tomcat、Netty 等。下面说到,Java 中的 NIO 采纳的是 IO 多路复用模型,此模型就是经典的 Reactor 反应器模式
Java NIO 由上面三个外围组件组成:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
Channel(通道)
在传统的 IO 中,所有的 IO 操作都须要通过输出流和输入流来实现;然而在 NIO 中,所有的 IO 操作都是从通道开始的,一个通道既能够输出也能够输入。
Buffer(缓冲区)
应用程序和通道交互就须要通过缓冲区,通道的读取就是将数据从通道写入到缓冲区,通道的写入就是将数据从缓冲区写入到通道中。
Selector(选择器)
Java 中的 NIO 是一种 IO 多路复用模型,那它是通过什么实现的呢?这就须要依附它的第三个组件 –Selector 选择器。通过选择器,一个线程能够查问多个通道的 IO 事件的就绪状态。
组件详解
Buffer 缓冲区
Buffer 类是一个抽象类,在 NIO 中有 8 种缓冲区类,别离如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedBuffer。其中应用最广的是 ByteBuffer。
Buffer 的重要属性
- capacity(容量)
初始化 Buffer 时,须要指定缓冲区的容量,当写入的数据超过这个容量时就不能再写入了。缓冲区的容量一旦初始化,就不能再扭转了,这是因为 Buffer 实质上就是一个内存块,相当于一个数组,内存调配好当前,它的大小就不能扭转了。capacity 容量不是 byte[]数组的字节数量,而是写入的数据对象的数量。
- position(读写地位)
在写模式下:刚进入写模式时,position 为 0,示意从头开始写,每写入一个数据,position 地位都往后移一位,当 position 达到 limit- 1 的时候,就不能再写入了。
在读模式下:刚进入读模式时,position 会重置为 0,每读取一个数据时,position 地位都往后移一位,当 position 达到 limit- 1 的时候,就没有数据可读了。
- limit(读写的限度)
在写模式下,limit 示意最大下限,等于 capacity 值。当切换到读模式时,limit 会变成切换前写模式时的 position 地位,而 position 则重置为 0。
Buffer 的重要办法
- allocate() 创立缓存区并初始化
- put() 写入数据
- flip() 翻转(进入读模式)
- get() 获取数据
- rewind() 倒带(反复读)
- mark()/reset() 标记 / 重置(从地位从新读取)
- clear() 革除(进入写模式)
public class NioDemo {public void test() {IntBuffer intBuffer = IntBuffer.allocate(10); | |
print("初始化", intBuffer); | |
for (int i = 0; i < 6; i++) {intBuffer.put(i); | |
} | |
print("写入 6 个数据", intBuffer); | |
intBuffer.flip(); | |
print("翻转后进入读模式", intBuffer); | |
for (int i = 0; i < 2; i++) {intBuffer.get(); | |
} | |
print("读取 2 个数据", intBuffer); | |
intBuffer.rewind(); | |
print("倒带", intBuffer); | |
for (int i = 0; i < 6; i++) {if (i == 3) {intBuffer.mark(); | |
} | |
intBuffer.get();} | |
print("读取 3 个数据,并保留第四个地位", intBuffer); | |
intBuffer.reset(); | |
for (int i = 3; i < 6; i++) {intBuffer.get(); | |
} | |
print("reset 后读取数据", intBuffer); | |
intBuffer.clear(); | |
print("clear 后进入写模式", intBuffer); | |
} | |
private void print(String name,IntBuffer intBuffer) {System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>" + name); | |
System.out.println(intBuffer.toString()); | |
} | |
public static void main(String[] args) {NioDemo demo = new NioDemo(); | |
demo.test();} | |
} | |
// 输入 | |
>>>>>>>>>>>>>>>>>>>>>>>>> 初始化 | |
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>> 写入 6 个数据 | |
java.nio.HeapIntBuffer[pos=6 lim=10 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>> 翻转 | |
java.nio.HeapIntBuffer[pos=0 lim=6 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>> 读取 2 个数据 | |
java.nio.HeapIntBuffer[pos=2 lim=6 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>> 倒带 | |
java.nio.HeapIntBuffer[pos=0 lim=6 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>> 读取 3 个数据,并保留第四个地位 | |
java.nio.HeapIntBuffer[pos=6 lim=6 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>>reset 后读取数据 | |
java.nio.HeapIntBuffer[pos=6 lim=6 cap=10] | |
>>>>>>>>>>>>>>>>>>>>>>>>>clear 后进入写模式 | |
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10] |
Channel 通道
Java NIO 中有四种常见的 Channel:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
- FileChannel:文件通道,用于文件的读写。
- SocketChannel:套接字通道,用于 TCP 连贯的数据读写。
- ServerSocketChannel:服务端套接字通道,用于监听 TCP 连贯。
- DatagramChannel:数据报通道,用于 UDP 连贯的数据读写。
FileChannel 文件通道
FileChannel 为阻塞模式,不能设置为非阻塞模式。
上面通过一个简略的复制文件来介绍 FileChannel 的用法
public class ChannelDemo { | |
/** | |
* 获取输出通道 | |
* @param filePath | |
* @return | |
* @throws Exception | |
*/ | |
public FileChannel getInputChannel(String filePath) throws Exception {FileInputStream inputStream = new FileInputStream(filePath); | |
return inputStream.getChannel();} | |
/** | |
* 获取输入通道 | |
* @param targetPath | |
* @return | |
* @throws Exception | |
*/ | |
public FileChannel getOutputChannel(String targetPath) throws Exception {FileOutputStream outputStream = new FileOutputStream(targetPath); | |
return outputStream.getChannel();} | |
/** | |
* 数据从输出通道读取到缓冲区,再从缓冲区读取到输入通道 | |
* @param inputChannel | |
* @param outputChannle | |
* @throws Exception | |
*/ | |
public void copyFile(FileChannel inputChannel,FileChannel outputChannle) throws Exception {ByteBuffer byteBuffer = ByteBuffer.allocate(1024); | |
// 初始是写入模式 | |
while (inputChannel.read(byteBuffer) != -1) {System.out.println("byteBuffer 写入了"+byteBuffer.position()+"个数据"); | |
// 变成读取模式 | |
byteBuffer.flip(); | |
while (outputChannle.write(byteBuffer) != 0) {System.out.println("byteBuffer 读取到 outputChannle 实现"); | |
} | |
// 读取完转为写入模式 | |
byteBuffer.clear();} | |
inputChannel.close(); | |
outputChannle.close();} | |
public static void main(String[] args) throws Exception{ChannelDemo demo = new ChannelDemo(); | |
FileChannel inputChannel = demo.getInputChannel("c:/soft/test/hello.txt"); | |
FileChannel outputChannel = demo.getOutputChannel("c:/soft/test/hello-copy.txt"); | |
demo.copyFile(inputChannel, outputChannel); | |
} | |
} |
SocketChannel 套接字通道
socketChannel 负责 socket 传输,作用于客户端和服务端,反对阻塞和非阻塞模式,个别都是应用非阻塞模式,应用 socketChannel.configureBlocking(false)
设置为非阻塞模式。
上面通过一个简略的客户端发送 socket 连贯示例来介绍 SocketChannel
public class SocketClientDemo {public void send(byte[] bytes) throws Exception {// 通过 SocketChannel 的静态方法 open()获取 socketChannel 实例 | |
SocketChannel socketChannel = SocketChannel.open(); | |
// 连贯服务端的 ip 和端口 | |
socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",1234)); | |
// 设置为非阻塞 | |
socketChannel.configureBlocking(false); | |
// 因为非阻塞的,连贯会立刻返回,所以此处须要轮询,查看是否实现连贯 | |
while (!socketChannel.finishConnect()) {System.out.println("还没连贯上,重试。。。"); | |
} | |
System.out.println("连贯胜利!"); | |
// 应用缓存区发送数据 | |
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); | |
// 向缓冲区中写入数据 | |
byteBuffer.put(bytes); | |
// 翻转,缓存区变成读取模式 | |
byteBuffer.flip(); | |
// 缓冲区中数据写入到 socketChannel 通道中 | |
socketChannel.write(byteBuffer); | |
System.out.println("写入实现"); | |
// 敞开连贯,失常状况下在 finally 中敞开,这里只是简略演示 | |
socketChannel.close();} | |
public static void main(String[] args) throws Exception {SocketClientDemo demo = new SocketClientDemo(); | |
demo.send("hello".getBytes()); | |
} | |
} |
ServerSocketChannel 是服务端连贯 socket 数据的通道,具体用例联合前面介绍的选择器一起介绍。
DatagramChannel 数据报通道
DatagramChannel 通道用来解决 UDP 协定的,UDP 协定和 TCP 协定不一样,它间接通过 IP 和端口就能够间接向对方发送数据,不须要建设连贯。
上面通过简略的客户端用例来介绍 DatagramChannel 用法
public class DatagramClient {public void send(String data) throws Exception{DatagramChannel datagramChannel = DatagramChannel.open(); | |
datagramChannel.configureBlocking(false); | |
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); | |
byteBuffer.put(data.getBytes()); | |
byteBuffer.flip(); | |
datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 4444)); | |
datagramChannel.close();} | |
public static void main(String[] args) throws Exception{new DatagramClient().send("hello udp"); | |
} | |
} |
DatagramChannel 服务端的用法也是通过上面的选择器一起介绍。
Selector 选择器
Selector 是第三个重要的组件,NIO 次要是通过这个组件来实现多路复用。一个通道代表一个连贯,通过选择器来同时监控多个通道的变动。通道首先通过 register 实现在选择器上的注册,注册时须要两个参数,一个是选择器实例,一个是要监控的 IO 事件类型,这个事件类型有上面四种:
- SelectionKey.OP_READ 可读
- SelectionKey.OP_WRITE 可写
- SelectionKey.OP_CONNECT 连贯
- SelectionKey.OP_ACCEPT 承受
上面通过简略的 DatagramChannel 服务端的代码来介绍 Selector 的应用
public class DatagramServer {public void accept() throws Exception {DatagramChannel datagramChannel = DatagramChannel.open(); | |
datagramChannel.configureBlocking(false); | |
datagramChannel.bind(new InetSocketAddress(4444)); | |
// 通过 Selector 的 open()办法获取选择器实例 | |
Selector selector = Selector.open(); | |
// 通道注册到选择器上 | |
datagramChannel.register(selector, SelectionKey.OP_READ); | |
System.out.println("开启监听"); | |
// 轮询 | |
while (selector.select() > 0) { | |
// 获取选择器上所有变动的选择键 | |
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); | |
// 遍历 | |
while (iterator.hasNext()) { | |
// 获取具体的选择键实例 | |
SelectionKey selectionKey = iterator.next(); | |
// 可读事件 | |
if (selectionKey.isReadable()) { | |
// 上面实现具体可读当前的解决 | |
System.out.println("可读"); | |
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); | |
// 和 ServerSocketChannel 承受数据不一样,这里应用 receive | |
datagramChannel.receive(byteBuffer); | |
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit())); | |
} | |
} | |
// 移除此选择键,避免反复读取 | |
iterator.remove();} | |
selector.close(); | |
datagramChannel.close();} | |
public static void main(String[] args) throws Exception{new DatagramServer().accept();} | |
} |
总结一下 Selector 选择器的应用步骤:首先获取选择器实例,而后将通道注册到选择器实例上,接着选出监听的 IO 事件,最初进行具体的解决逻辑。
上面介绍一下平时罕用的 Socket 服务端程序,看看通过 Selector 选择器怎么实现多路复用,也就是一个线程实现多个 IO 事件的处理程序。
public class SocketServerDemo {public void accept() throws Exception { | |
// 获取选择器实例 | |
Selector selector = Selector.open(); | |
// 获取 ServerSocketChannel 实例 | |
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); | |
// 设置为非阻塞模式 | |
serverSocketChannel.configureBlocking(false); | |
// 监听 5555 端口 | |
serverSocketChannel.bind(new InetSocketAddress(5555)); | |
// ServerSocketChannel 通道注册到选择器上,并监听可连贯的事件 | |
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); | |
// 轮询 | |
while (selector.select() > 0) { | |
// 所有选择键 | |
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); | |
while (iterator.hasNext()) { | |
// 获取具体选择键实例 | |
SelectionKey selectionKey = iterator.next(); | |
// 如果有连贯事件,阐明有客户端通过此端口连贯到服务端 | |
if (selectionKey.isAcceptable()) { | |
// 进行可连贯的解决逻辑 | |
handlerAccept(selectionKey); | |
} else if (selectionKey.isReadable()) {handlerRead(selectionKey); | |
} | |
} | |
// 移除,避免反复生产 | |
iterator.remove();} | |
serverSocketChannel.close();} | |
private void handlerAccept(SelectionKey selectionKey) throws IOException {System.out.println("解决连贯"); | |
// 获取 SocketChannel 实例,用来进去数据传输 | |
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept(); | |
// 设置为非阻塞模式 | |
socketChannel.configureBlocking(false); | |
// 将 SocketChannel 通道注册到选择器上,并监听可读取事件 | |
socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); | |
} | |
private void handlerRead(SelectionKey selectionKey) throws Exception {System.out.println("解决数据"); | |
// 获取选择键上的 SocketChannel | |
SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); | |
// 设置为非阻塞模式 | |
socketChannel.configureBlocking(false); | |
// 数据读取到缓冲区中 | |
ByteBuffer byteBuffer = ByteBuffer.allocate(1024); | |
if (socketChannel.read(byteBuffer) == -1) {System.out.println("无数据,敞开"); | |
socketChannel.close(); | |
return; | |
} | |
String s = new String(byteBuffer.array()).trim(); | |
// 打印客户端传过来的数据 | |
System.out.println(s); | |
System.out.println("读取实现"); | |
} | |
public static void main(String[] args) throws Exception {new SocketServerDemo().accept();} | |
} |
总结
Java 的 NIO 次要通过 Buffer、Channel 和 Selector 三个组件来实现多路复用的 IO 操作,首先将通道注册到选择器上,而后查问选择器上对应的选择键,获取监听的 IO 事件,接着将通道中数据读取或写入缓冲区中,实现对应的事件处理。
下面次要是介绍了 Java 中 NIO 的简略操作,外面还有很多待优化的中央,比方监听逻辑和读写逻辑都注册在同一个选择器上,如果读写耗时,还是会阻塞监听逻辑的。前面通过介绍 Reactor 模式来解决这些问题。
<center> 扫一扫,关注我 </center>