JAVA之NIO

JAVA 之 NIO


由于最近需要实战到 NIO 的有关代码,而自己之前所学的东西也差不多忘光了,因此重新捡起了 NIO,复习的同时也将其写成博客,促进自己对其的理解。

NIO 是个什么东西?为什么 IO 会比 NIO 快?

答:IO 靠字符和字节传输,速度慢。NIO 靠 Buffer 一块一块传输,速度快!与此同时,NIO 还加入了多线程控制机制,即:一个 NIO 流可以同时传输多个块,实现异步传输。

一、核心组成

  1. Channels
  2. Buffers
  3. Selectors

Channel 和 Buffer

​ 基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。

Selector

​ Selector允许单线程处理多个 Channel。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

​ 下面,我们来认识一下这三个核心的组件。

二、Channel

  1. Channel 是什么

    一Java NIO的 Channel 类类似流,但又比流高级:

    • Channel 可以异步地读写
    • 从 Channel 中读写取数据时,总要先读或写到一个 Buffer ,然后再进行各自操作
  2. Channel 的实现

    一部分重要的通道实现:

    • FileChannel:重文件中读取数据
    • DatagramChannel:能通过UDP读写网络中的数据
    • SocketChannel:能通过TCP读写网络中的数据
    • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
  3. 简单调用示例:

    RandomAccessFile tFile = new RandomAccessFile("example","rw");
    FileChannel fileChannel = tFile.getChannel();
    ByteBuffer buf = ByteBuffer.allocate(48);
    int bytesRead = fileChannel.read(buf);//注意,此处是 int 型数字,指读到的数据大小
    while (bytesRead != -1) {
    System.out.println("Read " + bytesRead);
        ...//其余部分将在 Buffer 中讲解
    }

三、Buffer

  1. Buffer 是什么
    一通俗地理解,Buffer 就是一个存放数据的内存块,NIO 提供了一些访问这块内存块的方法,以此来对数据进行传输操作。

  2. Buffer 的类型

    • ByteBuffer
    • MappedByteBuffer
    • CharBuffer
    • DoubleBuffer
    • FloatBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer
  3. Buffer 的基本用法

    Buffer 读写数据一般遵循以下四个步骤:

    1. Buffer 的分配
    2. 写入数据到Buffer
    3. 调用flip()方法
    4. 从Buffer中读取数据
    5. 调用clear()方法或者compact()方法

    ​ 我们分别以这五个操作来介绍 Buffer(但是 Buffer 并不只提供这几个方法,还有 mark() 和 reset() 等,由于不是主要操作,所以我们不细讲)

  4. Buffer 的分配

    XXXBuffer buf = XXXBuffer.allocate(48); 
  5. 向 Buffer 中写数据

    写数据到Buffer有两种方式:

    1. 从 Channel 写到 Buffer

      int bytesRead = inChannel.read(buf); //注意!方法名是 read。是读 inChannel 中的数据
    2. 通过 Buffe r的 put() 方法写到 Buffer 里

      buf.put(127);//put方法还有很多版本,以不同的方式把数据写入到Buffer中。例如,写到一个指定的位置,或者把一个字节数组写入到Buffer
  6. 调用 filp() 方法

    flip 方法将 Buffer 从写模式切换到读模式(将访问 Buffer 中数据的指针移动到最初写进 Buffer 中的数据的位置)

  7. 从 Buffer 中读取数据

    与从 Buffer 中写数据一样,读数据也有两种方式:

    1. 从 Buffer 读取数据到 Channel

      int bytesWritten = inChannel.write(buf);//注意!方法名是 write,buffer 是主语
    2. 使用 get() 方法从 Buffer 中读取数据

      byte aByte = buf.get();//get方法也有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组
  8. 调用clear()方法或者compact()方法

    ​ 操作 Buffer 快其实就像在操作定长链表,而这个两个方法就像是对链表中指针的操作。

    ​ clear() 方法是将访问 Buffer 块的指针指向了 0 号位置,为下一次写操作做准备。我们会以为这个操作会将 Buffer 块中的数据清空,等待新数据赋值进来,其实并没有,当再次写入时只不过是重新赋值罢了。

    ​ 而在上一步的读取数据操作时,我们可能会漏访问掉一些元素,那么这个时候 compact() 就派上用场了,compact()方法将所有未读的数据拷贝到Buffer起始处。然后将访问指针设到最后一个未读元素正后面,为下次写入操作做准备。

  9. 注意点

    在执行 5 和 7 操作之前,均必须执行 filp() 或 clear() 和 compact() 方法!

四、Selector

由于单独讲解 Selector 不能很好地讲清它的作用,因此我们将结合 ServerSocketChannel(服务端) 和 SocketChannel (客户端)一起来讲解它的作用

  1. Selector 是什么

    ​ Selector 翻译过来是:选择器,很好理解,是一个用来选择 Channel 的组件。在 Java NIO 中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备。它就是你注册对各种 I/O 事件感兴趣的地方,而且当这些事件发生时,它会告诉你所发生的事件。通过 Selector,一个单独的线程可以管理多个channel,从而管理多个网络连接。

    ​ 创建一个 Selector 是第一件事:

    Selector selector = Selector.open();
  2. 如何向 Selector 注册通道呢?

    ​ 答案是:对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。

    ​ 首先,我们创建一个 ServerSocketChannel 对象,用于向 Selector 注册通道

    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking( false );//将 ServerSocketChannel 设置为 非阻塞的 。否则异步 I/O 就不能工作
    
    //将 ssc 绑定到给定的端口
    ServerSocket ss = ssc.socket();
    InetSocketAddress address = new InetSocketAddress( ports[i] );
    ss.bind( address );

    ​ 对象创建好了,我们调用register方法将其注册给 Selector。

    //监听 accept 事件,也就是在新的连接建立时所发生的事件。
    //SelectionKey 代表这个通道在此 Selector 上的这个注册信息, Selector 通过该 //SelectionKey 来通知你某个传入事件。关于 SelectionKey 将在下面细讲。
    SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

    注意: OP_ACCEPT(接收就绪)适用于 ServerSocketChannel (服务端)的唯一事件类型!

    ​ 对于其他对象,如果你不止对一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,

    ​ 如:

    SelectionKey key = channel.register(selector,Selectionkey.OP_READ|SelectionKey.OP_WRITE);

    穿插点:SelectionKey 知识

    SelectionKey 中的属性:

    • interest集合

      //可以通过该值判断有无感兴趣的操作
      int interestSet = selectionKey.interestOps();
      
      boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
      //其他值也是类似的
    • ready集合

      int readySet = selectionKey.readyOps();//readySet是Selection.xxx 的几个常量的值
    • Channel

    • Selector

    • 附加的对象(可选)

      可以在用register()方法向Selector注册Channel的时候附加对象。如:

      SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
  3. 注册完之后如何使用 Selector 来接收信息呢?

    ​ 答案是:内部循环。等待注册事件的发生。

    ​ 使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

    while (true) {
     try{
    //这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() //方法将返回所发生的事件的数量。
    int num = selector.select();
    
    //它返回发生了事件的 SelectionKey 对象的一个集合
    Set selectedKeys = selector.selectedKeys();
    Iterator it = selectedKeys.iterator();
    
    //通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件
    while (it.hasNext()) {
        SelectionKey key = (SelectionKey)it.next();
        switch (key.readyOps()) {
                   case SelectionKey.OP_ACCEPT:
                                ...
                               break;
                   case SelectionKey.OP_READ:
                               ...
                                break;
                    default:
                               ...
                                break;
        }
        it.remove();
    }
     }catch(){
     }
    }
  4. 接收完信息之后服务端如何处理信息呢?

    以上的操作都是服务端的操作,接下来,该排到客户端出马了!

    ​ 执行第三步的方法之后,我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它(不用担心 accept() 操作会阻塞):

    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
    SocketChannel sc = ssc.accept();//客户端的连接请求,服务端将其接收下来

    ​ 下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:

    sc.configureBlocking( false );
    //将客户端的读取操作(对象)也注册到 Selector 上,这样 Selector 就同时监听两个对象了
    SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
  5. 接收完信息之后就可以溜之大吉了吗?

    ​ 回答是:否!

    ​ 我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey

    it.remove(); //之后方可跳出第三步的代码中的主循环
  6. 当客户端发出连接操作时,我们又回到了第三步的主循环中,对读操作进行处理。

总结

​ 本篇内容对 JAVA NIO 的一些常用工具组件进行了讲解,也对各自的操作进行了简要的介绍,并在最后一小节,Selector 的介绍中实战了服务端与客户端的部分代码,希望对你有所帮助。如果博客中有所缺漏或者错误,欢迎骚扰指正。

​> 注:Java 的 NIO 不只有上面提到的那些组件,还有 datagram 和 pipe 等。由于不常用,所以没有在这里进行讲解,《疯狂 Java 讲义》一书中也有对这些组件的介绍,感兴趣的童鞋可以自己去查阅。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值