IO到NIO的前因后果,以及NIO的用法(2)——Selector、Channel

Selector

Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

Selector——java.channels.Selector

SelectableChannel是一个类,Java通道中最重要的ServerSocketChannelSocketChannelDatagramSocketChannel都是它的间接子类。

SelectableChannel可以是阻塞的。也可以是非阻塞模式。阻塞模式下,通道上的IO操作完成之前都是阻塞的,在非阻塞模式下的IO操作,即使是传输少于要求的字节数甚至一字节也没有,也不会造成阻塞。是否处于阻塞模式,可以通过SelectableChannel类的isBlocking()方法进行判断。

SelectableChannel和Selector配合使用的基本步骤:

1. 创建一个Selector实例

一般调用Selector.open方法创建一个Selector

Selector selector = Selector.open()

2. 通过SelectableChannel的register方法,把SelectableChannel对象注册到注册到一个Selector,从而得到一个选择键SelectionKey对象。

channel.configureBlocking(false); 
SelectionKey selectionKey = channel.register(selector,SelectionKey.OP_WRITE|Selectionkey.OP_READ);

------------------------------------------------------------------------------

Channel必须是非阻塞的

所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。SocketChannel可以正常使用。

SelectableChannel抽象类有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)

注意:

SelectableChannel抽象类configureBlocking() 方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 

--------------------------------------------------------------------------

register()方法的第二个参数是一个"interest集合",意思是在通过Selector监听Channel时对什么事件感兴趣。四个不同的类型的事件:

SelectionKey.OP_WRITE:写操作

SelectionKey.OP_READ:读操作

SelectionKey.OP_ACCEPT:接受套接字操作

SelectionKey.OP_CONNECT:套接字连接操作

如果你对不止一种事件感兴趣,使用或运算符即可,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

兴趣集合也就是说该通道需要进行什么操作,但是没有准备好,也就是说还没有到读写数据的时候。

通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个ServerSocketChannel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。

3. 从Selector中选择channel

选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中.

Selector维护的三种类型SelectionKey集合:

  1. keys所有注册到Selector的Channel所表示的SelectionKey都会存在于该集合中。keys元素的添加会在Channel注册到Selector时发生。
  2. selectedKeys:该集合中的每个SelectionKey都是其对应的Channel在上一次操作selector期间被检查到至少有一种SelectionKey中所感兴趣的操作已经准备好被处理。该集合是keys的一个子集。
  3. cancelledKeys:执行了取消操作的SelectionKey会被放入到该集合中。该集合是keys的一个子集。

一旦向Selector注册了通道,就可以调用select()方法来选择一些选择键,这些选择键对应的通道已经准备好进行IO操作。

1. abstract int select()
阻塞到至少有一个通道准备好
2. abstract int select(long timeout);
和select一样,但是最长阻塞时间为timeout
3. abstract int selectNow()
非阻塞,只要有通道就绪就立刻返回

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下:

Set<SelectionKey> selectKeys = selector.selectKeys();
Iterator<SelectionKey> keyIterator = selectKeys.iterator();
while(keyIterator.hasNext()){
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()){
            //检测每一个键所对应的通道的就绪事件是否希望处理的事件
        doaccetp(key);//自定义方法
    }else if(key.isConnectable()){
        doconnectable(key);
    }else if(key.isReadable()){
        doreadable(key);
    }else if(key.isWritale()){
        dowritable(key);
    }
    keyIterator.remove();//调用remove()处理已经处理过的键。
}

4. 关闭Selector

selector.close();

Selector关闭的时候,相关的所有没有取消的选择键都会失效,注册的通道也会消失注册。

SelectionKey介绍

一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

1. key.attachment();
//返回SelectionKey的attachment,attachment可以在注册channel的时候指定
2. key.channel()
返回SelectionKey对应的channel
3. key.selector();
返回SelectionKey对应的Selector
4. key.interestOps()
返回代表需要Selector监控的IO操作的bit mask
5. key.readyOps()
返回一个bit mask , 代表相应channel上可以进行的IO操作

我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:key.interestOps()

int interestSet = selectionKey.interestOps(); 
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;

ready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.key.readyOps()

//获得就绪集合
int readySet = selectionKey.readyOps();
//检查这些操作是否就绪的方法
key.isAcceptable();//是否可读,是返回 true
boolean isWritable()://是否可写,是返回 true
boolean isConnectable()://是否可连接,是返回 true
boolean isAcceptable()://是否可接收,是返回 true

可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

final Object attach(Object obj);
----------------------------------
key.attach(theObject);
Object attachedObj = key.attachment();

Channel介绍

对于Selector进行介绍时,我们直接使用了channel变量,下面我们介绍一下Channel:

Channel主要分为两类,文件读写FileChannel以及网络读写SelectableChannel(ServerSocketChannel、SocketChannel、DatagramChannel是他的子类)

java.nio.channels

ServerSocketChannel

ServerSocketChannel与ServerSocket一样都是socket监听器,其主要区别是前者可以运行在非阻塞模式下运行。

1. 创建ServerSocketChannel

static ServerSocketChannel open();
open方法打开server端Socket的通道,打开的通道默认是不绑定的,需要后续使用bind()方法绑定

2. 绑定地址

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(5678));

abstract ServerSocketChannel bind(SocketAddress address, int backlog);
backlog指定了连接等待队列的最大长度,如果backlog=0或者负值,将不起作用,采用系统的默认值

3. 接收请求

同ServerSocket一样,ServerSocketChannel通过accept方法监听连接请求,并接收请求,当accept返回时,就得到一个对应某个客户端的SocketChannel对象,accept方法依然是阻塞的。

while(true){
    SocketChannel sc = ServerSocketChannel.accept();
    //do something...
}

4. 设置非阻塞模式

在ServerSocketChannel的父类AbstractSelectableChannel中,定义了设置非阻塞模式的方法
final SelectableChannel configureCBlocking(boolean block)
true--阻塞
false--非阻塞

4. 关闭ServerSocketChannel

ssc.close()

SocketChannel

SocketChannel以非阻塞的方式读取Socket,使用一个线程就可以和多个连接进行通信,通过把多个SocketChannel注册到Selector,之后在循环中使用Selector的select方法,一旦有事件发生,就会得到通知,进行相应的处理。

1. 创建SocketChannel

1. static SocketChannel open();
//不带参数的open方法,用于后续使用connect方法进行连接远程主机

举例:

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("http://www.foo.com",80));

2. static SocketChannel open(SocketAddress remote);
3. adstract SocketChannel accept();//ServerSocketChannel的方法,返回SocketChannel

2. 连接/终止连接SocketChannel

连接:

connect(SocketAddress remote);

可以通过isConnected()方法判断是否已经连接。

终止连接:

finishConnect();

3. 关闭SocketChannel

sc.close()

4. 读取SocketChannel

1. abstract int read(ByteBuffer dst)
从SocketChannel读取的数据要写入Buffer,之后程序通过读取Buffer来获得数据

example:
    ByteBuffer bB = ByteBuffer.allocate(100);
    int count = socketChannel.read(bB);
    从通道中读到的字节,会放到缓冲区但当前位置position开始的位置。

2. final long read(ByteBuffer[] dsts);
从通道读取字节序列,存到dsts给定的一组缓冲区中。
3. abstract long read(ByteBuffer dsts, int offset, int length);
从通道中读取的字节,从dsts缓冲区数组的offset开始的length个缓冲区中。

5. 写入SocketChannel

1. abstract int write(ByteBuffer src);
从字节缓冲区src中读取一系列字节写入通道中。
2. final long write(ByteBuffer[] srcs);
从字节缓冲区数组srcs依次读取每个缓冲区,将读取的一系列字节写入通道中。
3. abstract long write (ByteBuffer[] srcs, int offset, int length);
从字节缓冲区数组srcs一次读取每个缓冲区,将读取的一系列字节写入通道中,从offset的开始读。

6. 设置非阻塞模型

sc.configureBlocking(false);

DatagramChannel

DatagramChannel类支持以非阻塞方式发送和接收UDP数据报。它也是SelectableChannel的子类。同DatagramSocket类似,UDP不是面向连接的。

1. 创建DatagramChannel

static DatagramChannel open()

DatagramChannel dc = DatagramChannel.open()

2. 绑定地址

把通道的套接字绑定到本地地址

abstract DatagramChannel bind(SocketAddress local)

dc.bind(new InetSocketAddress(5678))

3. 接收数据报

ByteBuffer bB = ByteBuffer.allocateDirect(100);
dc.receiver(buf);

如果DatagramChannel工作在阻塞模式,那么receive方法会一直等到有可以读取的数据报才返回。

如果DatagramChannel工作在非阻塞模式,那么receive方法如果没有接收到数据报,会立即返回null。

4. 发送数据报

abstract int send(ByteBuffer src, SocketAddress target);
src是要发送的内容,具体指的是src中剩余的数据,target指的是数据报的目的地址。
返回值表示的是发送的字节数。

ByteBuffer bB = ByteBuffer.allocateDirect(100);
buf.put(msg.getBytes());
buf.flip();
int count = dc.send(bB, new InetSocketAddress("www.foo.com",5678));

同UDP的DatagramSocket类似,DatagramChannel不会接收到关于发送和接收数据报的通知。

ByteBuffer对象在通过put方法存入数据后,需要调用flip()方法重置position的位置,以便把刚存入的数据重新读出。需要重复读取ByteBuffer时,相应地,可以调用rewind()方法。

5. 管理固定连接

abstract DatagramChannel connect(SocketAddress remote);
//与特定的主机remote建立固定连接
abstract DatagramChannel disconnect()
//解除固定连接关系
abstarct boolean isConnected()
//只有当DatagramChannel打开并且有固定连接的时候才返回true

6. 读写数据

read和receive方法一样,都能接收数据报。使用read方法时,通道必须已经与某个远程主机有固定连接。

abstarct int read(BYteBuffer dst)
final long read(ByteBuffer[] dsts)
abstarct long read(ByteBuffer[] dsts, int offset, int length)

abstarct int write(ByteBuffer[] srcs);
final long write(ByteBuffer[] srcs);
abstract long write(ByteBuffer[] srcs, int offset, int length)

FileChannel

暂不做介绍

模板代码:

有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码

服务端:

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress("localhost",8080));
ssc.configureBlocking(false);

Selector selector = Selector.open();
ssc.reister(selector, SelectionKey.OP_ACCEPT);

while(true){
    int readyNum = selector.select();
    if(readyNum == 0){
        continue;
    }
    
    Set<SelectionKey> selectionKeys = seletor.selectedKeys();
    Iterator<SelectionKey> it = selectedKeys.iterator();
    
    while(it.hasNext()){
        SelectionKey key = it.next();
        if(key.isAcceptable()){
            //创建连接,并且把连接注册到selector
            SocketChannel socketChannel = ssc.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        }else if(key.isConnectable()){
            
        }else if(key.isWritable()){
            writeBuffer.rewind();
            SocketChannel socketChannel = (SocketChannel)key.channel();
            socketChannel.write(writeBuffer);
            key.interestOps(SelectionKey.OP_READ);
        }else if(key.isReadable()){
            SocketChannel socketChannel = (SocketChannel)key.channel();
            readBuffer.clear();
            socketChannel.read(readCBuffer);
            readBuffer.flip();
            System.out.println(new String(readBuffer.array()));
            key.interestOps(SelectionKey.OP_WRITE);
        }
        it.remove();
    }
}

举例:客户端和服务端交互

服务端:

try{
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.bind(new InetSocketAddress("127.0.0.1",5678));
    ssc.configureBlocking(false);
    
    Selector selector = Selector.open();
    ssc.register(selector, SelectionKey.OP_ACCEPT);
    
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    
    writeBuffer.put("recevive:".getBytes());
    writeBuffer.flip();
    
    while(true){
        int nReady = selector.select();
        if(nReady == 0){
            continue;
        }
        Set<SelectionKey> keys = selector.selectiedKeys();
        Iterator<SelectionKey> it = keys.iterator();
        while(it.hasNext()){
            SelectionKey key = it.next();
            if(key.isAcceptable()){
                //创建新的连接
                SocketChannel socketChannel = ssc.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SeletionKey.OP_READ);
            }else if(key.isReadable()){
                SocketChannel socketChannel = (SocketChannel)key.channel();
                readBuffer.clear();
                socketChannel.read(readCBuffer);
                
                readBuffer.flip();
                System.out.println(new String(readBuffer.array()));
                key.interestOps(SelectionKey.OP_WRITE);
             }else if(key.isWritable()){
                writeBuffer.rewind();
                SocketChannel socketChannel = (SocketChannel)key.channel();
                socketChannel.write(writeBuffer);
                key.interestOps(SelectionKey.OP_READ);
            }
        }
    }
    
}catch(Exception e){
    e.printStatckTrace()
}

客户端:

try{
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("127.0.0.1",5678));
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    
    writeBuffer.put("hello".getBytes());
    writeBuffer.flip();
    
    while(true){
        writeBuffer.rewind();
        socketChannel.write(writeBuffer);
        
        readBuffer.clear();
        socketChannel.read(readBuffer);
    }
}catch(Exception e){
    e.printStackTrace();
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章