17

java NIO理解分析与基本使用

 4 years ago
source link: http://www.cnblogs.com/buptleida/p/12633675.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我前段时间的一篇博客 java网络编程——多线程数据收发并行 总结了服务端与客户端之间的收发并行实践。原理很简单,就是针对单一客户端,服务端起两个线程分别负责read和write操作,然后线程保持阻塞等待读写执行。

事实上,这样的模式非常糟糕。因为每一个客户端在服务端需要占用两条线程,假如有1000个客户端,则需要2000+条线程。cpu需要花费大量的时间进行线程上下文切换,造成系统资源浪费。

想要缩减线程数量,先要解决阻塞问题。而NIO可以通过IO多路复用将read和write的阻塞给抹去。再配合线程池,即可实现用少量的线程支撑起上百万个客户端的连接。

什么是NIO

NIO与IO多路复用

java NIO全称java non-blocking IO。字面意思即非阻塞式IO。实际上这里的非阻塞只是宏观的说法。

关于IO模式,这里引一个别人的博客,介绍了几种IO模式的区别:

简述同步IO、异步IO、阻塞IO、非阻塞IO之间的联系与区别

本博客不再赘述这些,只是想说NIO属于其中的IO复用模型。(实验室里有一本《UNIX网络编程》疫情结束回学校一定把这部分好好看看)

多路复用IO模型中,会有一个线程去不断轮询多个socket的状态,当socket有读写事件时,才来调用IO操作。因为是一个线程来管理多个socket,系统不需要建立其它线程、维护线程,只有socket就绪时,才会使用IO资源,所以它大大降低了资源占用。

java NIO中,使用selector.select()监听多个通道是否有到达事件,没有事件就一直阻塞,有事件就调用IO进行处理。

三大核心

  • 通道(Channel)
  • 缓冲区(Buffer)
  • 选择器(Selectors)

详细介绍如下

7NBBBjF.png!web

NIO使用举例

这里以服务端读取客户端消息的流程为例,介绍NIO的使用(完整内容只有输入,暂且不管输出)。画了一个流程图,如下所示:

ZRvUniA.png!web
  1. 建立selector和ServerSocketChannel,并绑定注册,用于监听客户端连接请求,代码如下:
selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
// 设置为非阻塞
server.configureBlocking(false);
// 绑定本地端口
server.socket().bind(new InetSocketAddress(port));
// 注册客户端连接到达监听
server.register(selector, SelectionKey.OP_ACCEPT);

同时还要建立readSelector和writeSelector。其实线程池也是提前建立的,这里暂且不写。

readSelector = Selector.open();
writeSelector = Selector.open();
  1. 监听通道,得到客户端,并建立SocketChannel,用于监听后续客户端消息
//select()方法返回已就绪的通道数
if (selector.select() == 0) {
    continue;
}

Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {

    SelectionKey key = iterator.next();
    iterator.remove();

    // 检查当前Key的状态是否是accept的
    // 客户端到达状态
    if (key.isAcceptable()) {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        // 非阻塞状态拿到客户端连接
        SocketChannel socketChannel = serverSocketChannel.accept();

        try {
            // 客户端构建异步线程
            // 添加同步处理
            //此处代码暂且忽略
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("客户端连接异常:" + e.getMessage());
        }
    }
  1. 将SocketChannel注册进readSelector和writeSelector
/**
*参数分别是:channel,对应的selector,以及
*registerOps:待注册的操作集,这个在后文中有详细解析;
*locker:用于标识同步代码块的状态,是锁定还是可用;
*runnable:执行具体读写操作的类,送给线程池执行;
*map:建立SelectionKey与Runnable映射关系的HashMap。
*/
private static SelectionKey registerSelection(SocketChannel channel, Selector selector,
                                                  int registerOps, AtomicBoolean locker,
                                                  HashMap<SelectionKey, Runnable> map,
                                                  Runnable runnable) {
    synchronized (locker) {
    // 设置锁定状态
    locker.set(true);
    
    try {
        // 唤醒当前的selector,让selector不处于select()状态
        //注册channel时一定要将selector唤醒,否则当前select中没有刚注册的channel
        selector.wakeup();
    
        SelectionKey key = null;
        if (channel.isRegistered()) {
            // 查询是否已经注册过
            key = channel.keyFor(selector);
            if (key != null) {
            //将新的Ops添加进去
                key.interestOps(key.readyOps() | registerOps);
            }
        }
    
        if (key == null) {
            // 注册selector得到Key
            key = channel.register(selector, registerOps);
            // 注册回调
            map.put(key, runnable);
        }
    
        return key;
    } catch (ClosedChannelException e) {
        return null;
    } finally {
        // 解除锁定状态
        locker.set(false);
        try {
            // 通知
            locker.notify();
        } catch (Exception ignored) {
        }
    }
    }
}
  1. 监听各个客户端消息,通过selectionKeys获取channel,再执行输入操作
try {
if (readSelector.select() == 0) {
    continue;
}

Set<SelectionKey> selectionKeys = readSelector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
    if (selectionKey.isValid()) {
        //IO处理代码,暂且忽略
    }
}
selectionKeys.clear();
} catch (IOException e) {
e.printStackTrace();
}

注意:以上都是一些代码片段,没有完全串联起来,省略了一些类对象调用、方法调用以及关键的线程池操作等等。但是基本的方法已经展示出来,剩下的后面的博客再去填坑。

光看上面的代码,对于一些NIO方法的认知还是很模糊的。下面通过阅读selector类和SelectionKey类的源码注释,来加深对部分方法的理解。

selector类

selector是NIO的核心类,下面是选择器的一些重要方法:

  • open相关
    • open()开启一个selector
    • public abstract boolean isOpen(); 判断是否开启
public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}
  • keys相关
    • public abstract Set keys();返回所有key的集合
    • public abstract Set selectedKeys();返回已被选择的key的集合
  • select
    • 下面几个方法都是返回已就绪通道的数量,可能是0;
    • selectNow(),非阻塞方法;
    • select(),仅在三种情况下返回,1.通道被选择;2.调用wakeup方法;3.线程中断。
    • select(timeout),比select()多一个解除阻塞的条件,即超时。
  • wakeup(),解除正在阻塞的select方法的阻塞,立即返回
  • close(),关闭selector。

SelectionKey类

注册进selector的任何一个channel都用一个SelectionKey对象来指代。

操作集

  • Operation-set:操作集,一些常量int值,代表各种类型的操作;一个selection key包含两个操作集,interest set和ready-operation set
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
  • interest set:兴趣集;一个channel所有的操作集;可通过interestOps(int)方法来更新

  • ready-operation set:就绪操作集,只包含使得channel被报告就绪的操作,底层通过与或操作来更新;例如当一个channel读取就绪时,将read操作集加入到就绪集中。

方法列表

  • public abstract SelectableChannel channel():返回此选择键所关联的通道.即使此key已经被取消,仍然会返回;
  • public abstract Selector selector():返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回;
  • public abstract boolean isValid():检测此key是否有效.当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效;
  • public abstract void cancel():请求将此键取消注册.一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合);
  • public abstract int interesOps():获得此键的interes集合;
  • public abstract SelectionKey interestOps(int ops):将此键的interst设置为指定值.此操作会对ops和channel.validOps进行校验.如果此ops不会当前channel支持,将抛出异常;
  • public abstract int readyOps():获取此键上ready操作集合.即在当前通道上已经就绪的事件;
  • public final boolean isReadable(): 检测此键"read"事件是否就绪.等效于:(readyOps() & OP_READ) != 0;还有isWritable(),isConnectable(),isAcceptable()
  • public final Object attach(Object ob):将给定的对象作为附件添加到此key上.在key有效期间,附件可以在多个ops事件中传递;
  • public final Object attachment():获取附件.一个channel的附件,可以再当前Channel(或者说是SelectionKey)生命周期中共享,但是attachment数据不会作为socket数据在网络中传输。

终于写完了,这篇博客只能算是对NIO简单介绍,一些东西还没讲到。channel和buffer部分的方法没有分析,线程池部分没有加上,还有输出操作那一套,都没讲。总想尽可能多地详细完整一点,但是越深入,知识点就越庞大,所以只能放弃一部分内容,于是成了现在这个样子。如果详细规划一下拆开多个博客写会更好。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK