NIO各种事件

版权声明:本文是作者在学习与工作中的总结与笔记,如有内容是您的原创,请评论留下链接地址,我会在文章开头声明。 https://blog.csdn.net/usagoole/article/details/82584851

NIO各种事件

  1. 客户端的SocketChannel支持 OP_CONNECT, OP_READ, OP_WRITE三个操作。服务端ServerSocketChannel只支持OP_ACCEPT操作,在服务端由ServerSocketChannelaccept()方法产生的SocketChannel只支持OP_READ, OP_WRITE操作。

    client/Server SocketChannel/ServerSocketChannel OP_ACCEPT OP_CONNECT OP_WRITE OP_READ
    client SocketChannel Y Y Y
    server ServerSocketChannel Y
    server SocketChannel Y Y
  2. 就绪条件

    • OP_ACCEPT就绪条件:当收到一个客户端的连接请求时,该操作就绪。这是ServerSocketChannel上唯一有效的操作。
    • OP_CONNECT就绪条件:只有客户端SocketChannel会注册该操作,当客户端调用SocketChannel.connect()时,该操作会就绪。
    • OP_READ就绪条件:该操作对客户端和服务端的SocketChannel都有效,当OS的读缓冲区中有数据可读时,该操作就绪。
    • OP_WRITE就绪条件:该操作对客户端和服务端的SocketChannel都有效,当OS的写缓冲区中有空闲的空间时(大部分时候都有),该操作就绪。

OP_CONNECT

  1. 客户端调用connect()并注册OP_CONNECT事件后,连接操作就会就绪,但是连接就绪不代表连接成功。OP_CONNECT底层本质上是Write

    SocketChannel channel = SocketChannel.open();
    channel.configureBlocking(false);
    channel.connect(addr);
    channel.register(selector, SelectionKey.OP_CONNECT);
    
    //判断连接就绪,通过`finishConnect()`判断
    if (key.isValid() && key.isConnectable()) {
        SocketChannel ch = (SocketChannel) key.channel();
        if (ch.finishConnect()) {
            // Connect successfully
            // key.interestOps(SelectionKey.OP_READ);
        } else {
            // Connect failed
        }
    }
    

OP_ACCEPT

  1. OP_ACCEPT的处理与OP_CONNECT基本一样,服务端监听,并注册OP_ACCEPT事件后,就已准备好接受客户端的连接了

    
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    ssc.socket().bind(new InetSocketAddress(port));
    channel.register(selector, SelectionKey.OP_ACCEPT);
    
    if (key.isValid() && key.isAcceptable()) {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel ch = ssc.accept();
        if (ch != null) {
            ch.configureBlocking(false);
            SelectionKey sk = ch.register(selector, SelectionKey.OP_READ);
        }
    }
    

OP_WRITE

  1. OP_WRITE事件相对特殊,一般情况,不应该注册OP_WRITE事件OP_WRITE的就绪条件为操作系统内核缓冲区有空闲空间(OP_WRITE事件是在Socket发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT时发生),而写缓冲区绝大部分事件都是有空闲空间的,所以当你注册写事件后,写操作一直就是就绪的,这样会导致Selector处理线程会占用整个CPU的资源。所以最佳实践是当你确实有数据写入时再注册OP_WRITE事件,并且在写完以后马上取消注册。
  2. 从上面分析可以看出, OP_WRITE事件并不是表示在调用channelwrite()方法之后就会发生这个事件。实际上完全可以不注册OP_WRITE事件,直接调用SocketChannel.write(ByteBuffer)是可以把数据直接写到缓冲区并发送的,但是这种直接写的方式Selector不会选择到isWriteable()分支,而且必须通过while(ByteBuffer.hasRemain())来检查写的状态. 注册OP_WRITE是比较好的做法,注册方式有两种
    • 直接注册到SocketChannel: SocketChannel.register(selector, SelectionKey.OP_WRITE),这种方式直接用SocketChannel来写ByteBuffer
    • SelectonKey方式注册: SelectionKey.interestOps(SelectionKey.interestOps() | SelectionKey.OP_WRITE)
  3. 代码

    将数据写入到缓冲区中,并注册写事件
    public void write(byte[] data) throws IOException {
        writeBuffer.put(data);
        key.interestOps(SelectionKey.OP_WRITE);
    }
    //写操作就绪,将之前写入缓冲区的数据写入到Channel,并取消注册
    channel.write(writeBuffer);
    key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
    
    
    另外一种方式:我们在注册的时候添加attachment,业务方法只需要操作attachment,往attachment里面写,然后在Selector.isWriteable()分支里面取出写好的Attachment,然后调用SocketChannel.Write(ByteBuffer) 方法来把数据写到Channel。
    
    

OP_WRITE的处理解决网速慢的连接

  1. 不对OP_WRITE处理的代码,

    
    while (bb.hasRemaining()) {
        int len = socketChannel.write(bb);
        if (len < 0) {
            throw new EOFException();
        }
    }
  2. 上面这样写在大多数的情况下都没有问题,但是在客户端的网络环境很糟糕的情况下,就有问题了。因为如果客户端的网络或者是中间交换机的问题,使得网络传输的效率很低,这时候会出现服务器已经准备好的返回结果无法通过TCP/IP层传输到客户端。这时候在执行上面这段程序的时候就会出现以下情况。

    • bb.hasRemaining()一直为”true”,因为服务器的返回结果已经准备好了。
    • socketChannel.write(bb)的结果一直为0,因为由于网络原因数据一直传不过去。
    • 因为是异步非阻塞的方式,socketChannel.write(bb)不会被阻塞,立刻被返回
    • 在一段时间内,由于缓冲区一直满,这段代码会被无休止地快速执行着,消耗着大量的CPU的资源。事实上什么具体的任务也没有做,一直到网络允许当前的数据传送出去为止。
  3. 以上的结果肯定不是我们想要的,以上OP_WRITE代码需要进一步处理才可以达到目的,以下程序在网络不好的时候,将此ChannelOP_WRITE操作注册到Selector上,这样,当网络恢复缓冲区有空间时(OP_WRITE的就绪条件为底层缓冲区有空闲空间),Channel可以继续将结果数据返回客户端,此时Selector会通过SelectionKey来通知应用程序,再去执行write操作。这样就能节约大量的CPU资源,使得服务器能适应各种恶劣的网络环境。

    while (bb.hasRemaining()) {
        int len = socketChannel.write(bb);
        if (len < 0){
            throw new EOFException();
        }
        //返回0表示缓冲区满
        if (len == 0) {
            selectionKey.interestOps(
                            selectionKey.interestOps() | SelectionKey.OP_WRITE);
            mainSelector.wakeup();
            break;
        }
    }
  4. Grizzly中不是按照上面方式对OP_WRITE做处理.在Grizzly中,对请求结果的返回是在ProcessTask中处理的,经过SocketChannelOutputBuffer的类,最终通过OutputWriter类来完成返回结果的动作。在OutputWriter中处理OP_WRITE的代码如下:

    public static long flushChannel(SocketChannel socketChannel,
            ByteBuffer bb, long writeTimeout) throws IOException
    {
        SelectionKey key = null;
        Selector writeSelector = null;
        int attempts = 0;
        int bytesProduced = 0;
        try {
            while (bb.hasRemaining()) {
                int len = socketChannel.write(bb);
                attempts++;
                if (len < 0){
                    throw new EOFException();
                }
                bytesProduced += len;
                //写阻塞了
                if (len == 0) {
                    if (writeSelector == null){
                        // 获取一个新的selector 
                        writeSelector = SelectorFactory.getSelector();
                        if (writeSelector == null){
                            // Continue using the main one
                            continue;
                        }
                    }
                    // 在新selector上注册写事件,而不是在主selector上注册
                    key = socketChannel.register(writeSelector, key.OP_WRITE);
                    //利用writeSelector.select(timeout)来阻塞当前线程,等待可写事件发生,总共等待可写事件的时长是3*writeTimeout
                    if (writeSelector.select(writeTimeout) == 0) {
                        if (attempts > 2)
                            throw new IOException("Client disconnected");
                    } else {
                        attempts--;
                    }
                } else {
                    attempts = 0;
                }
            }
        } finally {
            if (key != null) {
                key.cancel();
                key = null;
            }
            if (writeSelector != null) {
                // Cancel the key.
                writeSelector.selectNow();
                SelectorFactory.returnSelector(writeSelector);
            }
        }
        return bytesProduced;
    } 
  5. 当发现由于网络情况而导致的发送数据受阻(len==0)时,第一种方式的处理是将当前的Channel注册到当前的Selector中;而在Grizzly中,程序从SelectorFactory中获得了一个临时的Selector。在获得这个临时的Selector之后,程序做了一个阻塞的操作:writeSelector.select(writeTimeout)。这个阻塞操作会在一定时间内(writeTimeout)等待这个Channel的发送状态。如果等待时间过长,便认为当前的客户端的连接异常中断了。
  6. Grizzly的处理方式事实上放弃了NIO中的非阻塞的优势,使用writeSelector.select(writeTimeout)做了个阻塞操作。虽然CPU的资源没有浪费,可是线程资源在阻塞的时间内,被这个请求所占有,不能释放给其他请求来使用。
  7. Grizzly作者的解释
    • 使用临时的Selector的目的是减少线程间的切换。当前的Selector一般用来处理OP_ACCEPT,和OP_READ的操作。使用临时的Selector可减轻主Selector的负担;而在注册的时候则需要进行线程切换,会引起不必要的系统调用。这种方式避免了线程之间的频繁切换,有利于系统的性能提高。
    • 虽然writeSelector.select(writeTimeout)做了阻塞操作,但是这种情况只是少数极端的环境下才会发生。大多数的客户端是不会频繁出现这种现象的,因此在同一时刻被阻塞的线程不会很多。
    • 利用这个阻塞操作来判断异常中断的客户连接。
    • 经过压力实验证明这种实现的性能是非常好的。

OP_READ

  1. OP_READ事件一次可能读取的数据不完整,此时可以将buffer attach到SelectionKey上,下次OP_READ事件发生时再继续读取

    if (nextKey.isReadable()) {
      /**
       * 一次select只是通知载协议栈的读操作有数据可读,至于这次读到的数据是多少,
       * 是否是一次完整的交互数据,selector并不关心
       */
        this.read(nextKey);
    }
    
    
    private void read(SelectionKey key) throws IOException {
        System.out.println("执行read...");
        SocketChannel sc = (SocketChannel) key.channel();
        //读取echo数据
        ByteBuffer echoBuffer = ByteBuffer.allocate(1024);
        while (true) {
            echoBuffer.clear();
    
            /**
             * 1. SocketChannel的read()没有超时的设置http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4614802
             *    1.1 解决办法:
             *      1.1.1 使用传统socket读
             *      1.1.2 使用ReadableByteChannel这个类(不能解决读写双向的Channel阻塞问题)
             *          InputStream is = sock.socket().getInputStream();
             *          ReadableByteChannel readCh = Channels.newChannel(is);
             * 2. 对于非阻塞的read
             *      1.1 read返回-1表示客户端的数据发送完毕,并且主动close socket,这种情况下,服务端程序需要关闭
             *      SocketChannel并且取消key,并且退出。如果服务端继续使用该SocketChannel,会抛出IO异常
             *      1.2 read返回0
             *          * 当前SocketChannel没有数据可读,网络不好或者客户端确实没有发送数据
             *          * 客户端数据发送完毕
             *          * Bytebuffer的position等于limit了,即bytebuffer的remaining等于0
             */
    
            int result = sc.read(echoBuffer);
            if (result == -1) {
                System.err.println("客户端主动关闭");
                sc.close();
                //取消`SocketChannel`与Selector的注册关系
                key.cancel();
                return;
            } else if (result == 0) {
                System.err.println("没有数据等到下次select()调用");
                break;
            } else {
                System.out.println("写数据到客户端");
                echoBuffer.flip();
                while (echoBuffer.hasRemaining()) {
                    int len = sc.write(echoBuffer);
                    if (len < 0) {
                        throw new EOFException();
                    }
                   /* if (len == 0) { //缓冲区满了
                        System.out.println("len==0");
                        key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
                        selector.wakeup();
                        break;
                    }*/
                }
    
            }
    
        }
    }   

特殊的close事件

  1. 当关闭客户端时,服务端会发生一个read事件,并且在read的时候抛出异常来表示关闭。如果是服务端关闭,则客户端在write的时候会抛出异常。这个关闭事件会不断的发生,即使从准备好的集合移除也没有用,必须关闭channel或者调用key的cancel().因为SelectionKey代表的是SelectorChannel之间的联系,所以在Channel关闭之后,对于Selector来说,这个Channel永远都会发出关闭这个事件,表示已经关闭,直到从该Selector移除去

猜你喜欢

转载自blog.csdn.net/usagoole/article/details/82584851