1. 什么是NIO?

NIO (Non-blocking lO,非阻塞IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

  • NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
  • Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。

2. NIO 与 BIO

2.1 BIO

BIO全称是Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型。

Java BIO:服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示:

io流阻塞线程 java java非阻塞io_java

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。

2.2 NIO

Java NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有I/O请求就进行处理。

模型图:

io流阻塞线程 java java非阻塞io_数据_02

 

 一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理,NIO擅长1个线程管理多条连接,节约系统资源。

3. NIO实现原理

NIO 包含3个核心的组件:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。

1. Buffer(缓冲区)

缓冲区本质上是一块可以写入数据,可从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。

2. Channel(通道)

Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入,通道可以支持读取或写入缓冲区,也支持异步地读写。

Selector(选择器)

Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率。

NIO原理图:

io流阻塞线程 java java非阻塞io_io流阻塞线程 java_03

原理图说明:

  • 每个 Channel 对应一个 Buffer。
  • Selector 对应一个线程,一个线程对应多个 Channel。
  • 该图反应了有三个 Channel 注册到该 Selector。
  • 程序切换到那个 Channel 是由事件决定的(Event)。
  • Selector 会根据不同的事件,在各个通道上切换。
  • Buffer 就是一个内存块,底层是有一个数组。
  • 数据的读取和写入是通过 Buffer,但是需要flip()切换读写模式,而 BIO 是单向的,要么输入流要么输出流。
  • Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到lO设备(例如:磁盘文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据。

3.1 Channel(通道)

Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

NIO 的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

Channel 在 NIO 中是一个接口

public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;
}

常用的Channel实现类:

FileChannel:用于读取、写入、映射和操作文件的通道。

DatagramChannel:通过 UDP 读写网络中的数据通道。

SocketChannel:通过 TCP 读写网络中的数据。

ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 

3.1.1 FileChannel类

本地文件IO通道,用于读取、写入、映射和操作文件的通道,使用文件通道操作文件的一般流程为:

1. 获取通道

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket
  • 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过FileChannel静态方法 open() 打开并返回指定通道。
// 获取通道
FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);

2. 创建字节缓冲区

文件相关的字节缓冲区有两种,一种是基于堆的 HeapByteBuffer,另一种是基于文件映射,放在堆外内存中的 MappedByteBuffer。

// 分配字节缓存
ByteBuffer buf = ByteBuffer.allocate(10);

3. 读写操作

读取数据

一般需要一个循环结构来读取数据,读取数据时需要注意切换 ByteBuffer 的读写模式。

// 获取通道
FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
// 定义一个缓冲区
ByteBuffer buf= ByteBuffer.allocate(1024)
// 读取数据
String text = "";
while (channel.read(buf) != -1){ // 读取通道中的数据,并写入到 buf 中
    // flip():Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
    // 缓存区写模式切换到读模式
    buf.flip(); 
    // 可用but.hasRemaining()方法代替
    while (buf.position() < buf.limit()){
        // 读取 buf 中的数据
        text.append((char)buf.get());
    }
    buf.clear(); // 清空 buffer,缓存区切换到写模式
}
// 关闭通道
channel.close();

写入操作

// 获取通道
FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
// 定义一个缓冲区
ByteBuffer buf= ByteBuffer.allocate(1024)
// 写数据
String text = "hello,使用Buffer和channel实现写数据到文件中";
for (int i = 0; i < text.length(); i++) {
    buf.put((byte)text.charAt(i)); // 写入缓冲区,需要将 2 字节的 char 强转为 1 字节的 byte
    if (buf.position() == buf.limit() || i == text.length() - 1) { // 缓存区已满或者已经遍历到最后一个字符
        buf.flip(); // 将缓冲区由写模式置为读模式
        channel.write(buf); // 将缓冲区的数据写到通道
        buf.clear(); // 清空缓存区,将缓冲区置为写模式,下次才能使用
    }
}
// 将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 方法可以确保对文件的操作能够更新到磁盘。
channel.force(false);
// 关闭通道
channel.close();

4. 案例-使用Buffer完成文件复制

/**
     * 使用 FileChannel(通道) ,完成文件的拷贝。
     * @throws Exception
     */
    @Test
    public void copy() throws Exception {
        // 源文件
        File srcFile = new File("E:\\test\\Aurora-4k.jpg");
        File destFile = new File("E:\\test\\Aurora-4k-new.jpg");
        // 得到一个字节字节输入流
        FileInputStream fis = new FileInputStream(srcFile);
        // 得到一个字节输出流
        FileOutputStream fos = new FileOutputStream(destFile);
        // 得到的是文件通道
        FileChannel isChannel = fis.getChannel();
        FileChannel osChannel = fos.getChannel();
        // 分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while(isChannel.read(buffer)>0){
            // 已经读取了数据 ,把缓冲区的模式切换成可读模式
            buffer.flip();
            // 把数据写出到
            osChannel.write(buffer);//将buffer缓冲区中的数据写入到osChannel中
            // 必须先清空缓冲然后再写入数据到缓冲区
            buffer.clear();
        }
        isChannel.close();
        osChannel.close();
        System.out.println("复制完成!");
    }

5. 案例-分散读取 (Scatter) 和聚集写入 (Gather) 

  • 分散读取(Scatter ):是指把Channel通道的数据读入到 多个缓冲区中去
  • 聚集写入(Gathering )是指将多个 Buffer 中的数 据“聚集”到 Channel。
// 分散和聚集
@Test
public void test() throws IOException{
		RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
	//1. 获取通道
	FileChannel channel1 = raf1.getChannel();
	
	//2. 分配指定大小的缓冲区
	ByteBuffer buf1 = ByteBuffer.allocate(100);
	ByteBuffer buf2 = ByteBuffer.allocate(1024);
	
	//3. 分散读取
	ByteBuffer[] bufs = {buf1, buf2};
	channel1.read(bufs);
	
	for (ByteBuffer byteBuffer : bufs) {
		byteBuffer.flip();
	}
	
	System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
	System.out.println("-----------------");
	System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
	
	//4. 聚集写入
	RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
	FileChannel channel2 = raf2.getChannel();
	
	channel2.write(bufs);
}

3.1.2 SocketChannel类

网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。

TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:

客户端:

1. 打开通道,连接到服务端

SocketChannel channel = SocketChannel.open(); // 打开通道,此时还没有打开 TCP 连接
channel.connect(new InetSocketAddress("localhost", 9090)); // 连接到服务端

2. 分配缓冲区

ByteBuffer buf = ByteBuffer.allocate(1024);

3. 配置是否为阻塞方式。(默认为阻塞方式)

channel.configureBlocking(false); // 配置通道为非阻塞模式

4. 与服务端进行数据交互

Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
	String str = scan.nextLine();
	buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
			+ "\n" + str).getBytes());
	buf.flip();
	channel.write(buf);
	buf.clear();
}

5. 关闭连接

channel.close();          // 关闭通道

3.1.3 ServerSocketChannel类

网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:

服务端:

1. 打开一个 ServerSocketChannel 通道, 绑定端口。

ServerSocketChannel server = ServerSocketChannel.open(); // 打开通道

2. 绑定端口

server.bind(new InetSocketAddress(9090)); // 绑定端口

3.  配置是否为阻塞方式。(默认为阻塞方式)

server.configureBlocking(false); // 配置通道为非阻塞模式

4. 获取选择器

Selector selector = Selector.open();

5. 将通道注册到选择器上, 并且指定“监听接收事件”

server.register(selector, SelectionKey.OP_ACCEPT);

5. 阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信。等待连接到来的代码一般放在一个循环结构中。

轮询式的获取选择器上已经“准备就绪”的事件

while (selector.select() > 0){
    System.out.println("开启事件处理");
    // 获取选择器中所有注册的通道中已准备好的事件
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    // 开始遍历事件
    while (it.hasNext()){
        SelectionKey selectionKey = it.next();
        System.out.println("--->"+selectionKey);
        // 判断这个事件具体是啥
        if (selectionKey.isAcceptable()){
            // 获取当前接入事件的客户端通道
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 切换成非阻塞模式
            socketChannel.configureBlocking(false);
            // 将本客户端注册到选择器
            socketChannel.register(selector,SelectionKey.OP_READ);
        }else if (selectionKey.isReadable()){
            // 获取当前选择器上的读
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            // 读取
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len;
            while ((len = socketChannel.read(buffer)) > 0){
                buffer.flip();
                System.out.println(new String(buffer.array(),0,len));
                // 清除之前的数据(覆盖写入)
                buffer.clear();
            }
        }
        // 处理完毕后,移除当前事件
        it.remove();
    }
}

6. 通过 SocketChannel 与客户端进行数据交互

7. 关闭 SocketChannel

client.close();

3.1.4 NIO 非阻塞网络编程案例-群聊系统

服务端:

package nio.chat;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
 
/**
 *
 */
public class Server {
    // 定义属性
    private Selector selector;
    private ServerSocketChannel ssChannel;
    private static final int PORT = 9999;
    // 构造器
    // 初始化工作
    public Server() {
        try {
            // 1、获取通道
            ssChannel = ServerSocketChannel.open();
            // 2、切换为非阻塞模式
            ssChannel.configureBlocking(false);
            // 3、绑定连接的端口
            ssChannel.bind(new InetSocketAddress(PORT));
            // 4、获取选择器Selector
            selector = Selector.open();
            // 5、将通道都注册到选择器上去,并且开始指定监听接收事件
            ssChannel.register(selector , SelectionKey.OP_ACCEPT);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 监听
    public void listen() {
        System.out.println("监听线程:" + Thread.currentThread().getName());
        try {
            while (selector.select() > 0){
                // 7、获取选择器中的所有注册的通道中已经就绪好的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8、开始遍历这些准备好的事件
                while (it.hasNext()){
                    // 提取当前这个事件
                    SelectionKey sk = it.next();
                    // 9、判断这个事件具体是什么
                    // 处理连接事件
                    if(sk.isAcceptable()){
                        // 10、直接获取当前接入的客户端通道
                        SocketChannel schannel = ssChannel.accept();
                        // 11 、切换成非阻塞模式
                        schannel.configureBlocking(false);
                        // 12、将本客户端通道注册到选择器
                        System.out.println(schannel.getRemoteAddress() + " 上线 ");
                        schannel.register(selector , SelectionKey.OP_READ);
                        // 提示
                    // 处理读取事件
                    }else if(sk.isReadable()){
                        // 处理读 (专门写方法..)
                        readData(sk);
                    }
 
                    it.remove(); // 处理完毕之后需要移除当前事件
                }
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 发生异常处理....
 
        }
    }
 
    // 读取客户端消息
    private void readData(SelectionKey key) {
        // 获取关联的channel
        SocketChannel channel = null;
        try {
            // 得到channel
            channel = (SocketChannel) key.channel();
            // 创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = channel.read(buffer);
            // 根据count的值做处理
            if(count > 0) {
                // 把缓存区的数据转成字符串
                String msg = new String(buffer.array());
                // 输出该消息
                System.out.println("来自客户端---> " + msg);
                // 向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }
        }catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                e.printStackTrace();
                // 取消注册
                key.cancel();
                // 关闭通道
                channel.close();
            }catch (IOException e2) {
                e2.printStackTrace();;
            }
        }
    }
 
    // 转发消息给其它客户(通道)
    private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
        // 遍历 所有注册到selector 上的 SocketChannel,并排除 self
        for(SelectionKey key: selector.keys()) {
            // 通过 key  取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            // 排除自己
            if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                // 转型
                SocketChannel dest = (SocketChannel)targetChannel;
                // 将msg 存储到buffer
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                // 将buffer 的数据写入 通道
                dest.write(buffer);
            }
        }
    }
 
    public static void main(String[] args) {
        // 创建服务器对象
        Server groupChatServer = new Server();
        groupChatServer.listen();
    }
}

客户端:

package nio.chat;
 
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
 
public class Client {
    // 定义相关的属性
    private final String HOST = "127.0.0.1"; // 服务器的ip
    private final int PORT = 9999; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;
 
    // 构造器, 完成初始化工作
    public Client() throws IOException {
        selector = Selector.open();
        // 连接服务器
        socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 将channel 注册到selector
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username + " is ok...");
    }
 
    // 向服务器发送消息
    public void sendInfo(String info) {
        info = username + " 说:" + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    // 读取从服务器端回复的消息
    public void readInfo() {
        try {
            int readChannels = selector.select();
            if(readChannels > 0) {// 有可以用的通道
 
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
 
                    SelectionKey key = iterator.next();
                    if(key.isReadable()) {
                        // 得到相关的通道
                        SocketChannel sc = (SocketChannel) key.channel();
                        // 得到一个Buffer
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        // 读取
                        sc.read(buffer);
                        // 把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                }
                iterator.remove(); // 删除当前的selectionKey, 防止重复操作
            } else {
                // System.out.println("没有可以用的通道...");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) throws Exception {
        // 启动我们客户端
        Client chatClient = new Client();
        // 启动一个线程, 每个3秒,读取从服务器发送数据
        new Thread() {
            public void run() {
                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.currentThread().sleep(3000);
                    }catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
 
        // 发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

3.2 Buffer(缓冲区)

缓冲区 Buffer 是 Java NIO 中一个核心概念,在NIO库中,所有数据都是用缓冲区处理的。

在读取数据时,它是直接读到缓冲区中的,在写入数据时,它也是写入到缓冲区中的,任何时候访问 NIO 中的数据,都是将它放到缓冲区中。

而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

io流阻塞线程 java java非阻塞io_数据_04

3.2.1 Buffer 数据类型

io流阻塞线程 java java非阻塞io_io流阻塞线程 java_05

从类图中可以看到,7 种数据类型对应着 7 种子类(ByteBuffer、CharBuffer、ShortBuffer 、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer ),这些名字是 Heap 开头子类,数据是存放在 JVM 堆中的。

上述 Buffer 类他们都采用相似的方法进行管理数据,只是各自 管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象。

MappedByteBuffer

MappedByteBuffer 则是存放在堆外的直接内存中,可以映射到文件。

通过java.nio包和MappedByteBuffer允许Java程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。

Mmap内存映射和普通标准IO操作的本质区别在于它并不需要将文件中的数据先拷贝至OS的内核IO缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。

io流阻塞线程 java java非阻塞io_java_06

只有当缺页中断发生时,直接将文件从磁盘拷贝至用户态的进程空间内,只进行了一次数据拷贝,对于容量较大的文件来说(文件大小一般需要限制在1.5~2G以下),采用Mmap的方式其读/写的效率和性能都非常高,大家熟知的RocketMQ就使用了该技术。 

3.2.2 缓冲区的基本属性 Buffer 中的重要概念:

容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小, 也称为"容量",缓冲区容量不能为负,并且创建后不能更改。

限制 (limit):表示缓冲区中可以操作数据的大小 (limit 后数据不能进行读写)。缓冲区的限制不能 为负,并且不能大于其容量。 写入模式,限制等于 buffer的容量。读取模式下,limit等于写入的数据量。

位置 (position):下一个要读取或写入的数据的索引。 缓冲区的位置不能为 负,并且不能大于其限制

标记 (mark)与重置 (reset):标记是一个索引, 通过 Buffer 中的 mark() 方法 指定 Buffer 中一个 特定的 position,之后可以通过调用 reset() 方法恢 复到这 个 position.

标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity

io流阻塞线程 java java非阻塞io_io流阻塞线程 java_07

3.2.3 Buffer数据流程

应用程序可以通过与 I/O 设备建立通道来实现对 I/O 设备的读写操作,操作的数据通过缓冲区 Buffer 来进行交互。

io流阻塞线程 java java非阻塞io_io流阻塞线程 java_08

从 I/O 设备读取数据时:

1)应用程序调用通道 Channel 的 read() 方法;

2)通道往缓冲区 Buffer 中填入 I/O 设备中的数据,填充完成之后返回;

3)应用程序从缓冲区 Buffer 中获取数据。

往 I/O 设备写数据时:

1)应用程序往缓冲区 Buffer 中填入要写到 I/O 设备中的数据;

2)调用通道 Channel 的 write() 方法,通道将数据传输至 I/O 设备。

3.2.4 Buffer 核心方法

缓冲区存取数据的两个核心方法:

1)put():存入数据到缓冲区

put(byte b):将给定单个字节写入缓冲区的当前位置

put(byte[] src):将 src 中的字节写入缓冲区的当前位置

put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

2)get():获取缓冲区的数据

get() :读取单个字节

get(byte[] dst):批量读取多个字节到 dst 中

get(int index):读取指定索引位置的字节(不会移动 position)

3.3 Selector(选择器)

Selector类是NIO的核心类,Selector(选择器)选择器提供了选择已经就绪任务的能力。

Selector会不断的轮询注册在上面的所有channel,如果某个channel为读写等事件做好准备,那么就处于就绪状态,通过Selector可以不断轮询发现出就绪的channel,进行后续的IO操作。

Selector的作用:

  • Selector可以同时监控多个Channel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个(Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程避免了多线程之间的上下文切换导致的开销。

io流阻塞线程 java java非阻塞io_java_09

3.3.1  选择器使用步骤

1. 获取选择器

与通道和缓冲区的获取类似,选择器的获取也是通过静态工厂方法 open() 来得到的。

Selector selector = Selector.open(); // 获取一个选择器实例

2. 获取可选择通道

能够被选择器监控的通道必须实现了 SelectableChannel 接口,并且需要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打开 SocketChannel 并连接到本机 9090 端口
socketChannel.configureBlocking(false); // 配置通道为非阻塞模式

3. 将通道注册到选择器

通道在被指定的选择器监控之前,应该先告诉选择器,并且告知监控的事件,即:将通道注册到选择器,并绑定事件。

通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来完成,ops 表示关注的事件,如果需要关注该通道的多个 I/O 事件,可以传入这些事件类型或运算之后的结果。这些事件必须是通道所支持的,否则抛出 IllegalArgumentException。

ops表示的事件类型:

  • 读 : SelectionKey.OP_READ (1)
  • 写 : SelectionKey.OP_WRITE (4)
  • 连接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)

若注册时不止监听一个事件,则可以使用“位或”操作符连接。

int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE

// 将套接字通过到注册到选择器,关注 read 和 write 事件
socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

4. 轮询 select 就绪事件

通过调用选择器的 Selector.select() 方法可以获取就绪事件,该方法会将就绪事件放到一个 SelectionKey 集合中,然后返回就绪的事件的个数。这个方法映射多路复用 I/O 模型中的 select 系统调用,它是一个阻塞方法。正常情况下,直到至少有一个就绪事件,或者其它线程调用了当前 Selector 对象的 wakeup() 方法,或者当前线程被中断时返回。

while (selector.select() > 0){ // 轮询,且返回时有就绪事件
Set<SelectionKey> keys = selector.selectedKeys(); // 获取就绪事件集合
.......
}

有 3 种方式可以 select 就绪事件:

1)select() 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup() 或者当前线程被中断时返回。

2)select(long timeout) 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup(),或者当前线程被中断,或者阻塞时长达到了 timeout 时返回。不抛出超时异常。

3)selectNode() 不阻塞,如果无就绪事件,则返回 0;如果有就绪事件,则将就绪事件放到一个集合,返回就绪事件的数量。

5. 处理就绪事件

每次可以 select 出一批就绪的事件,所以需要对这些事件进行迭代。

for(SelectionKey key : keys){
    if(key.isWritable()){ // 可写事件
    if("Bye".equals( (line = scanner.nextLine()) )){
        socketChannel.shutdownOutput();
        socketChannel.close();
        break;
    }
    buf.put(line.getBytes());
    buf.flip();
    socketChannel.write(buf);
        buf.compact();
   }
}

从一个 SelectionKey 对象可以得到:1)就绪事件的对应的通道;2)就绪的事件。通过这些信息,就可以很方便地进行 I/O 操作。