NIO笔记(五)之NIO各种事件

文章目录

  • NIO各种事件
    • OP_CONNECT
    • OP_ACCEPT
    • OP_WRITE
      • OP_WRITE的处理解决网速慢的连接
    • OP_READ
    • 特殊的close事件

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移除去

你可能感兴趣的:(#,java-NIO)