NIO(二)——SELECT

本文的行文思路是对照一个典型的例子,将select的整个IO过程捋一遍。,另外还会涉及一些平时容易忽略的知识点。

概述
IO过程主要分为:1)数据准备阶段;2)数据操作阶段。所谓的阻塞非阻塞IO,主要指的都是第一个阶段。
NIO主要有三大部分:Channel,Buffer,Selector。NIO(Non-Blocking IO或New IO)是相对于BIO(Blocking IO)而言的。BIO是传统的IO模型,它基于字节流和字符流进行操作,每个socket对应一个线程,因此整个过程需要多个线程;NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,在IO的第一个阶段——数据准备阶段,可以用单个线程可以监听多个数据通道(第二个阶段属于memory copy,速度极快,故可以用单个线程操作,当然也可以用多个线程操作)。

先上一张图,描述的数据在这几个组件中的流动情况

这里写图片描述

下面的文字大部分是从http://www.importnew.com/19816.html摘录下来的。

Channel
首先说一下Channel,国内大多翻译成“通道”。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO中的Channel的主要实现有:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是围绕这4个类型的Channel进行陈述的。

Buffer
NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等这里先不进行陈述。

Selector
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

关于Buffer的部分,就不细讲了,比较简单,有需要的可以参考这篇文章:http://www.importnew.com/19816.html ,Buffer的部分讲的比较详细。

我们主要讲述另外的两个部分SelectableChannel(属于Channel)和Selector
NIO的Channel都是非阻塞的,因此像FileChannel这种阻塞的Channel就不在讨论之列。
从流程逻辑上来讲,整个过程首先是SelectableChannel对象注册到Selector对象中,这个注册关系用一个SelectionKey对象来保存。SelectionKey包含了两个集合(该集合是用int型表示的比特集,一个是interest集合,一个是ready集合,分别表示注册初始化时标注对哪些感兴趣及有哪些事件已将就绪,ready是interest的子集)当有SelectableChannel就绪,Selector就会在对应的SelectionKey的ready集合记上标记。IO第一阶段就结束了,然后就可以进入IO的第二阶段,第二阶段就可以用一个线程或线程池来对数据进行操作了(read或write)。SelectableChannel和Selector的关系非常非常重要,因此这个SelectionKey对象非常非常重要。

再稍微详细介绍一下这三个类:Selector,SelectableChannel,SelectionKey
Selector:
Selector代表的是操作系统,它是“BOSS”,由他来改变每个通道的就绪状态,而我们可以通过API(readyOps())来获取通道的状态!ready集合的状态只能由操作系统(selector)来改变!!!
SelectableChannel
这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。 FileChannel 对象不是可选择的,因为它们没有继承 SelectableChannel。SelectableChannel 可以被注册到 Selector 对象上,同时可以指定感兴趣的操作。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
SelectionKey
SelectionKey非常重要,着重介绍一下。当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性:interest集合,ready集合,Channel,Selector。
interest集合:就像向Selector注册通道一节中所描述的,interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey读写interest集合。
ready 集合,是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合:
int readySet = selectionKey.readyOps();
可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。
每个Selector对象维护三个SelectionKey的集合,或者说SelectionKey存在三种状态,这三种状态用三种集合Set来存放:
1),已注册键的集合(registered key set),通过keys()返回。这是一个总的,并且已注册的键的集合是不能直接修改的。
2),已选择键的集合(selected key set),通过selectKeys()返回。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且它ready集合必定是interest集合的子集。
3),已取消键的集合(cancelled key set), 这个集合包含了 cancel( )方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

对于整个过程,用一个完整的程序来说明。

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class Main
{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;

    public static void main(String[] args)
    {
        selector();
    }

    public static void handleAccept(SelectionKey key) throws IOException{
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));
    }

    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }
    }

    public static void handleWrite(SelectionKey key) throws IOException{
        ByteBuffer buf = (ByteBuffer)key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while(buf.hasRemaining()){
            sc.write(buf);
        }
        buf.compact();
    }

    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            selector = Selector.open();
            ssc= ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress(PORT));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            while(true){
                if(selector.select(TIMEOUT) == 0){
                    System.out.println("==");
                    continue;
                }
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }

        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

程序很简单。我们主要关注selector()函数,函数首先生成一个Selector对象,一个SelectableChannel,即ServerSocketChannel,它属于服务端的SelectableChannel,它通过调用accept()函数能生成新的SocketChannel(多嘴一句,这属于socket网络编程部分,不熟的可自行查阅相关资料)。ServerSocketChannel的socket部分bind对应的IP和port,然后再注册到Selector中,然后就进入死循环中。死循环中的关键是select()函数。
(SelectionKey的三种键的集合都是用Set表示, 而interest和ready的集合是用int值来表示。因此当interest和ready的值发生改变,也就是意味着其对应的集合发生了改变。)
下面只说主干逻辑:
1)检查取消的键的集合,它表示的是要取消的通道的注册关系。因此如果非空,那么这个键就会被从另外两个集合中移除,且相关的通道也会被注销。注,这个集合是Selector对象的private成员,因此无法直接访问。
2)检查已注册的键的集合。采用的是select模式的检查,目的是检查有哪些channel已经就绪,具体检查的过程见http://blog.csdn.net/nyyjs/article/details/76209036的图二SELECT模式。这里再重复一遍这个逻辑,调用select()时,内核开始检查register过的那些channel的状态,看哪些channel已经就绪,这个检查的过程的模型相当于检查一个数组,这些被register的channel的状态信息被放在这些数组里,然后内核开始从头遍历该数组,如果发现有channel状态已经就绪,那么就将该channel的状态拷贝到另外一个数组中(另外的这个数组中的channel将会作为结果被返回给应用层),直至把该数组遍历完,然后把最终结果返回给应用层。在应用层中分为两个部分处理:该键已经就绪,那么Selector对象就会将对应的ready集合置为相应的就绪状态(write,read,connect,accept中的一个或多个),直观来说就是ready的int值发生了变化。同时该键也会被复制在已选择键的集合(set集合)中一份;该键没有就绪,则该键的ready集合会被清空。强调一下,只有在select()期间,ready的值才会发生改变。
3)select()返回的是一个int值,它表示的是ready值发生了改变的selectionKey的数目,它表示的“增量”。而已选择键的集合set中表示的是已经就绪的选择键的总数目,它表示的是“总量”。我们的判断条件是,如果第2步结束后,返回的额“增量”值为零那么就重复1,2步,直到返回的“增量”值不为0。
4)返回的“增量”非零时。调用selectKeys(),返回已选择键的集合。我们按顺序检查每个键,相关的通道也会根据键的集合进行处理,处理完之后该键将从已选择键的集合中被移除(调用Iterator的remove()方法),直至该集合处理完。
5)重复1-4步
6)根据key的状态,进行实际的IO数据操作(write或read)。至此,整个逻辑完。

补充说明:
1)注销channel是一个代价很高的操作,这可能需要重新分配资源。Selectorkey和channel
是相关的,他们之间有非常复杂的交互过程。因此,这个漫长的注销必须要慎重,须在合适的时候进行,防止与正在进行的select()冲突;
2)三者都有close()方法,他们之间的作用是不一样的。
Selector 是“老大”,它的关闭是调用close()方法:结果是所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)
SelectionKey表示的是一种特定的注册关系,它的的关闭调用cancel()方法:它表示终结这种特定的注册关系。当被取消之后,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey 将被返回。
SelectionChannel表示一个channel,它的关闭调用的是close():所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)
3)并非每种通道类型都支持所有的操作(read,write,connect,accept共四种操作)。如socketchannel不支持accept,只有ServerSocketChannel支持accept。调用validOps()可以查看该通道支持的操作类型
4)关于channnel的读写操作,其针对的对象是channel(不是buffer),因此:channel.read(buffer)的意思是从channel读数据到buffer,channel.write(buffer)的意思是把buffer的数据写到channel

就这么多吧,以后想到再补充。

参考:
http://www.importnew.com/19816.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值