java nio基础

前言

这两天比较空闲,准备研究下netty,看的掘金小册Netty 入门与实战:仿写微信 IM 即时通讯系统,文中开始就用java nio做演示,看完后意识到对java nio还不够理解,因此决定先研究下java的nio.

java nio

关于io模式可以参考我写过的另一篇文章IO模式和IO多路复用,java中的nio对于不同平台实现也是不同的,参见java 和netty epoll实现

重要组成
  • Channel 通道,和阻塞io中的stream很类似,只不过Channel是双向的
  • Buffer 缓冲区,用来缓冲数据
  • Selectors 选择器 参见IO模式和IO多路复用中nio原理部分
三者的简要说明

这部分主要参考Java NIO Tutorial

Channel

主要有下面四种Channel

  1. FileChannel 对应文件IO
  2. DatagramChannel 对应UDP网络IO
  3. SocketChannel 对应TCP网络IO
  4. ServerSocketChannel 服务端使用,监听TCP连接
Buffer 缓冲区

这部分基本复制自理解Java NIO.

缓冲区对象有四个基本属性:

  • 容量Capacity:缓冲区能容纳的数据元素的最大数量,在缓冲区创建时设定,无法更改
  • 上界Limit:缓冲区的第一个不能被读或写的元素的索引
  • 位置Position:下一个要被读或写的元素的索引
  • 标记Mark:备忘位置,调用mark()来设定mark=position,调用reset()设定position=mark

这四个属性总是遵循这样的关系:0<=mark<=position<=limit<=capacity,Buffer对象中很多方法都是对这几个属性做设置,如flip,rewind,clear,reset等等. 下图是新创建的容量为10的缓冲区逻辑视图

执行五次调用put后的缓冲区如下图

    buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
复制代码

现在缓冲区满了,我们必须将其清空。我们想把这个缓冲区传递给一个通道,以使内容能被全部写出,但现在执行get()无疑会取出未定义的数据。我们必须将 posistion设为0,然后通道就会从正确的位置开始读了,但读到哪算读完了呢?这正是limit引入的原因,它指明缓冲区有效内容的未端。这个操作在缓冲区中叫做翻转对应的方法是buffer.flip()

flip方法的源码如下:


/** * Flips this buffer.  The limit is set to the current position and then  the position is set to zero.  If the mark is defined then it is  discarded.  After a sequence of channel-read or put operations, invoke  this method to prepare for a sequence of channel-write or relative get operations.
*/
public final Buffer flip() {    limit = position;    position = 0;    mark = -1;    return this;}
复制代码

随便提一下rewind操作,它一般用来重复读取buffer,与flip相似,但是不影响limit。

Selectors

一图胜千言

Channel和Buffer
Channel和Buffer的关系

Buffer和Channel的关联如下图

nio中,数据总是从Channel读取到Buffer或从Buffer写入到Channel( 这里的读取到和写入有点拗口,两者本质上都是write操作)

Channel和Buffer的基本操作

   RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
    FileChannel inChannel = aFile.getChannel();

    ByteBuffer buf = ByteBuffer.allocate(48);

    int bytesRead = inChannel.read(buf);
    while (bytesRead != -1) {

      System.out.println("Read " + bytesRead);
      buf.flip();

      while(buf.hasRemaining()){
          System.out.print((char) buf.get());
      }

      buf.clear();
      bytesRead = inChannel.read(buf);
    }
    aFile.close();
复制代码

从上例可以看出nio中读取写入的一般逻辑:

  1. 打开一个Channel对象c
  2. 创建Buffer对象b(,如果是写入到Channel,这里会对Buffer进行写入)
  3. 从c中循环读取到到b,或者从b写入到到c

一般从Channel写入到Buffer,调用的是Channel.read(Buffer b),而从Buffer写入到Channel调用的是Channel.write(Buffer b),笔者之前这个地方经常搞混,实质在于这里的读写是站在Channel的角度来说的

Selector详解

在阻塞IO中,一个线程对应一个连接,对于成千上万的连接显然是hold不住(另外线程切换的消耗也是很大的).nio中的Selector就是为了解决这个问题出现的:一个Selector对应并处理多个连接,也就是单个线程对应多个连接.


# 注册到selector

Selector selector = Selector.open();
//使用selector时,channel必须设置为非阻塞IO,由于FileChannel只能是阻塞的,所以也肯定不能为FileChannel

channel.configureBlocking(false);
//将该连接注册到Selector **1**
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


# selector死循环等待事件到来
while(true) {

  //如果没有可*处理*的channel,继续循环 **3**
  int readyChannels = selector.select();

  if(readyChannels == 0) continue;

   //拿到可处理channel列表 **2**
  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();
    //这里根据事件类型来分别处理
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    // **4*
    keyIterator.remove();
  }
}
复制代码
  1. 注册channel到selector时,需要选定该channel感兴趣的事件类型,这里可以使用或操作 |

    1. connect 连接事件
    2. accept 连接接受事件
    3. read 读事件就绪
    4. write 写事件就绪
  2. SelectionKey 这个对象有以下几个属性

    1. interestOps() 该key感兴趣的事件类型列表
    2. readyOps 准备就绪的事件类型列表
    3. attach(theObject) 附加Obj到这个key上,可以是用来标识这个key或者存储其他信息
  3. selector.select()返回就绪的channel数量,一共有三种形式

    1. select() 等待直到至少一个channel就绪才会返回
    2. select(long timeout),等待指定时间或至少一个channel就绪才会返回
    3. selectNow() 非阻塞,立刻返回,实现上复用了2 ,即select(0)

    结合IO模式和IO多路复用关于阶段的说明,对于1,2第一阶段是阻塞的,对于3第一阶段是非阻塞的 selector.selectedKeys()返回所有就绪的key,可以根据事件类型对这些key进行处理

  4. selector.selectedKeys()返回内部变量publicSelectedKeys,但是在遍历publicSelectedKeys中的key后,这些key不会自动移除,这里需要自己手动删除,如果一个key k1对应的事件已经被处理了但是没有手动移除,那么下次调用selector.selectedKeys()它还会被返回(在k1再次变为准备状态这段时间,就相当于对一个不可能触发的key做无用遍历)

总结

本文简单介绍了java nio的基本概念,Channel,Buffer,Selector等的作用.写完这篇也扫清了自己对Selector的疑问.希望能对大家有点帮助.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值