当前位置 博文首页 > JavaEdge全是干货的技术号:我的生物系学妹也能听懂的Java NIO中

    JavaEdge全是干货的技术号:我的生物系学妹也能听懂的Java NIO中

    作者:[db:作者] 时间:2021-07-21 09:45

    1 NIO Server端

    1.1 多路复用开发一般步骤

    //打开选择器
    Selector selector = Selector.open();
    //打开通到
    ServerSocketChannel socketChannel = ServerSocketChannel.open();
    //配置非阻塞模型
    socketChannel.configureBlocking(false);
    //绑定端口
    socketChannel.bind(new InetSocketAddress(8080));
    //注册事件,OP_ACCEPT只适用于ServerSocketChannel 
    socketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iter = selectionKeys.iterator();
        while(iter.hasNext()) {
            SelectionKey key = iter.next();
            if(key.isAcceptable()) {
                SocketChannel channel = ((ServerSocketChannel)key.channel()).accept();
                channel.configureBlocking(false);
                channel.register(selector,SelectionKey.OP_READ);
            }
            
            if(key.isWritable()) {
            }
            
            if(key.isReadable()) {
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                channel.read(readBuffer);
                readBuffer.flip();
                // handler Buffer
                // 一般是响应客户端的数据
                // 直接是write写不就完事了嘛,为啥需要write事件?
                // channel.write(...)
            }
            iter.remove();
        }
    }
    

    1.2 解惑写事件

    对NIO的写操作:

    • 为什么要注册写事件
    • 何时注册写事件
    • 为什么写完之后要取消注册写事件

    如果有channel在Selector上注册了SelectionKey.OP_WRITE,在调用selector.select();时,系统会检查内核写缓冲区是否可写:

    • 如果可写,selector.select();立即返回,进入key.isWritable()
    • 何时不可写?比如缓冲区已满,channel调用了shutdownOutPut等

    当然除了注册写事件,你也可以在channel直接调用write(…),也可以将数据发出去,但这样不够灵活,而且可能浪费CPU。

    比如服务端需要发送一个200M的Buffer,看看是否使用OP_WRITE事件的区别。

    不使用事件

    程序运行到这会等到200M文件发送完成后才继续往下执行,不符合异步事件模型的思想。若缓冲区一直处不可写状态,则该过程一直在这里死循环,浪费CPU。

    // 200M的Buffer
    ByteBuffer buffer = .... 
    
    while(buffer.hasRemaining()) {
        // 该方法只会写入小于socket's output buffer空闲区域的任何字节数
        // 并返回写入的字节数,可能是0字节
        channel.write(buffer);
    }
    

    使用事件

    if(key.isReadable()) {
    	// 200M Buffer
        ByteBuffer buffer = .... 
        // 注册写事件
        key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
        // 绑定Buffer
        key.attach(buffer);
    }
    // 可写分支
    if(key.isWritable()) {
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        SocketChannel channel = (SocketChannel) key.channel();
        if (buffer.hasRemaining()) {
            channel.write(buffer)
        } else {
            // 发送完了就取消写事件,否则下次还会进入写事件分支(因为只要还可写,就会进入)
            key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
        }
    }
    

    要触发写事件,需要先向 selector 注册该通道的写事件,跟注册读事件一样,当底层写缓冲区有空闲就会触发写事件了,而一般来说底层的写缓冲区大部分都是空闲的。所以一般只要注册了写事件,就会立马触发了,为了避免 cpu 空转,在写操作完成后需要把写事件取消掉,然后下次再有写操作时重新注册写事件。

    2 NIO Client端

    开发的一般步骤

    // 打开选择器
    Selector selector = Selector.open();
    // 打开通道
    SocketChannel socketChannel = SocketChannel.open();
    // 配置非阻塞模型
    socketChannel.configureBlocking(false);
    // 连接Server
    socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
    // 注册事件
    socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
    // 循环处理
    while (true) {
        selector.select();
        Set<SelectionKey> keys = selector.selectedKeys();
        Iterator<SelectionKey> iter = keys.iterator();
        while(iter.hasNext()) {
            SelectionKey key = iter.next();
            if(key.isConnectable()) {
                // 连接建立或者连接建立不成功
                SocketChannel channel = (SocketChannel) key.channel();
                // 完成连接建立
                if(channel.finishConnect()) {
                    
                }
            }
            
            if(key.isReadable()) {
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(500 * 1024 * 1024);
                buffer.clear();
                channel.read(buffer);
                // buffer Handler
            }
            iter.remove();
        }
    }
    

    起初对OP_CONNECT事件还有finishConnect不理解,OP_CONNECT事件何时触发,特别是为什么要在key.isConnectable()分支里调用finishConnect方法后才能进行读写操作。

    首先,在non-blocking模式下调用socketChannel.connect(new InetSocketAddress(“127.0.0.1”,8080));连接远程主机,如果连接能立即建立就像本地连接一样,该方法会立即返回true,否则该方法会立即返回false,然后系统底层进行三次握手建立连接。连接有两种结果,一种是成功连接,第二种是异常,但是connect方法已经返回,无法通过该方法的返回值或者是异常来通知用户程序建立连接的情况,所以由OP_CONNECT事件和finishConnect方法来通知用户程序。不管系统底层三次连接是否成功,selector都会被唤醒继而触发OP_CONNECT事件,如果握手成功,并且该连接未被其他线程关闭,finishConnect会返回true,然后就可以顺利的进行channle读写。如果网络故障,或者远程主机故障,握手不成功,用户程序可以通过finishConnect方法获得底层的异常通知,进而处理异常。

    cs