NIO Buffer、Channel、Selector分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013857458/article/details/82427412
Buffer

这里写图片描述

public abstract class Buffer {
    ...
    // Invariants: mark <= position <= limit <= capacity
      private int mark = -1;
      private int position = 0;
      private int limit;
      private int capacity;
    ...
}
  • 容量( Capacity)
    缓冲区能够容纳的数据元素的最大数量,可以理解为数组的长度。 这一容量在缓冲区创建时被设定,并且永远不能被改变。
  • 上界( Limit)
    缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
  • 位置( Position)
    下一个要被读或写的元素的索引。Buffer类提供了get( )和 put( )函数 来读取或存入数据,position位置会自动进行相应的更新。
  • 标记( Mark)
    一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。标记在设定前是未定义的(undefined)。
    这四个属性之间总是遵循以下关系:
    0 <= mark <= position <= limit <= capacity

缓冲区API

public abstract class Buffer {
    //JDK1.4时,引入的api
    public final int capacity( )//返回此缓冲区的容量
    public final int position( )//返回此缓冲区的位置
    public final Buffer position (int newPositio)//设置此缓冲区的位置
    public final int limit( )//返回此缓冲区的限制
    public final Buffer limit (int newLimit)//设置此缓冲区的限制
    public final Buffer mark( )//在此缓冲区的位置设置标记
    public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
    public final Buffer clear( )//清除此缓冲区
    public final Buffer flip( )//反转此缓冲区
    public final Buffer rewind( )//重绕此缓冲区
    public final int remaining( )//返回当前位置与限制之间的元素数
    public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
    public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区

    //JDK1.6时引入的api
    public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
    public abstract Object array();//返回此缓冲区的底层实现数组
    public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}

//flip()
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
//clear()
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
//重头开始
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}
//新建一个堆缓存
public static ByteBuffer wrap(byte[] array,int offset, int length){
   try {
       return new HeapByteBuffer(array, offset, length);
   } catch (IllegalArgumentException x) {
       throw new IndexOutOfBoundsException();
   }
}

//get
public byte get() {
    return hb[ix(nextGetIndex())];
}
final int nextGetIndex() {                          // package-private
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

//put
public ByteBuffer put(byte x) {
    hb[ix(nextPutIndex())] = x;
    return this;
}

ByteBuffer的实现类包括”HeapByteBuffer”和”DirectByteBuffer”两种。

HeapByteBuffer
public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) {  
    super(-1, 0, lim, cap, new byte[cap], 0);
}

HeapByteBuffer通过初始化字节数组hd,在虚拟机堆上申请内存空间。

DirectByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

DirectByteBuffer通过unsafe.allocateMemory申请堆外内存,并在ByteBuffer的address变量中维护指向该内存的地址。
unsafe.setMemory(base, size, (byte) 0)方法把新申请的内存数据清零。

Channel

通道(Channel)可以理解为数据传输的管道。通道与流不同的是,流只是在一个方向上移动(一个流必须是inputStream或者outputStream的子类),而通道可以用于读、写或者同时用于读写。
Channel类继承关系
这里写图片描述
I/O 可以分为广义的两大类别: File I/O 和 Stream I/O。

那么相应地有两种类型的通道也就不足为怪了,它们是文件( file)通道和套接字( socket)通道。仔细看一下上图,你会发现有一个 FileChannel 类和三个 socket 通道类: SocketChannel、 ServerSocketChannel 和 DatagramChannel。

Channel单向还是双向看它有没有实现ReadableByteChannel、WritableByteChannel接口,实现了这两个接口就是双向的通道。

public interface ReadableByteChannel extends Channel {
    public int read(ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel{
    public int write(ByteBuffer src) throws IOException;
}

可以看出read和write都是接受一个ByteBuffer参数,read就是往ByteBuffer中put数据,write就是往ByteBuffer中get数据,以便发送给远程的主机。

在通道类中,DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而 ServerSocketChannel不实现。 ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。

全部 NIO中的socket 通道类( DatagramChannel、 SocketChannel 和 ServerSocketChannel)在被实例化时都会创建一个对等的BIO中的 socket 对象( Socket、 ServerSocket和 DatagramSocket)。

DatagramChannel、 SocketChannel 和 ServerSocketChannel通道类都定义了socket()方法,我们可以通过这个方法获取其关联的socket对象。另外每个Socket、 ServerSocket和 DatagramSocket都定义了getChannel()方法,来获取对应的通道。

需要注意是,只有通过通道类创建的socket对象,其getChannel方法才能返回对应的通道,如果直接new了socket对象,那么其getChannel方法返回的永远是null。

ServerSocketChannel

让我们从最简单的 ServerSocketChannel 来开始对 socket 通道类的讨论。

public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{

    protected ServerSocketChannel(SelectorProvider provider) {
        super(provider);
    }


    public static ServerSocketChannel open() throws IOException {
        return SelectorProvider.provider().openServerSocketChannel();
    }

    public final int validOps() {
        return SelectionKey.OP_ACCEPT;
    }

    public final ServerSocketChannel bind(SocketAddress local) throws IOException{
        return bind(local, 0);
    }
    //绑定端口,与监听端口
    public abstract ServerSocketChannel bind(SocketAddress local, int backlog)
        throws IOException;

    public abstract ServerSocket socket();

    public abstract SocketChannel accept() throws IOException;

    @Override
    public abstract SocketAddress getLocalAddress() throws IOException;
}

ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket执行相同的基本任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。

用静态的 open( )工厂方法创建一个新的 ServerSocketChannel 对象,将会返回同一个未绑定的java.net.ServerSocket 关联的通道。该对等 ServerSocket 可以通过在返回的 ServerSocketChannel 上调用 socket( )方法来获取。作为 ServerSocketChannel 的对等体被创建的 ServerSocket 对象依赖通道实现。这些 socket 关联的 SocketImpl 能识别通道。通道不能被封装在随意的 socket 对象外面。

绑定和监听在bind方法中完成,accept接受客户端请求。

SocketChannel
public abstract class SocketChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{
    protected SocketChannel(SelectorProvider provider) {
        super(provider);
    }

    public static java.nio.channels.SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel();
    }

    public static java.nio.channels.SocketChannel open(SocketAddress remote) throws IOException {
        java.nio.channels.SocketChannel sc = open();
        try {
            sc.connect(remote);
        } catch (Throwable x) {
            try {
                sc.close();
            } catch (Throwable suppressed) {
                x.addSuppressed(suppressed);
            }
            throw x;
        }
        assert sc.isConnected();
        return sc;
    }

    public final int validOps() {
        return (SelectionKey.OP_READ
                | SelectionKey.OP_WRITE
                | SelectionKey.OP_CONNECT);
    }
    @Override
    public abstract java.nio.channels.SocketChannel bind(SocketAddress local)
            throws IOException;

    @Override
    public abstract <T> java.nio.channels.SocketChannel setOption(SocketOption<T> name, T value)
            throws IOException;

    public abstract java.nio.channels.SocketChannel shutdownInput() throws IOException;
    public abstract java.nio.channels.SocketChannel shutdownOutput() throws IOException;

    public abstract Socket socket();

    public abstract boolean isConnected();

    public abstract boolean isConnectionPending();

    public abstract boolean connect(SocketAddress remote) throws IOException;
    public abstract boolean finishConnect() throws IOException;
    public abstract SocketAddress getRemoteAddress() throws IOException;
    public abstract int read(ByteBuffer dst) throws IOException;

    public abstract long read(ByteBuffer[] dsts, int offset, int length)
            throws IOException;

    public final long read(ByteBuffer[] dsts) throws IOException {
        return read(dsts, 0, dsts.length);
    }

    public abstract int write(ByteBuffer src) throws IOException;

    public abstract long write(ByteBuffer[] srcs, int offset, int length)
            throws IOException;

    public final long write(ByteBuffer[] srcs) throws IOException {
        return write(srcs, 0, srcs.length);
    }
    public abstract SocketAddress getLocalAddress() throws IOException;

}

Socket 和 SocketChannel 类封装点对点、有序的网络连接,就是我们所熟知并喜爱的 TCP/IP网络连接。 SocketChannel 扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收。

每个 SocketChannel 对象创建时都是同一个对等的 java.net.Socket 对象串联的。静态的 open( )方法可以创建一个新的 SocketChannel 对象,而在新创建的 SocketChannel 上调用 socket( )方法能返回它对等的 Socket 对象;在该 Socket 上调用 getChannel( )方法则能返回最初的那个 SocketChannel。

新创建的 SocketChannel 虽已打开却是未连接的。在一个未连接的 SocketChannel 对象上尝试一个 I/O 操作会导致 NotYetConnectedException 异常。我们可以通过在通道上直接调用 connect( )方法或在通道关联的 Socket 对象上调用 connect( )来将该 socket 通道连接。一旦一个 socket 通道被连接,它将保持连接状态直到被关闭。您可以通过调用布尔型的 isConnected( )方法来测试某个SocketChannel 当前是否已连接。

面向流的的 socket 建立连接状态需要一定的时间,因为两个待连接系统之间必须进行包对话以建立维护流 socket 所需的状态信息。跨越开放互联网连接到远程系统会特别耗时。假如某个SocketChannel 上当前正由一个并发连接, isConnectPending( )方法就会返回 true 值。

调用 finishConnect( )方法来完成连接过程,该方法任何时候都可以安全地进行调用。

Selector

选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能,就绪选择和多元执行使得单线程能够有效率地同时管理多个 I/O 通道(channels)。需要将一个或多个Channel注册到Selector中,一个键(SelectionKey)将会被返回。SelectionKey 会记住您关心的通道。Selector会追踪对应的通道是否就绪。
这里写图片描述
每个Channel在注册到Selector上的时候,都有一个感兴趣的操作。

  • 对于ServerSocketChannel,只会在选择器上注册一个,其感兴趣的操作是ACCEPT,表示其只关心客户端的连接请求
  • 对于SocketChannel,通常会注册多个,因为一个server通常会接受到多个client的请求,就有对应数量的SocketChannel。So从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个 I/0 操作的能力。例如,我们需要了解一个 SocketChannel 对象是否还有更多的字节需要读取,或者我们需要知道ServerSocketChannel 是否有需要准备接受的连接。

当调用一个Selector对象的 select( )方法时,相关的SelectionKey 会被更新,用来检查所有被注册到该选择器的通道是否已经准备就绪。也就是说,程序需要主动的去调用Selector.select()方法。 select() 方法会返回一个准备就绪的SelectionKey的集合。通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。cketChannel感兴趣的操作是CONNECT、READ、WRITE,因为其要于server建立连接,也需要进行读、写数据。

创建选择器

Selector 对象是通过调用静态工厂方法 open( )来实例化的。选择器不是像通道或流(stream)那样的基本 I/O 对象:数据从来没有通过它们进行传递。类方法 open( )向 SPI 发出请求,通过默认的 SelectorProvider 对象获取一个新的实例。通过调用一个自定义的 SelectorProvider对象的 openSelector( )方法来创建一个 Selector 实例也是可行的。您可以通过调用 provider( )方法来决定由哪个 SelectorProvider 对象来创建给定的 Selector 实例。大多数情况下,您不需要关心 SPI;只需要调用 open( )方法来创建新的 Selector 对象。

方式一:
Selector selector = Selector.open( );

方式二:
SelectorProvider provider = SelectorProvider.provider();
Selector abstractSelector = provider.openSelector();
注册通道到选择器上

注册通道到选择器上,是通过register方法进行的。

通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用 configureBlocking(false))。

ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("localhost",80));
ssc.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey sscSelectionKey = ssc.register(selector, SelectionKey.OP_ACCEPT);//注册ServerSocketChannel
while(true){
    SocketChannel sc = ssc.accept();
    if(sc==null){
        continue;
    }
    sc.configureBlocking(false);
    //注册SocketChannel
    SelectionKey scselectionKey = sc.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_WRITE);
    //...其他操作
}

Selector的实现类中有两个集合,一个SelectionKey关联着一个Channel,有这个Channel感兴趣的事件和就绪事件的属性

//就绪的key,即就绪的Channel
protected Set<SelectionKey> selectedKeys = new HashSet();
//注册到Selector的Channel
protected HashSet<SelectionKey> keys = new HashSet();
选择键(SelectionKey)

SelectionKey对象被register( )方法 返回,并提供了方法来表示表示这种注册关系。

public abstract SelectableChannel channel( ); //获得这个SelectionKey关联的channel
public abstract Selector selector( ); //获得这个selectionKey关联的Selector

同时选择键包含指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

public abstract int interestOps( );//感兴趣兴趣的操作
public abstract int readyOps( );//感兴趣的操作中,已经准备就绪的操作

可以通过调用键的 readyOps( )方法来获取相关的通道的已经就绪的操作。 ready 集合是 interest集合的子集 。当前的 interest 集合可以通过调用键对象的 interestOps( )方法来获取。最初,这应该是通道被注册时传进来的值。这个 interset 集合永远不会被选择器改变,用户可以通过调用 带参数的interestOps方法,并传入一个新的比特掩码参数来改变它。

使用选择器
public abstract class Selector{
    // This is a partial API listing
    public abstract Set keys( );
    public abstract Set selectedKeys( );
    public abstract int select( ) throws IOException;
    public abstract int select (long timeout) throws IOException;
    public abstract int selectNow( ) throws IOException;
    public abstract void wakeup( );
}
选择过程

在详细了解 API 之前,您需要知道一点和 Selector 内部工作原理相关的知识。就像上面探讨的那样,选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey 对象中的。每一个 Selector 对象维护三个键的集合:

  • 已注册的键的集合(Registered key set)
    与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys( )方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引 java.lang.UnsupportedOperationException。

  • 已选择的键的集合(Selected key set)
    已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的 interest 集合中的操作。这个集合通过 selectedKeys( )方法返回(并有可能是空的)。
    不要将已选择的键的集合与 ready 集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的 ready 集合,指示了所关联的通道已经准备好的操作。(readyOps,一个通道可能对多个操作感兴趣,ready的可能只是其中某个操作)。SelectionKey可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

  • 已取消的键的集合(Cancelled key set)
    已注册的键的集合的子集,这个集合包含了 cancel( )方法被调用过的键(这个键已经被无效 化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

public abstract class AbstractSelector
    extends Selector
{
...
private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();//取消的keys
...
}

public abstract class SelectorImpl extends AbstractSelector {
    protected Set<SelectionKey> selectedKeys = new HashSet();//选择的key
    protected HashSet<SelectionKey> keys = new HashSet();//注册的keys
.....
}

Selector 类的核心是选择过程。基本上来说,选择器是对 select( )、 poll( )等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是 Selector 所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。

Selector 类的 select( )方法有以下三种不同的形式:

public abstract int select( ) throws IOException;
public abstract int select (long timeout) throws IOException;
public abstract int selectNow( ) throws IOException;

这三种 select 的形式,仅仅在它们在所注册的通道当前都没有就绪时,是否阻塞的方面有所不同。最简单的没有参数的形式可以用如下方式调用:

  1. select(),这种调用在没有通道就绪时将无限阻塞。一旦至少有一个已注册的通道就绪,选择器的选择键就会被更新,并且每个就绪的通道的 ready 集合也将被更新。返回值将会是已经确定就绪的通道的数目。正常情况下, 这些方法将返回一个非零的值,因为直到一个通道就绪前它都会阻塞。但是它也可以返回非 0 值,如果选择器的 wakeup( )方法被其他线程调用。

  2. select (long timeout) :有时您会想要限制线程等待通道就绪的时间。这种情况下,可以使用一个接受一个超时参数的select( long timeout)方法的重载形式:这种调用与之前的例子完全相同,除了如果在您提供的超时时间(以毫秒计算)内没有通道就绪时,它将返回 0。如果一个或者多个通道在时间限制终止前就绪,键的状态将会被更新,并且方法会在那时立即返回。将超时参数指定为 0 表示将无限期等待,那么它就在各个方面都等同于使用无参数版本的 select( )了。

  3. selectNow():就绪选择的第三种也是最后一种形式是完全非阻塞的:selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。

停止选择过程

Selector 的 API 中的最后一个方法, wakeup( ),提供了使线程从被阻塞的 select( )方法中优雅地退出的能力:

public abstract void wakeup( );

有三种方式可以唤醒在 select( )方法中睡眠的线程:

  1. 调用 wakeup( )
    调用 Selector 对象的 wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对 select( )方法的一种形式的调用将立即返回。后续的选择操作将正常进行。

  2. 调用 close( )
    如果Selector的 close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像wakeup( )方法被调用了一样。与选择器相关的通道将被注销, 而键将被取消。

  3. 调用 interrupt( )
    如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。

猜你喜欢

转载自blog.csdn.net/u013857458/article/details/82427412