zoukankan      html  css  js  c++  java
  • Java NIO 入门

    本文主要记录 Java 中  NIO 相关的基础知识点,以及基本的使用方式。

    一、回顾传统的 I/O 

    刚接触 Java 中的 I/O 时,使用的传统的 BIO 的 API。由于 BIO 设计的类实在太多,至今我仍然不能信手拈来的写出完整的 BIO 的代码。不过它基本的特点和分类,我还是记得一二的。

    1. 从方向上看,分为输入流和输出流;
    2. 从类别上看,分为字节流和字符流;
    3. 从缓冲去上看,分为带缓冲的流和不带缓冲的流。

    一个便于使用的的流对象的构建,一般都是由相对底层的流逐渐构建出相对高级的流。通常我都是用 BIO 来做一些本地文件的 I/O 操作 。在网络编程方面,BIO 因为其阻塞的原因,大家使用的都比较少,一般都使用 NIO,尤其是在服务端的网络开发。强大的 Netty 正是基于 Java NIO 的基础而开发出来的高性能框架,在学习 Netty 之前,很有必要去掌握 NIO 的基本使用。

    二、NIO 底层 API

    NIO  相关的核心概念有 3 个,Channel、Buffer 和 Selector。

    2.1 Channel

      Java 中传统的 BIO 分为输入流和输出流,在同一个 Socket 连接或者文件的 IO 中,需要同时使用这两种流才能进行数据的交互。而 NIO 则使用了 Channel 的概念,可以对 Channel 进行双向操作。我们可以将数据写入到 Channel,也可以从 Channel 中读取数据。

    Channel 的主要实现有以下 4 类:

    1. FileChannel
    2. DatagramChannel
    3. SocketChannel
    4. ServerSocketChannel

    从名称上看,就能知道这些类分别对应了文件、UDP、TCP(Client、Server)。

    2.2 Buffer

    Buffer 也就是缓冲区。它负责将 Channel 中的数据取出来(读数据),或者将用户程序的数据放入 Channel(写数据)。如果将 Channel 比作是一架飞机及其航线,那么 Buffer 就是航站楼与飞机之

    间的摆渡车。下图就是 Buffer 和 Channel 之间的交互:

    Buffer 是用户程序与 Channel 进行数据交互的工具。ByteBuffer 是 NIO 中最底层的实现,在此基础上,还有 CharBuffer、DoubleBuffer 等。

    ByteBuffer 实质上是维护了一个字节数组,它包含了一个几个特殊的属性:

    1. capacity:缓冲区的长度;
    2. position:下一个要操作元素的索引;
    3. limit:当前可操作元素的最大索引;
    4. mark:标记当前 position 的前 1 位。

      具体怎么用,可以查询 JDK API,只要直到其他只属性即可。

    2.3 Selector

      在学习了 Channel 和 Buffer 之后,已经可以使用这两个类了进行阻塞式的 I/O 操作了。但是 Selector 才是 Java NIO 的核心优势点。只有 ScoketChannel 才能设置为非阻塞模式,所以 Selector 只能在网络 I/O 中才能使用。

      Selector 的核心方法就是 select()方法,该方法会一直阻塞,直到注册在该选择器上的通道有用户所感兴趣的事件准备就绪了才会返回。这里面又涉及到 Selector 与 Channel 之间的映射,这个关系用 SelectionKey 来表示。在调用选择器的 select()方法前,用户可以使用 Channel 的 register()方法,将其注册到选择器上,同时表明用户对该通道的哪些操作感兴趣。注意,register()方法返回的就是 SelectionKey 对象。

    三、客户端示例

    3.1 客户端示例:

     1  public class Client {
     2      private static final int REMOTE_PORT = 8888;
     3      private static final String REMOTE_HOST = "127.0.0.1";
     4      private static final int BUFF_SIZE = 1024;
     5  
     6      public static void main(String[] args) throws IOException {
     7          Selector selector = Selector.open();
     8          SocketChannel socketChannel =
     9                  SocketChannel.open(new InetSocketAddress(REMOTE_HOST, REMOTE_PORT));
    10       //将套接字通道设置为非阻塞模式
    11          socketChannel.configureBlocking(false);
    12       //将通道注册到 Selector 中,第二个参数为该通道感兴趣的事件,此处为读事件
    13       //注册方法会返回一个 SelectionKey 对象,它代表通道与选择器之间的映射关系
    14          socketChannel.register(selector,
    15                  SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUFF_SIZE));
    16          if (!socketChannel.isConnected()) {
    17              socketChannel.finishConnect();
    18          }
    19          for (; ; ) {
    20         //选择器的 select()方法会阻塞到有通道所感兴趣的事件已经就绪
    21              if (selector.select() > 0) {
    22            //调用选择器的 selectedKeys() 方法会返回,本次所有就绪通道对应的 SelectionKey 集合
    23                  Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    24                  while (it.hasNext()) {
    25                      SelectionKey key = it.next();
    26                      //通过迭代去删除掉本次将要处理的 key
    27                      //如果不删除,下次 select 还会返回该 key
    28                      it.remove();
    29                      if (key.isReadable()) {
    30                          ByteBuffer bf = (ByteBuffer) key.attachment();
    31                          bf.clear();
    32                          SocketChannel sc = (SocketChannel) key.channel();
    33                          int i = sc.read(bf);
    34                          if (i < 0) {
    35                              key.cancel();
    36                              sc.close();
    37                              return;
    38                          }
    39                          bf.flip();
    40                          byte[] ret = new byte[bf.remaining()];
    41                          bf.get(ret);
    42                          for (byte b : ret) {
    43                              System.out.print(b);
    44                          }
    45                          System.out.println();
    46                      }
    47                  }
    48              }
    49          }
    50      }
    51  }

        关于该示例的一些说明:

    1. 示例中将通道注册到选择器时,没有注册写事件(15 行)。原因是当操作系统中发送数据的缓冲区未满时,写操作一般都是可用的。如果注册了写事件,由于写事件一直是就绪的,那么 select()方法会立刻返回,这就会导致程序的 CPU 使用率一路飙升;
    2. 示例中未包含向通道写入数据的演示。 写入数据可以直接将数据写入通道,也可以使用通道注册时返回的 key 来获取通道,然后写入数据到通道;
    3. 示例代码中的 31 行,当读取到的字节数为 -1 时,会让人产生疑惑,既然这个通道被 select 出来了,那么为什么没有数据可读呢?有一种情况就是对端关闭了套接字连接,此时客户端的 select()方法每次都会立刻返回,导致空轮询。并且每次都能 select 出这个对端已经关闭了的通道。如果我们不关闭该通道,稍后可能就会抛出 IOException远程主机强迫关闭了一个现有的连接JAVA NIO客户端主动关闭连接,导致服务器空轮询 - SegmentFault 思否

      对于上面的第 3 点,之前遇到过一个问题,对端如果发现连接在指定的间隔内没有数据通讯,就会关闭掉连接,这个时候我们也需要关闭对应的通道。下图是抓包的结果:

      

    3.2 服务端示例:

     1 public class Server {
     2 
     3     private static final int PORT = 8888;
     4 
     5     public static void main(String[] args) throws IOException {
     6         Selector selector = Selector.open();
     7         ServerSocketChannel ssc = ServerSocketChannel.open();
     8         ssc.configureBlocking(false);
     9         ssc.socket().bind(new InetSocketAddress(PORT));
    10         //对可连接事件感兴趣
    11         ssc.register(selector, SelectionKey.OP_ACCEPT);
    12         for (; ; ) {
    13             if (selector.select() > 0) {
    14                 Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    15                 while (it.hasNext()) {
    16                     SelectionKey key = it.next();
    17                     it.remove();
    18                     if (key.isAcceptable()) {
    19                         //处理客户端的连接
    20                         SocketChannel sc = ssc.accept();
    21                         sc.configureBlocking(false);
    22                         sc.register(selector, SelectionKey.OP_READ);
    23                     }
    24                     if (key.isReadable()) {
    25                         //通过该 key 处理对应的读事件
    26                     }
    27                 }
    28             }
    29         }
    30     }
    31 }                

      基于 Selector 的 NIO 模式,可以使用一个线程来处理大量的连接,优势十分明显。

    四、总结

      以上只是 Java 中 NIO 的粗略介绍,仍需进一步熟悉各个 API 的使用方法。在熟悉 NIO 之后,下一步准备学习一下 Netty 框架的使用。

    准备使用 Netty 的原因:

    1. BIO 和 NIO 的 API 都很繁杂,使用起来十分的不优雅。尤其是在 BIO 和 NIO 之间切换的时候,几乎是推倒重建;
    2. NIO 还有空轮询的 BUG;
    3. 实现稳定可靠的网络 I/O 程序是一个极具挑战的任务,这里面涉及了很多的知识:计算机网络、操作系统、程序设计等。

    五、参考资料

  • 相关阅读:
    pointcut通配符
    Java设计模式-建造者(Builder)模式
    mybatis 初步使用(IDEA的Maven项目, 超详细)
    静态代理和动态代理
    Java 多线程(五)之 synchronized 的使用
    Java 多线程(四)之守护线程(Daemon)
    HashMap 源码解析(一)之使用、构造以及计算容量
    Java 多线程(三)之线程状态及其验证
    Java 多线程(二)之 Thread 优先级
    Java 多线程之 Thread 类 和 Runnable 接口初步使用
  • 原文地址:https://www.cnblogs.com/magexi/p/9937430.html
Copyright © 2011-2022 走看看