深入Java NIO:Buffer、Channel、Selector源码分析

150 阅读22分钟

概述

  1. I/O模型就是用什么样的通道或者通信方式进行数据的发送和接收,这很大程度上决定了程序通信的性能

  2. Java支持3种I/O模型:BIO、NIO、AIO

    • BIO(Blocking I/O):同步阻塞,一个线程专门处理一个连接
    • NIO(Non-blocking I/O):同步非阻塞,一个线程处理多个连接(请求),多个client注册到一个selector上,一个线程在一个selector上轮询是否有新的请求
    • AIO(Asynchronous I/O)(NIO.2):异步非阻塞,一个线程处理多个连接(请求),多个client注册到一个selector上,一个线程在一个selector上监听是否有新的请求,如果有则触发异步回调(OS已经为IO操作注册好回调函数了,上层应用程序只需要去使用即可)
  3. I/O多路复用是一种允许单个进程或线程监视多个I/O流的活动状态,并能在一个或多个流准备好进行读写时得到通知的技术

测试代码仓库:gitee.com/csuyth/io-t…

Java BIO

package java.io;

Java 1.0引入java.io包,并且在Java 1.1引入了Reader

  • InputStreamOutputStream:输出输出流的抽象基类
  • ReaderWriter:流包装类
  • 阻塞模式:总是等待完整的数据

Java NIO

概要介绍

package java.nio;

Java 1.4引入了java.nio包,并且在Java 1.7更新了NIO.2(增强文件操作、ASynchronousSocketChannel)

  • Buffer:原始类型的数据的容器
  • charset.CharsetDecodercharset.CharsetEncoder:字符集解码/编码器
  • channels.Channel:IO通道,代表与外界(硬件设备、文件、socket等)的连接
  • channels.SelectorSelectableChannel的多路复用器
  • 非阻塞模式:随时读取已经准备好的数据

NIO中Buffer、Channel、Selector关系图:

                                  Server
                                    |
                  +-----------------+-----------+-------------+
                  |                             |             |
                Thread                        Thread         ...
                  |                             |
               Selector                       Selector
                  |                             |
  +---------+-----+----+-------+        +-------+-------+
  |         |          |       |        |               |
Channel   Channel     ...   Channel   Channel          ...
  |         |                  |        |
Buffer    Buffer            Buffer    Buffer
  |         |                  |        |
Client    Client            Client    Client

Buffer

@see java.nio.Buffer

  • 定义:存储具体原始类型(比如:byte类型、int类型)的数据的容器

  • buffer存储的数据是线性的、有限的

  • buffer最关键的属性为:capacitylimitposition

    • capacity:容量,buffer存储的元素个数,一旦定义不可改变,capacity >= 0
    • limit:边界,第一个不该被读取或写入的元素的下标,0 <= limit <= capacity
    • position:当前位置,下一个该被读取或写入的元素的下标
  +-------------------+------------------+------------------+
  |     used bytes    |  remaining bytes |  out of bounds   |
  |                   |                  |                  |
  +-------------------+------------------+------------------+
  |                   |                  |                  |
  0        <=      position     <=     limit      <=     capacity
  • 传输数据:由子类定义的getput方法

    • 相对操作:如果下标越界,get操作抛出BufferUnderflowException异常,put操作抛出BufferOverflowException
    • 绝对操作:如果下标越界,抛出IndexOutOfBoundsException异常
  • mark属性和reset方法

    • 如果mark == -1,代表标记位未定义,使用未定义的mark,则抛出InvalidMarkException
    • 规定:0 <= mark <= position <= limit <= capacity
    • mark:标记,把mark标记为position的数值
    • reset:重置,把position重置为mark的数值
  • Clearing、 flipping、 rewinding

    • clear:清空数据,准备好接收新的数据,把limit设置为capacity,把position设置为0
    • flip:反转模式,准备好读取buffer数据,把limit设置为当前position,把position设置为0
    • rewind:倒回操作,准备好从头覆盖写数据,不改变limit,把position设置为0
  • compact:整理buffer,来更好的利用碎片空间,即利用[0, position - 1]之间的空间

    • [position, limit - 1]之间的数据拷贝到[0, limit - position - 1]
    • 把新position设置为limit - position,把新limit设置为capacity
  • 每个Buffer都是可读的,但并不是每个Buffer都是可写的

    • 操作只读Buffer,会抛出异常ReadOnlyBufferException
    • isReadOnly方法,返回当前Buffer是否只读
  • Buffer不是线程安全的

  • Buffer支持链式编程,例如:

    myBuffer.flip().position(23).limit(42);
    
  • 额外:

    • remaining:指的是positionlimit中间的空间
    • isDirect:分配的空间是不是直接内存
    • hasArray、array:背后是不是靠Java数组实现的(HeapBuffer)

DirectByteBuffer

@see java.nio.DirectByteBuffer

分配的内存空间是直接内存,也可以称为狭义上的堆外内存,通过unsafe实例调用native方法进行内存分配。

为什么要用直接内存呢?

通信过程中,如果使用堆内内存,那么需要把内存数据拷贝一份出去,再进行发送,所以直接使用堆外内存既提高了通信效率,又减少了GC负担。

  • 属性
protected static final Unsafe unsafe = Bits.unsafe();
​
/** byte数组的对象头所占的字节数 */
private static final long arrayBaseOffset = (long)unsafe.arrayBaseOffset(byte[].class);
​
/** 是否线性 */
protected static final boolean unaligned = Bits.unaligned();
​
/** 
 * 附属对象,用于GC时避免附属对象被清理
 * 执行duplicating, slicing这类派生操作时,源ByteBuffer即为att附属对象
 */
private final Object att;
​
/** 用于清理直接内存,避免内存泄露的 */
private final Cleaner cleaner;
  • 内部类Deallocator:内存清理者
private static class Deallocator implements Runnable {
    private static Unsafe unsafe = Unsafe.getUnsafe();
​
    private long address; // 基地址
    private long size;    // 真实分配的直接内存
    private int capacity; // 真实使用的直接内存
​
    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }
​
    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        unsafe.freeMemory(address); // 释放直接内存
        address = 0;
        Bits.unreserveMemory(size, capacity); // 更新直接内存的使用量统计,释放空间
    }
}
​
class Bits {
  ...
​
  static void unreserveMemory(long size, int cap) {
    long cnt = count.decrementAndGet();
    long reservedMem = reservedMemory.addAndGet(-size);
    long totalCap = totalCapacity.addAndGet(-cap);
    assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
  }
​
  ...
}
  • 构造器
DirectByteBuffer(int cap) {                   // package-private
​
    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); // 把数据初始化为0
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 创建内存cleaner,并添加到Cleaner链表里去
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}
​
class Bits {
  ...
  
  // These methods should be called whenever direct memory is allocated or
  // freed.  They allow the user to control the amount of direct memory
  // which a process may access.  All sizes are specified in bytes.
  static void reserveMemory(long size, int cap) {
​
    if (!memoryLimitSet && VM.isBooted()) {
      maxMemory = VM.maxDirectMemory();
      memoryLimitSet = true;
    }
​
    // optimist!
    // 直接尝试分配空间
    if (tryReserveMemory(size, cap)) {
      return;
    }
​
    final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
​
    // retry while helping enqueue pending Reference objects
    // which includes executing pending Cleaner(s) which includes
    // Cleaner(s) that free direct buffer memory
    // 尝试清理DirectByteBuffer对应的堆外内存
    while (jlra.tryHandlePendingReference()) {
      // 清理成功,则再尝试分配
      if (tryReserveMemory(size, cap)) {
        return;
      }
    }
​
    // trigger VM's Reference processing
    // 触发GC,清理堆内的DirectByteBuffer
    System.gc();
​
    // a retry loop with exponential back-off delays
    // (this gives VM some time to do it's job)
    boolean interrupted = false;
    try {
      long sleepTime = 1;
      int sleeps = 0;
      while (true) {
        // 等待守护线程清理直接内存,循环尝试分配内存
        if (tryReserveMemory(size, cap)) {
          return;
        }
        if (sleeps >= MAX_SLEEPS) {
          break;
        }
        if (!jlra.tryHandlePendingReference()) {
          try {
            Thread.sleep(sleepTime);
            sleepTime <<= 1;
            sleeps++;
          } catch (InterruptedException e) {
            interrupted = true;
          }
        }
      }
​
      // no luck
      // 实在是没有空间来分配了,抛出异常
      throw new OutOfMemoryError("Direct buffer memory");
​
    } finally {
      if (interrupted) {
        // don't swallow interrupts
        Thread.currentThread().interrupt();
      }
    }
  }
  
  ...
}
  • 直接内存清理机制

    • 依赖于Cleaner,把堆外内存的清理与堆内的VM GC关联起来。
    • PhantomReference(虚引用)只能用于跟踪对象是何时被回收的。
    • 但是如果gc过程中发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那么VM会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理。
    • DirectByteBuffer关联的Cleaner是PhantomReference的一个子类,在最终的处理时,会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。
    • 由于young gc不会清理老年代对象,所以如果一直没有做full gc,那么老年代的DirectByteBuffer(冰山对象 Iceberg Object)背后关联的大量直接内存将无法释放,偷偷耗尽我们的物理内存。
public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;
​
    private static synchronized Cleaner add(Cleaner var0) {
        ...
    }
    private static synchronized boolean remove(Cleaner var0) {
        ...
    }
    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }
​
    public static Cleaner create(Object var0, Runnable var1) {
        // var0是directByteBuffer对象,var1是Deallocator对象
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }
​
    public void clean() {
        if (remove(this)) { // 从链表中移除cleaner
            try {
                this.thunk.run(); // 执行清理逻辑
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }
​
                        System.exit(1);
                        return null;
                    }
                });
            }
​
        }
    }
}
public abstract class Reference<T> {
​
    ...
​
    /* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */
    transient private Reference<T> discovered;  /* used by VM */
​
    ...
​
    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;  // JVM会把引用放入这个队列
​
    /* High-priority thread to enqueue pending References
     */
    private static class ReferenceHandler extends Thread {
​
        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }
​
        static {
            // pre-load and initialize InterruptedException and Cleaner classes
            // so that we don't get into trouble later in the run loop if there's
            // memory shortage while loading/initializing them lazily.
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }
​
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }
​
        public void run() {
            // 循环尝试处理cleaner,清理直接内存
            while (true) {
                tryHandlePending(true);
            }
        }
    }
​
    /**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    // 从链表中取出cleaner
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();  // 等待JVM来唤醒自己
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }
​
        // Fast path for cleaners
        if (c != null) {
            c.clean();  // 清理直接内存
            return true;
        }
​
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }
​
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 创建一个守护线程handler,处理cleaner
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
​
        // 给SharedSecrets提供主动尝试清理的方法
        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                // 返回true,则说明成功释放了部分直接内存
                return tryHandlePending(false);
            }
        });
    }
​
    ...
​
}
  • get、put:逻辑很简单,基地址寻址操作
private long ix(int i) {
    return address + ((long)i << 0);
}
public byte get() {
    return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {
    return ((unsafe.getByte(ix(checkIndex(i)))));
}
public ByteBuffer put(byte x) {
    unsafe.putByte(ix(nextPutIndex()), ((x)));
    return this;
}
public ByteBuffer put(int i, byte x) {
    unsafe.putByte(ix(checkIndex(i)), ((x)));
    return this;
}

Channel

@see java.nio.channels.Channel

/**
 * Channel通道,代表连接到一个实体(比如:硬件设备、文件、网络套接字)的连接。
 * 能够支持一个或多个I/O操作(比如:读、写)。
 * 通道要么是关闭的,要么是开启的。
 * 创建通道时,通道即为开启状态,一旦通道被关闭,则永远维持关闭状态。
 * 一般来说,Channel需要是线程安全的。
 */
public interface Channel extends Closeable {
​
    /**
     * Tells whether or not this channel is open.
     */
    public boolean isOpen();
​
    /**
     * Closes this channel.
     */
    public void close() throws IOException;
}

常见的Channel子类:

  • FileChannel:文件操作
  • DatagramChannel:UDP/IP
  • ServerSocketChannel:B/S、TCP/IP
  • SocketChannel:C/S、TCP/IP

相关比较基本的子接口:

Jdk中接口是怎么定义的,怎么划分层次的,值得学习参考。

/** A channel that can write bytes. */
public interface WritableByteChannel extends Channel {
    public int write(ByteBuffer src) throws IOException;
}
​
/** A channel that can read bytes. */
public interface ReadableByteChannel extends Channel {
    public int read(ByteBuffer dst) throws IOException;
}
​
/** A channel that can read and write bytes. */
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
}
​
/** A channel that can read bytes into a sequence of buffers. */
public interface ScatteringByteChannel extends ReadableByteChannel {
    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
    public long read(ByteBuffer[] dsts) throws IOException;
}
​
/** A channel that can write bytes from a sequence of buffers. */
public interface GatheringByteChannel extends WritableByteChannel {
    public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
    public long write(ByteBuffer[] srcs) throws IOException;
}

SeekableByteChannel

@see java.nio.channels.SeekableByteChannel

  • FileChannel:继承InterruptibleChannel的骨架实现类,实现SeekableByteChannel接口
/** A channel for reading, writing, mapping, and manipulating a file. */
public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
  ...
}
  • InterruptibleChannel:作为标记接口(Marker Interface),对channel的方法重新进行了规定,本身没有新增方法
/** A channel that can be asynchronously closed and interrupted. */
public interface InterruptibleChannel extends Channel {
​
    /**
     * Closes this channel.
     * 当前在该通道上的I/O操作中阻塞的任何线程都将收到一个AsynchronousCloseException。
     */
    @Override
    public void close() throws IOException;
}
​
/** InterruptibleChannel的基本骨架实现 */
public abstract class AbstractInterruptibleChannel
    implements Channel, InterruptibleChannel {
  ... // 省略
}
  • SeekableByteChannel:基本上来说,这个接口是专门为了文件通道操作而生的
/** A byte channel that maintains a current position and allows the position to be changed. */
public interface SeekableByteChannel extends ByteChannel {
​
    @Override
    int read(ByteBuffer dst) throws IOException;
    @Override
    int write(ByteBuffer src) throws IOException;
​
    /**
     * Returns this channel's position.
     */
    long position() throws IOException;
​
    /**
     * Sets this channel's position.
     */
    SeekableByteChannel position(long newPosition) throws IOException;
​
    /**
     * Returns the current size of entity to which this channel is connected.
     */
    long size() throws IOException;
​
    /**
     * Truncates the entity, to which this channel is connected, to the given
     * size.
     */
    SeekableByteChannel truncate(long size) throws IOException;
}

怎么理解SeekableByteChannelposition

通常,position是指从文件开头到当前读取或写入位置的字节数。

  1. 当你打开一个SeekableByteChannel通道时,初始位置通常会被设置为0(从文件开头)。
  2. 当你进行读取或写入操作时,position()方法会返回当前的读写位置。
  3. 每次读取或写入操作完成后,position()方法会更新当前位置,指向已读取或已写入数据之后的位置。

FileChannel

@see java.nio.channels.FileChannel

  • Zero-copy

在Java中,零拷贝技术(zero-copy)通常与网络IO和文件系统IO相关。它是一种减少或消除CPU参与数据传输过程的技术,允许操作系统直接将数据从文件系统缓存传输到网络堆栈,而不需要CPU将数据从一个内存区域复制到另一个内存区域。

FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
FileChannel targetChannel = new FileOutputStream("target.txt").getChannel();
​
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
// or
targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
​
sourceChannel.close();
targetChannel.close();
  • Memory-mapped files

Java NIO的 FileChannel 还可以创建内存映射文件。当文件内容被映射到内存时,应用程序可以直接在内存中操作这些内容,操作系统会负责将修改后的内容写回到文件中。这个过程中减少了数据在用户空间和内核空间之间的拷贝。

FileChannel fileChannel = new RandomAccessFile("largeFile.txt", "rw").getChannel();
​
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
​
// 在这个buffer上进行数据操作,这些操作将直接反映到文件上
buffer.put(0, (byte) 97); // example of write operation
​
fileChannel.close();

SelectableChannel

@see java.nio.channels.SelectableChannel

SelectableChannel

  • SelectableChannel是一个通过Selector进行多路复用的Channel。

  • 需要被selector使用,首先需要这个channel通过register方法来进行注册,这个register方法会返回一个SelectionKey代表注册证。

  • 一旦注册了,就将保持注册状态,直到取消注册diregistered,取消注册意味着释放所有由selector分配的资源。

  • channel不能直接被取消注册,注册证selectionKey需要调用cannel方法后,channel才可以取消注册。cancel一个SelectionKey的条件是:对应的channel在selector下一次选择操作之前进行取消注册。当channel执行了close操作,则这个channel全部的selectionKey都绝对会cannel。

  • SelectableChannel最多只能注册一个特定的selector

  • SelectableChannel是线程安全的

  • SelectableChannel要么处于“阻塞(blocking)”模式,要么处于“非阻塞(non-blocking)”模式

    • blocking:任何I/O操作都将阻塞直到被完成
    • non-blocking:任何I/O操作都不会阻塞,但是可能只能传输比请求更少的字节数,甚至0个字节数
  • 新创建的SelectableChannel总是处于blocking模式。non-blocking模式在与selector协调工作时发挥最佳效果,SelectableChannel必须在注册到一个selector之前被设置为non-blocking模式,并且在取消注册之前不能回退到blocking模式。

@see java.nio.channels.SelectionKey             // 代表channel注册到selector到注册证
@see java.nio.channels.spi.AbstractSelectionKey // 骨架实现基类
@see java.nio.channels.spi.SelectorProvider     // 工厂基类,提供构造channel对象的方法
@see java.nio.channels.spi.AbstractSelectableChannel  // 骨架实现基类
/**
 * A channel that can be multiplexed via a {@link Selector}.
 */
public abstract class SelectableChannel
    extends AbstractInterruptibleChannel
    implements Channel {
​
    protected SelectableChannel() { }
​
    /**
     * @return  The provider that created this channel
     */
    public abstract SelectorProvider provider();
​
    /**
     * 一个具体实现类的这个方法总是返回相同的数值。
     * 操作掩码见SelectionKey的静态变量,有四种:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT
     *
     * @return  合法操作合集
     */
    public abstract int validOps();
​
    // 下面的变量定义在java.nio.channels.spi.AbstractSelectableChannel中
    // Internal state:
    //   keySet, 一个数值实现的set,存放SelectionKey
    //   boolean isRegistered, protected by key set
    //   regLock, 一个锁,用于防止重复注册
    //   boolean isBlocking, protected by regLock
​
    /**
     * Tells whether or not this channel is currently registered with any
     * selectors.  A newly-created channel is not registered.
     *
     * <p> Due to the inherent delay between key cancellation and channel
     * deregistration, a channel may remain registered for some time after all
     * of its keys have been cancelled.  A channel may also remain registered
     * for some time after it is closed.  </p>
     *
     * @return <tt>true</tt> if, and only if, this channel is registered
     */
    public abstract boolean isRegistered();
    //
    // sync(keySet) { return isRegistered; }
​
    /**
     * Retrieves the key representing the channel's registration with the given
     * selector.
     *
     * @param   sel
     *          The selector
     *
     * @return  The key returned when this channel was last registered with the
     *          given selector, or <tt>null</tt> if this channel is not
     *          currently registered with that selector
     */
    public abstract SelectionKey keyFor(Selector sel);
    //
    // sync(keySet) { return findKey(sel); }
​
    /**
     * 用selecrot注册channel,返回表示注册关系的selectionKey
     */
    public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;
    //
    // sync(regLock) {
    //   sync(keySet) { look for selector }
    //   if (channel found) { set interest ops -- may block in selector;
    //                        return key; }
    //   create new key -- may block somewhere in selector;
    //   sync(keySet) { add key; }
    //   attach(attachment);
    //   return key;
    // }/** 重载 */
    public final SelectionKey register(Selector sel, int ops)
        throws ClosedChannelException
    {
        return register(sel, ops, null);
    }
​
    /**
     * 修改channel的阻塞模式
     */
    public abstract SelectableChannel configureBlocking(boolean block)
        throws IOException;
    //
    // sync(regLock) {
    //   sync(keySet) { throw IBME if block && isRegistered; }
    //   change mode;
    // }
​
    /**
     * @return <tt>true</tt> if, and only if, this channel is in blocking mode
     */
    public abstract boolean isBlocking();
​
    /**
     * @return  The blocking-mode lock object,即为regLock
     */
    public abstract Object blockingLock();
​
}

java.nio.channels.SelectionKey

  • 代表一个SelectableChannel注册到一个Selector到注册证。

  • 当channel注册到selector时,就会创建一个selectionKey,这个selectionKey维持有效(valid)状态直到被取消(cancel)。

  • 当客户端直接调用cancel方法时、当关联的channel进行close时、当关联的selecor进行close时,selectionKey转变为无效状态。

  • 一个selectionKey包含2种操作集,分别是interestOpsreadyOps,两种都分别通过一个整数来表示。

    • interestOps:哪些操作是感兴趣的,随时可能随着interestOps(int)方法的调用而更新
    • readyOps:哪些操作是准备就绪的,一般来说是interestOps的子集,一般在selector执行select时更新。readyOps作为一种客户端使用提示,但不能保证总是准确的,一般来说紧跟在select操作后面查询readOps则会得到准确的结果,外部事件I/O操作可能会让readyOps不准确。
  • 四种操作:

    • SelectionKey.OP_ACCEPT:通道接受连接的操作。
    • SelectionKey.OP_CONNECT:通道连接完成的操作。
    • SelectionKey.OP_READ:通道可读的操作。
    • SelectionKey.OP_WRITE:通道可写的操作。
  • selectionKey是线程安全的

/**
 * A token representing the registration of a SelectableChannel with a Selector.
 */
public abstract class SelectionKey {
    protected SelectionKey() { }
​
    // -- Channel and selector operations --
​
    /**
     * @return  This key's channel
     */
    public abstract SelectableChannel channel();
​
    /**
     * @return  This key's selector
     */
    public abstract Selector selector();
​
    /**
     * @return  如果selectionKey有效则返回true
     */
    public abstract boolean isValid();
​
    /**
     * 取消注册,永久进入invalid状态
     * 这个方法需要同步来保证线程安全,并发时可能短暂阻塞
     */
    public abstract void cancel();
​
​
    // -- Operation-set accessors --
​
    /**
     * @return  This key's interest set
     */
    public abstract int interestOps();
​
    /**
     * 设置感兴趣的操作集合
     */
    public abstract SelectionKey interestOps(int ops);
​
    /**
     * @return  This key's ready-operation set
     */
    public abstract int readyOps();
​
​
    // -- Operation bits and bit-testing convenience methods --
​
    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;
​
    public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }
​
    public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }
​
    public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }
​
    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }
​
​
    // -- Attachments --
    // selectionKey可以携带一个附件对象
​
    private volatile Object attachment = null;
​
    private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
        attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
            SelectionKey.class, Object.class, "attachment"
        );
​
    public final Object attach(Object ob) {
        return attachmentUpdater.getAndSet(this, ob);
    }
​
    public final Object attachment() {
        return attachment;
    }
}

SocketChannel、ServerSocketChannel、DatagramChannel

/** A selectable channel for stream-oriented connecting sockets. */
public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel {
  ...
}
​
/** A selectable channel for stream-oriented listening sockets. */
public abstract class ServerSocketChannel
    extends AbstractSelectableChannel
    implements NetworkChannel {
  ...
}
​
/** A selectable channel for datagram-oriented sockets. */
public abstract class DatagramChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {
  ...
}

Selector

@see java.nio.channels.Selector

  • 定义:给SelectableChannel实例使用的多路复用器

  • 创建selector实例:

    • 调用Selector.open()静态方法,通过系统默认的provider创建
    • 调用特定的provideropenSelector()方法创建
  • selector将维持“开启”状态直到调用close()方法“关闭”

  • 一个selector中包含3个不同的selectionKey集合:

    • key set:这个集合包括了当前所有在该selector上注册的channel对应的SelectionKey。你可以通过调用keys()方法来访问这个集合。
    • selected-key set:每当selector执行select()操作时,所有检测到已经准备就绪至少一个interesed操作的channel的SelectionKey都会被收集到这个集合中。这个集合可以通过selectedKeys()方法获取。你可以使用remove方法来从集合中移除特定的key。如果在处理过程中一个key被取消了,那么在随后的select操作期间,它会自动从集合中被清除。这个集合始终是key set的一个子集。
    • cancelled-key set:这个集合包含了所有已经被取消但是其关联channel尚未取消注册(deregistered)的SelectionKey。这些已取消的key将在下一次select操作期间被selector自动清除。这个集合也始终是key set的一个子集。
  • 可以通过select()select(long)selectNow()执行selection operation,其执行过程包含三个阶段:

    1. cancelled-key set中的全部key都从3个集合中删除(意味着cancelled-key set清空),并且把这些key关联的channel取消注册
    2. 查询底层操作系统,更新每个剩余的channel准备就绪的操作集合(readyOps,interestOps的子集)。对于这些更新后的channel,如果channel的key不在selected-key set中,则加入这个set。
    3. 在步骤2期间加入cancelled-key set中的key会像步骤1一样,从3个集合中删除并把关联channel取消注册。
  • selectNow()select(long)select()的区别与联系:

    • selectNow():非阻塞,执行select操作,并立即返回结果
    • select(long):阻塞,允许阻塞至多一段时间(除非中断或被唤醒)来等待channel就绪
    • select():阻塞,等同于select(0),允许永久阻塞(除非中断或被唤醒)来等待channel就绪
    • wakeup():唤醒selector,取消阻塞状态,让select(long)select()立即返回结果
  • 并发:selector自身是线程安全的,但是它的3个key集合并不是,如果在并发场景下修改key集合,则可能抛出java.util.ConcurrentModificationException。由于selectionKey和channel可能在任何时间关闭,所以应用代码需要谨慎使用同步机制来检查key是否valid,channel是否open。

/**
 * A multiplexor of {@link SelectableChannel} objects.
 *
 * @see SelectableChannel
 * @see SelectionKey
 */
public abstract class Selector implements Closeable {
​
    protected Selector() { }
​
    /**
     * Opens a selector.
     *
     * <p> The new selector is created by invoking the {@link
     * java.nio.channels.spi.SelectorProvider#openSelector openSelector} method
     * of the system-wide default {@link
     * java.nio.channels.spi.SelectorProvider} object.  </p>
     *
     * @return  A new selector
     */
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
​
    /**
     * @return 如果selector处于开启状态,则返回true
     */
    public abstract boolean isOpen();
​
    /**
     * @return  The provider that created this channel
     */
    public abstract SelectorProvider provider();
​
    /**
     * key set 不能被客户端修改元素,否则抛出异常
     * key set 不是线程安全的
     *
     * @return  This selector's key set
     */
    public abstract Set<SelectionKey> keys();
​
    /**
     * selected-key set 可以被客户端删除元素,但不能直接新增
     * selected-key set 不是线程安全的
     *
     * @return  This selector's selected-key set
     */
    public abstract Set<SelectionKey> selectedKeys();
​
    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
     * <p> This method performs a non-blocking <a href="#selop">selection
     * operation</a>.  If no channels have become selectable since the previous
     * selection operation then this method immediately returns zero.
     *
     * <p> Invoking this method clears the effect of any previous invocations
     * of the {@link #wakeup wakeup} method.  </p>
     *
     * @return  在本次select操作中,更新了“准备就绪操作集合”的key的数量
     */
    public abstract int selectNow() throws IOException;
​
    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
     * This method performs a blocking selection operation.
     * It returns only after at least one channel is selected,
     * this selector's {@link #wakeup wakeup} method is invoked, the current
     * thread is interrupted, or the given timeout period expires, whichever
     * comes first.
     *
     * <p> This method does not offer real-time guarantees: It schedules the
     * timeout as if by invoking the {@link Object#wait(long)} method. </p>
     *
     * @param  timeout  如果timeout > 0, 最多阻塞${timeout}毫秒来等待有至少一个channel就绪;
     *                  如果timeout == 0, 允许永久阻塞,来等待至少一个channel就绪;
     *                  如果timeout < 0,抛出非法参数异常
     *
     * @return  在本次select操作中,更新了“准备就绪操作集合”的key的数量
     */
    public abstract int select(long timeout)
        throws IOException;
​
    /**
     * Selects a set of keys whose corresponding channels are ready for I/O
     * operations.
     *
     * This method performs a blocking selection operation.
     * It returns only after at least one channel is selected,
     * this selector's {@link #wakeup wakeup} method is invoked, or the current
     * thread is interrupted, whichever comes first.  </p>
     *
     * @return  在本次select操作中,更新了“准备就绪操作集合”的key的数量
     */
    public abstract int select() throws IOException;
​
    /**
     * Causes the first selection operation that has not yet returned to return
     * immediately.
     *
     * <p> If another thread is currently blocked in an invocation of the
     * {@link #select()} or {@link #select(long)} methods then that invocation
     * will return immediately.  If no selection operation is currently in
     * progress then the next invocation of one of these methods will return
     * immediately unless the {@link #selectNow()} method is invoked in the
     * meantime.  In any case the value returned by that invocation may be
     * non-zero.  Subsequent invocations of the {@link #select()} or {@link
     * #select(long)} methods will block as usual unless this method is invoked
     * again in the meantime.
     *
     * <p> Invoking this method more than once between two successive selection
     * operations has the same effect as invoking it just once.  </p>
     *
     * @return  This selector
     */
    public abstract Selector wakeup();
​
    /**
     * Closes this selector.
     *
     * <p> If a thread is currently blocked in one of this selector's selection
     * methods then it is interrupted as if by invoking the selector's {@link
     * #wakeup wakeup} method.
     *
     * <p> Any uncancelled keys still associated with this selector are
     * invalidated, their channels are deregistered, and any other resources
     * associated with this selector are released.
     *
     * <p> If this selector is already closed then invoking this method has no
     * effect.
     *
     * <p> After a selector is closed, any further attempt to use it, except by
     * invoking this method or the {@link #wakeup wakeup} method, will cause a
     * {@link ClosedSelectorException} to be thrown. </p>
     *
     * @throws  IOException
     *          If an I/O error occurs
     */
    public abstract void close() throws IOException;
​
}

JavaNIO编程示例

package com.yth.nio;
​
import org.junit.Assert;
import org.junit.Test;
​
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
​
/**
 * {@link Selector}, {@link SelectionKey}, {@link ServerSocketChannel}
 *
 * @author yutianhong
 * @version 1.0
 * @since 2023/11/23 19:53
 */
public class ServerSocketTest {
​
    /**
     * 服务器的ip
     */
    private final String HOST = "127.0.0.1";
​
    /**
     * 服务器端口
     */
    private final int PORT = 8080;
​
    @Test
    public void testServer() throws IOException {
​
        // 创建Selector
        Selector selector = Selector.open();
​
        // 创建ServerSocketChannel,并绑定监听端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
        serverSocketChannel.configureBlocking(false);
​
        // 将Channel注册到Selector上,并指定监听事件为“接收”事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
​
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
​
        Charset charset = StandardCharsets.UTF_8;
        CharsetDecoder charsetDecoder = charset.newDecoder();
        CharBuffer charBuffer = CharBuffer.allocate(1024);
​
        while (true) {
            // 阻塞等待就绪的Channel
            if (selector.select() == 0) {
                continue;
            }
​
            // 获取就绪的Channel集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
​
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 移除Set中的当前key
                iterator.remove();
​
                // 根据就绪状态,调用对应方法处理业务逻辑
​
                // 如果是“接收”事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    // 接收客户端的连接
                    SocketChannel clientChannel = server.accept();
                    clientChannel.configureBlocking(false);
                    // 注册到Selector,等待连接
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted connection from " + clientChannel);
                }
​
                // 如果是“读取”事件
                if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    buffer.clear();
                    // 读取数据
                    StringBuilder stringBuilder = new StringBuilder();
                    while (clientChannel.read(buffer) != -1 || buffer.position() != 0) {
                        buffer.flip();
                        charsetDecoder.decode(buffer, charBuffer, true);
                        charBuffer.flip();
                        stringBuilder.append(charBuffer);
                        charBuffer.clear();
                        buffer.compact();
                    }
​
                    String message = stringBuilder.toString();
                    System.out.println("Received message from " + clientChannel + ": " + message);
                    // 回写数据
                    clientChannel.write(charset.encode("Echo: " + message));
​
                    // 客户端断开连接
                    clientChannel.close();
                }
            }
        }
    }
​
​
    @Test
    public void testClient() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
        Charset charset = StandardCharsets.UTF_8;
        CharsetDecoder charsetDecoder = charset.newDecoder();
​
        // 连接服务器
        try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT))) {
            // send message to server
            byteBuffer.put(charset.encode("hello server"));
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            socketChannel.shutdownOutput();
            byteBuffer.clear();
​
            // receive response from server
            StringBuilder stringBuilder = new StringBuilder();
            while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() != 0) {
                byteBuffer.flip();
                // decode and store
                CharBuffer decode = charsetDecoder.decode(byteBuffer);
                stringBuilder.append(decode);
                byteBuffer.compact();
            }
            System.out.println("Received message from server " + socketChannel + ": " + stringBuilder);
        } catch (IOException e) {
            Assert.fail();
        }
    }
​
}
​