内容参考:
https://zhuanlan.zhihu.com/p/363504902

几个概念

1.1 同步

同步是指当前线程调用一个方法之后,当前线程必须等到该方法调用返回后,才能继续执行后续的代码。

1.1.1 同步阻塞

同步阻塞是指在调用结果返回之前,当前线程会被挂起。当前线程只有在得到结果之后才会返回,然后才会继续往下执行。

1.1.2 同步非阻塞

同步非阻塞是指某个调用不能立刻得到结果时,该调用不会阻塞当前线程,此时当前线程可以不用等待结果就能继续往下执行其他的代码,等执行完别的代码再去检查一下之前的结果有没有返回。

1.2 异步

异步是指,当前线程在发出一个调用之后,这个调用就马上返回了,但是并没有返回结果,此时当前线程可以继续去执行别的代码,在调用发出后,调用者会通过状态、通知来通知调用者其返回结果,或者是通过回调函数来处理该调用的结果。

NIO三大组件:selector、channel、buffer

NIO中的三个核心分别是Selector、Channel、Buffer,他们之间的关系如下图:

nest 异步controller nio 异步_nest 异步controller


1、每个 channel 都会对应一个 Buffer。

2、Selector 对应一个线程, 一个线程对应channel(连接)。

3、该图反应了有三个 channel 注册到 该 selector //程序。

4、程序切换到哪个 channel 是由事件决定的, Event 就是一个重要的概念。

5、Selector 会根据不同的事件,在各个通道上切换。

6、Buffer 就是一个内存块 , 底层是一个数组。

7、数据的读取写入是通过 Buffer, 这个和 BIO不同 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO的Buffer是可以读也可以写, 需要 flip 方法切换channel是双向的

Buffer(缓冲区)

缓冲区本质上是一个 可以读写数据的内存块,可以理解成是一个 容器对象( 含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

nest 异步controller nio 异步_nest 异步controller_02

Buffer有几个重要属性

属性

含义

mark

标记作用,buffer.position(0).mark()进行标记,buffer.reset();就会回到刚刚的标记位置

capacity

它代表这个缓冲区的容量,一旦设定就不可以更改。

position

初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置,从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了

Limit

写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。


刚刚初始化时,position指向Buffer的最左边,而limit和capacity都指向buffer数组的最右边。

nest 异步controller nio 异步_客户端_03

Channel(通道)

Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。

nest 异步controller nio 异步_客户端_04

FileChannel类

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

1)获取通道

文件通道通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式。

// 获取文件通道
FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);

2)创建字节缓冲区

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

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

3)读写操作

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

while (channel.read(buf) != -1){ // 读取通道中的数据,并写入到 buf 中
    buf.flip(); // 缓存区切换到读模式
    while (buf.position() < buf.limit()){ // 读取 buf 中的数据
        text.append((char)buf.get());
    }
    buf.clear(); // 清空 buffer,缓存区切换到写模式
}

写入数据

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(); // 清空缓存区,将缓冲区置为写模式,下次才能使用
    }
}

4)将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 方法可以确保对文件的操作能够更新到磁盘。

channel.force(false);

5)关闭通道

channel.close();

SocketChannel类

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

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

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

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

2)分配缓冲区

ByteBuffer buf = ByteBuffer.allocate(10); // 分配一个 10 字节的缓冲区,不实用,容量太小

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

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

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

5)关闭连接

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

ServerSocketChannel类

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

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

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

2)绑定端口

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

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

SocketChannel client = server.accept(); // 阻塞,直到有连接过来

4)通过 SocketChannel 与客户端进行数据交互

5)关闭 SocketChannel

client.close();

Selector(选择器)

在传统的BIO当中,监听每个客户端的请求都需要一个线程去处理,线程数的上升会涉及到大量的上下文切换的操作,这也是非常浪费性能的。Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

Selector使用步骤

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。

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

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

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


SelectionKey

SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4

例子

NIOServer端

public class ServerTest {
    //1. 定义成员属性:选择器、服务端通道、端口
    private Selector selector;
    private ServerSocketChannel ssChannel;
    private static final int PORT = 9999;

    /**
     * 构造方法
     * @throws IOException
     */
    public ServerTest() throws IOException {
//        创建 Selector
        this.selector = Selector.open();
//        创建 ServerSocketChannel
        this.ssChannel = ServerSocketChannel.open();
//        为 ServerSocketChannel 绑定端口
        ssChannel.bind(new InetSocketAddress(PORT));
//        ServerSocketChannel 设置非阻塞模式
        ssChannel.configureBlocking(false);

//        将channel注册到selector上,监听连接事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务端创建成功");
    }

    /**
     * 监听方法
     */
    public void listen() {
        System.out.println("等待连接。。。。");
//        循环等待新接入的连接
        while (true) {
            try {
                //            select()方法返回的int值表示有多少通道已经就绪
                if (selector.select() > 0) {
//                获取可用channel的集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator iterator = selectionKeys.iterator();
//                开始遍历这些准备好的事件
                    while (iterator.hasNext()) {
                        // selectionKey实例
                        SelectionKey selectionKey = (SelectionKey) iterator.next();

                        iterator.remove();

                        // 如果是 接入事件
                        if (selectionKey.isAcceptable()) {
                            acceptHandler(ssChannel, selector);
                        }

                        // 如果是 可读事件
                        if (selectionKey.isReadable()) {
                            readHandler(selectionKey, selector);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    /**
     * 可读事件
     * @param selectionKey
     * @param selector
     */
    private void readHandler(SelectionKey selectionKey, Selector selector) {
        //得到当前客户端通道
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        //创建缓冲区对象开始接受客户端通道数据
        ByteBuffer buffer = ByteBuffer.allocate(4);

        // 循环读取客户端请求信息
        String request = "";
        try {
            while (socketChannel.read(buffer) > 0) {

                // 切换buffer为读模式
                buffer.flip();

                // 读取buffer中的内容
                request += Charset.forName("UTF-8").decode(buffer);
            }
            System.out.println("接收到客户端消息:" + bytes2hex(request.getBytes()));
            buffer.flip();
            socketChannel.write(ByteBuffer.wrap(new byte[] {0x00, 0x02}));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 接入事件
     * @param ssChannel
     * @param selector
     */
    private void acceptHandler(ServerSocketChannel ssChannel, Selector selector) {
        try {
            // 直接获取当前接入的客户端通道
            SocketChannel schannel = ssChannel.accept();
            // 切换成非阻塞模式
            schannel.configureBlocking(false);
            // 将本客户端通道注册到选择器
            System.out.println(schannel.getRemoteAddress() + " 上线 ");
            schannel.register(selector , SelectionKey.OP_READ);
        } catch (Exception e) {
             e.printStackTrace();
        }

    }

    public static String bytes2hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        String tmp;
        sb.append("[");
        for (byte b : bytes) {
            // 将每个字节与0xFF进行与运算,然后转化为10进制,然后借助于Integer再转化为16进制
            tmp = Integer.toHexString(0xFF & b);
            if (tmp.length() == 1) {
                tmp = "0" + tmp;//只有一位的前面补个0
            }
            sb.append(tmp).append(" ");//每个字节用空格断开
        }
        sb.delete(sb.length() - 1, sb.length());//删除最后一个字节后面对于的空格
        sb.append("]");
        return sb.toString();
    }

    public static void main(String[] args) throws IOException {
        ServerTest server = new ServerTest();
        server.listen();
    }
}

NIOClient端

public class ClientTest {
    //定义相关的属性
    private final String HOST = "127.0.0.1"; // 服务器的ip
    private final int PORT = 9999; //服务器端口
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public ClientTest() throws IOException {
        selector = Selector.open();
        //连接服务器
        socketChannel = socketChannel.open(new InetSocketAddress(HOST, 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(byte[] info) {
        try {
            socketChannel.write(ByteBuffer.wrap(info));
        }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(2);
                        //读取
                        sc.read(buffer);
                        //把读到的缓冲区的数据转成字符串
                        String msg = new String(buffer.array());
                        System.out.println(ServerTest.bytes2hex(msg.getBytes()));
                    }
                }
                iterator.remove(); //删除当前的selectionKey, 防止重复操作
            } else {
                //System.out.println("没有可以用的通道...");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        ClientTest client = new ClientTest();
        //发送数据给服务器端
        while (true) {
            client.sendInfo(new byte[] {0x00, 0x01});
            client.readInfo();
        }
    }
}