写在前面
Channel 简介
channel 是连接两个互通的 I/O 实例通道的抽象 , 两个 I/O 实例发出的数据在 channel 中传输。I/O 实例可能是硬盘,内存 , 网络设备等。
java nio channel 结构简图 (当中只选取了部分我自己认为比较关键的) :

AutoCloseable 、 Closeable 接口定义了 close 方法来关闭释放打开的资源。Channel 接口是一个高度抽象只定义了检测 channel 是否处于打开状态的方法。WritableByteChannel 和 ReadableByteChannel 分别定义了写入和读取的方法。而 ByteChannel 什么都没做只是继承了 WritableByteChannel 和 ReadableByteChannel ,任何实现了 ByteChannel 接口的 channel 都具备了读、写的能力。GatheringByteChannel 可以将多个 Buffer 中的数据写入 channel 中 , ScatteringByteChannel 可以将 channel 中的数据分片读取到多个 Buffer 中。FileChannel 和文件系统相关的 channel , 用来操作文件 I/O 。NetworkChannel 网络 I/O channel 。 SelectableChannel 基于多路复用 I/O 模型的 channel 抽象 。ServerSocketChannel , SocketChannel 基于 TCP 协议的网络 channel 。MulticastChannel 基于UDP协议的网络组播 channel。 DatagramChannel 基于 UDP 协议的网络 channel 。AsynchronousChannel 异步 I/O 模型 channel 。AsynchronousFileChannel 异步的文件系统 I/O channel 。 AsynchronousServerSocketChannel , AsynchronousSocketChannel 基于TCP协议的异步网络 I/O channel 。个人感觉 java channel 接口的粒度还是设计的比较细的。
从 FileChannel 入门
FileChannel 是专门针对文件系统 I/O 的 channel , FileChannel 总是阻塞式的 I/O 。FileChannel APIs 简介 :
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
protected FileChannel() { }
// 打开一个文件 channel , 根据 path 指定的文件路径打开该文件 channel , attrs 自定义的文件属性 , options 该文件 channel 支持的操作
public static FileChannel open(Path path,
Set<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException;
// 打开一个文件 channel , 根据 path 指定的文件路径打开该文件 channel , options 该文件 channel 支持的操作
public static FileChannel open(Path path, OpenOption... options) throws IOException;
// 将 channel 中的数据读取到 Buffer 中 , 返回读取的字节个数。
public abstract int read(ByteBuffer dst) throws IOException;
/**
* 将 channel 按照 Buffer 数组的顺序将数据读入到 Buffer 中 , 从 channel 的当前位置开始读取 , 返回读取的字节个数。
* offset : Buffer 数组中第一个元素的偏移量。
* length : Buffer 数组中用来接收 channel 中数据的最大 Buffer 数目 , 比如 Buffer 数组中有 3 个 Buffer length
* 为 2 , 那么 channel 中的数据只会被读取到 Buffer 数组中的第 0 , 1 位置的 Buffer 中。
*/
public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
// 从 channel 的当前位置将数据读取到 Buffer 数组中 , 返回读取的字节个数。
public final long read(ByteBuffer[] dsts) throws IOException;
// 将 Buffer 中的数据写入到 channel 中 , 从 channel 的当前位置开始写入 , 返回写入的字节个数。
public abstract int write(ByteBuffer src) throws IOException;
/**
* 将一组 Buffer 中的数据写入到 channel 中 , 从 channel 的当前位置开始写入 , 返回写入的字节个数。
* offset : Buffer 数组中第一个 Buffer 的偏移量。
* length : Buffer 数组中写入 channel 中的最大 Buffer 个数。
*/
public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
// 将一组 Buffer 中的数据写入到 channel 中 , 从 channel 的当前位置开始写入 , 返回写入的字节个数。
public final long write(ByteBuffer[] srcs) throws IOException;
// 获取到文件 channel 的当前位置 。
public abstract long position() throws IOException;
// 为 channel 设置一个新的当前位置。
public abstract FileChannel position(long newPosition) throws IOException;
// 获取文件的大小 , 单位是字节
public abstract long size() throws IOException;
// 将 channel 连接的文件大小截断为给定的长度 , 从文件开头开始计算。
public abstract FileChannel truncate(long size) throws IOException;
// 强制将此通道文件的任何更新写入包含该通道的存储设备
public abstract void force(boolean metaData) throws IOException;
// 将 channel 中的数据从指定的位置开始 , 传输指定个数的字节数 , 到另一个可写的 channel 中。
public abstract long transferTo(long position, long count,WritableByteChannel target) throws IOException;
/**
* 将一个可读 channel 中的数据传输到当前调用的 channel 中 。
* src :可读的源 channel 。
* position : 当前 channel 的位置 。
* count : 从源 channel 中传输的总字节数。
*/
public abstract long transferFrom(ReadableByteChannel src,long position, long count) throws IOException;
// 从 channel 中指定的位置开始 , 从 channel 中读取数据到 Buffer 中 , 返回读取的字节数。
public abstract int read(ByteBuffer dst, long position) throws IOException;
// 从 channel 中指定的位置开始 , 将 Buffer 中的数据写入 channel , 返回写入的字节数。
public abstract int write(ByteBuffer src, long position) throws IOException;
/**
* 从指定位置开始 , 获取一个指定大小的文件内存映射。
* mode : 内存映射的模式 , READ_ONLY , READ_WRITE ,PRIVATE
* position : channel 中的位置。
* size : channel 中要映射到内存中数据的大小。
*/
public abstract MappedByteBuffer map(MapMode mode,long position, long size) throws IOException;
/**
* 获取 channel 文件的给定区域的锁定。 如果锁定区域已经被锁定,而且获取的是一个排它锁,方法会阻塞,直到锁被释放。
* position : channel 中要锁定区域的起始位置。
* size : 锁定的区域大小 , 单位字节 。
* shared : 是否是共享锁 , true - 是共享锁 , false - 独占锁 。
*/
public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
// 获取 channel 文件的排它锁。如果该文件已经被锁定, 这个方法会阻塞。
public final FileLock lock() throws IOException;
/**
* 尝试获取 channel 文件的给定区域的锁定。 如果锁定区域已经被锁定,这个方法不会被锁定,而是返回 null 。
* position : channel 中要锁定区域的起始位置。
* size : 锁定的区域大小 , 单位字节 。
* shared : 是否是共享锁 , true - 是共享锁 , false - 独占锁 。
*/
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
// 获取 channel 文件的排它锁。如果该文件已经被锁定, 这个方法不会被锁定,而是返回 null 。
public final FileLock tryLock() throws IOException;
}
OpenOption :
OpenOption 定义了文件将被如何开打或者是创建。StandardOpenOption 定义了一些标准的操作类型:
public enum StandardOpenOption implements OpenOption {
// 文件是可读的
READ,
// 文件是可写的
WRITE,
// 写入时从文件末尾开始添加
APPEND,
// 如果文件已经存在并且是以 WRITE 方式打开 , 会将该文件内容清空到 0 字节大小 。
// 如果文件是以 READ 方式打开则不会清空文件 。
TRUNCATE_EXISTING,
// 如果文件不存在就创建一个文件。
CREATE,
// 创建一个新的文件,如果文件已经存在则创建失败。
CREATE_NEW,
// channel 关闭时删除文件。
DELETE_ON_CLOSE,
/**
* 稀疏文件,当与CREATE_NEW选项一起使用时,此选项提供了一个提示 ,新文件将是稀疏的。
* 当文件系统不支持创建稀疏文件时,该选项将被忽略。
*
* 稀疏文件就是在文件中留有很多空余空间,留备将来插入数据使用。
* 如果这些空余空间被ASCII码的NULL字符占据,并且这些空间相当大。
* 那么,这个文件就被称为稀疏文件,而且,并不分配相应的磁盘块。
*/
SPARSE,
// 要求将文件内容或元数据的每一次更新同步地写入底层存储设备。
SYNC,
// 要求将文件内容的每次更新同步地写入底层存储设备。
DSYNC;
}
FileChannel 的读、写操作是很简单的只是有一些需要注意的细节, 在进行读、写操作之前不要忘记对 Buffer 进行 flip() 。
文件锁定 :
锁可以是共享锁或者是独占锁 , 如果要获取共享锁文件需要是可读的 , 如果要获取独占锁文件需要是可写的。在 JDK1.4 之前是不支持文件锁定的,但是绝大多数的现代操作系统是早就支持文件锁定的。文件锁定的特性在很大程度上依赖本地操作系统的实现,并不是所有的操作系统都支持共享的文件锁。对于不支持共享文件锁的操作系统,对一个共享锁的请求会被自动提升为对独占锁的请求。锁定对多个 JVM 实例的访问,以及同一 JVM 中不同线程的访问都是有效的。这一点与 《Java NIO》 一书中描述的相反 , 《Java NIO》 中的描述是 : “锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。” 。实际上我在测试过程中发现 , 同一 JVM 内如果一个线程已经锁定了文件 , 在锁没有释放前另一个线程尝试锁定该文件会抛出 OverlappingFileLockException , 不同 JVM 中当有一个 JVM 中已经锁定了该文件,另一个 JVM 中试图获取该文件锁定则会阻塞,但不会抛出异常。锁的释放需要调用 FileLock 的 release() 方法 , 或者是 channel 关闭 , JVM 关闭时文件锁都会释放掉。 文件锁定测试代码 :
// 不同 JVM 中文件锁定测试
@Test
public void jvm1LockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.CREATE_NEW , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
// 尝试获取一个排他锁
FileLock fileLock = fChannel.tryLock();
// FileLock fileLock = fChannel.tryLock(0 , fChannel.size() , true);
try {
if (! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
String s0 = "一声梧叶一声秋,一点芭蕉一点愁,三更归梦三更后。落灯花,棋未收,叹新丰逆旅淹留。枕上十年事,江南二老忧,都到心头。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
Thread.sleep(100000);
} finally {
fileLock.release();
fChannel.close();
}
}
// 不同 JVM 中文件锁定测试
@Test
public void jvm2LockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
FileLock fileLock = fChannel.lock();
try {
if (Objects.isNull(fileLock) || ! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
String s0 = "@@@@@@@@@@@@@@@@@@@@@@@<<<<M><><><><><><><>>>>>>>>>>>";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
} finally {
if (Objects.nonNull(fileLock)) {
fileLock.release();
}
fChannel.close();
}
}
// 同一 JVM 中多线程文件锁定测试
@Test
public void MultiThreadingLockTest() throws Exception {
Path path = Paths.get("D:\\Documents\\Pictures\\test\\lock test.txt");
new Thread(() -> {
FileChannel fChannel = null;
FileLock fileLock = null;
try {
fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
// 获取一个排他锁
fileLock = fChannel.lock();
if (! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
System.out.println(Thread.currentThread().getName() + " Thread locking file");
String s0 = "一声梧叶一声秋,一点芭蕉一点愁,三更归梦三更后。落灯花,棋未收,叹新丰逆旅淹留。枕上十年事,江南二老忧,都到心头。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
Thread.sleep(100000);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(fileLock)) {
try {
fileLock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
if (Objects.nonNull(fChannel)) {
try {
fChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}, "FileLockThread-1").start();
new Thread(() -> {
FileChannel fChannel = null;
FileLock fileLock = null;
try {
fChannel = FileChannel.open(path , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
fileLock = fChannel.lock();
if (Objects.isNull(fileLock) || ! fileLock.isValid()) {
System.out.println("file lock is invalid");
return;
}
System.out.println(Thread.currentThread().getName() + " Thread locking file");
String s0 = "@@@@@@@@@@@@@@@@@@@@@@@<<<<M><><><><><><><>>>>>>>>>>>";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(fileLock)) {
try {
fileLock.release();
} catch (IOException e) {
e.printStackTrace();
}
}
if (Objects.nonNull(fChannel)) {
try {
fChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
} , "FileLockThread-2").start();
Thread.sleep(10000);
}
内存映射文件:
将文件直接映射到内存中,就可以在内存中直接操作文件内容。文件映射有三种模式 , READ_ONLY 只读模式, READ_WRITE 读写模式, PRIVATE 写时拷贝模式 。写时拷贝意味着通过 put( )方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer 实例可以看到。该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。尽管写时拷贝的映射可以防止底层文件被修改,您也必须以 read/write 权限来打开文件以建立 MapMode.PRIVATE 映射。只有这样,返回的MappedByteBuffer 对象才能允许使用 put( )方法。通过内存映射机制来访问一个文件会比使用常规方法读写高效得多,甚至比使用通道的效率都高。因为不需要做明确的系统调用,那会很消耗时间。更重要的是,操作系统的虚拟内存可以自动缓存内存页(memory page)。这些页是用系统内存来缓存的,所以不会消耗 Java 虚拟机内存堆(memory heap)。一旦一个内存页已经生效(从磁盘上缓存进来),它就能以完全的硬件速度再次被访问而不需要再次调用系统命令来获取数据。那些包含索引以及其他需频繁引用或更新的内容的巨大而结构化文件能因内存映射机制受益非常多。一个映射一旦建立之后将保持有效,直到MappedByteBuffer 对象被施以垃圾收集动作为止。同锁不一样的是,映射缓冲区没有绑定到创建它们的通道上。关闭相关联的 FileChannel 不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。MemoryMappedBuffer 直接反映它所关联的磁盘文件。如果映射有效时文件被在结构上修改,就会产生奇怪的行为(当然具体的行为是取决于操作系统和文件系统的)。MemoryMappedBuffer有固定的大小,不过它所映射的文件却是弹性的。具体来说,如果映射有效时文件大小变化了,那么缓冲区的部分或全部内容都可能无法访问,并将返回未定义的数据或者抛出未检查的异常。所有的 MappedByteBuffer 对象都是直接的,这意味着它们占用的内存空间位于 Java 虚拟机内存堆之外。 内存映射文件测试代码 :
@Test
public void mapTest() throws Exception {
// 在内存映射缓冲区上做的修改会同步到文件中
Path path = Paths.get("D:\\Documents\\Pictures\\test\\map test.txt");
FileChannel fChannel = FileChannel.open(path , StandardOpenOption.CREATE_NEW , StandardOpenOption.WRITE , StandardOpenOption.READ);
Assert.assertTrue(fChannel.isOpen());
String s0 = "一声梧叶一声秋,一点芭蕉一点愁,三更归梦三更后。落灯花,棋未收,叹新丰逆旅淹留。枕上十年事,江南二老忧,都到心头。";
byte[] bytes = s0.getBytes(Charset.forName("utf-8"));
ByteBuffer buffer0 = ByteBuffer.allocateDirect(bytes.length);
buffer0.put(bytes);
buffer0.flip();
fChannel.write(buffer0);
String s1 = ">>>>>>>>>>>>>>>>>>";
MappedByteBuffer buffer1 = fChannel.map(FileChannel.MapMode.READ_WRITE, 0, s1.getBytes().length);
fChannel.close();
buffer1.put(s1.getBytes(Charset.forName("utf-8")));
}
AsynchronousFileChannel 异步的文件 channel
AsynchronousFileChannel APIs 简介 (异步文件 channel 对文件的读、写、锁定操作都是异步进行的):
public abstract class AsynchronousFileChannel
implements AsynchronousChannel
{
protected AsynchronousFileChannel() {
}
/**
* 打开一个文件 channel , 用指定的线程池和 channel 绑定 ,如果 executor 为 null 使用默认的线程池
*/
public static AsynchronousFileChannel open(Path file,
Set<? extends OpenOption> options,
ExecutorService executor,
FileAttribute<?>... attrs) throws IOException ;
// 打开一个文件 channel , 使用默认的线程池和 channel 绑定
public static AsynchronousFileChannel open(Path file, OpenOption... options) throws IOException;
// 获取文件大小,单位字节
public abstract long size() throws IOException;
// 将文件截断为指定的大小
public abstract AsynchronousFileChannel truncate(long size) throws IOException;
// 强制将此通道文件的任何更新写入包含该通道的存储设备
public abstract void force(boolean metaData) throws IOException;
// 异步的获取指定文件区域的锁定 , 无阻塞 , 获取锁完成或者失败后会回调 CompletionHandler
public abstract <A> void lock(long position, long size, boolean shared, A attachment, CompletionHandler<FileLock,? super A> handler);
// 异步的获取文件区域的排它锁 , 无阻塞, 获取锁完成或者失败后会回调 CompletionHandler
public final <A> void lock(A attachment,CompletionHandler<FileLock,? super A> handler);
// 异步的获取指定文件区域的锁定 , 无阻塞 ,返回 Future
public abstract Future<FileLock> lock(long position, long size, boolean shared);
// 异步的获取指定文件区域的排它锁 , 无阻塞 ,返回 Future
public final Future<FileLock> lock();
// 尝试获取指定文件区域的锁定 , 如果该文件区域正被锁定返回 null , 无阻塞
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
// 尝试获取文件的锁定 , 如果该文件区域正被锁定返回 null , 无阻塞
public final FileLock tryLock() throws IOException ;
// 异步的将 channel 中的数据读取到 Buffer 中 , 无阻塞 , 读取完成或者失败后会回调 CompletionHandler
public abstract <A> void read(ByteBuffer dst, long position, A attachment,CompletionHandler<Integer,? super A> handler);
// 异步的将 channel 中的数据读取到 Buffer 中, 无阻塞 ,返回一个 Future
public abstract Future<Integer> read(ByteBuffer dst, long position);
// 异步的将 Buffer 中的数据写入到 channel 中 , 无阻塞 , 写入完成或者失败后会回调 CompletionHandler
public abstract <A> void write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer,? super A> handler);
// 异步的将 Buffer 中的数据写入到 channel 中 , 无阻塞 ,返回一个 Future
public abstract Future<Integer> write(ByteBuffer src, long position);
}
多路复用 Stream I/O Channel
多路复用 I/O 是同步非阻塞的 I/O 它的优点是可以通过一个线程来处理大量的网络连接不会阻塞 , 简单来说可以提高吞吐量 , 告别阻塞式 I/O 的一请求一线程模式,或者是线程池模式。
SelectableChannel 是所有支持多路复用 I/O channel 的基类 , APIs 简介 :
public abstract class SelectableChannel
extends AbstractInterruptibleChannel
implements Channel
{
protected SelectableChannel() { }
public abstract SelectorProvider provider();
// 获取该 channel 支持的操作
public abstract int validOps();
// 检查该 channel 是否注册
public abstract boolean isRegistered();
// 获取该 channel 在 Selector 上注册的选择键
public abstract SelectionKey keyFor(Selector sel);
// channel 将感兴趣的操作注册到选择器上
public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException;
// channel 将感兴趣的操作注册到选择器上
public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException;
// 设置 channel 处于阻塞模式或非阻塞模式 , true - 阻塞, false - 非阻塞
public abstract SelectableChannel configureBlocking(boolean block) throws IOException;
// 检查 channel 是否是阻塞模式
public abstract boolean isBlocking();
// 检索configureBlocking和register方法同步的对象
public abstract Object blockingLock();
}
从 SelectableChannel 的 API 反映出来一些特性 :
1. channel 都有支持的操作类型;
2. channel 具有注册到某个选择器(也称为多路复用器) Selector 的能力,注册的时候要指定一个事件(操作类型) , 这个事件是 channel 感兴趣的事件,但并不代表某一时刻正在发生的事件。
3. channel 可以工作在阻塞模式或者是非阻塞模式下 。多路复用 I/O 模型要求 channel 必须工作在非阻塞模式下。
4. channel 注册到选择器上后会得到一个 SelectionKey (选择键)。
顺着梳理的逻辑继续学习 , 多路复用器 Selector , Selector APIs 简介 :
public abstract class Selector implements Closeable {
protected Selector() { }
// 打开一个多路复用器
public static Selector open() throws IOException ;
// 检查多路复用器是否处于打开状态
public abstract boolean isOpen();
// 获取创建此选择器的提供者
public abstract SelectorProvider provider();
// 获取该多路复用器中注册的所有选择键
public abstract Set<SelectionKey> keys();
// 获取多路复用器中选定的选择键
public abstract Set<SelectionKey> selectedKeys();
// 选择一组其 channel 已经处于 I/O 就绪状态的选择键 , 该方法不会阻塞立即返回 , 返回选中的选择键的个数
public abstract int selectNow() throws IOException;
// 选择一组其 channel 已经处于 I/O 就绪状态的选择键 ,在阻塞时间到达指定的 timeout 时间后返回
public abstract int select(long timeout);
// 选择一组其 channel 已经处于 I/O 就绪状态的选择键 , 该方法会阻塞直到至少有一个 channel 被选中。
// 返回值不表示选中的处于就绪状态的通道数量,而是处于就绪状态的通道中就绪状态已经更新的通道数量 ,
// 所以不能以这个返回值是否为 0 来判断是否有处于就绪状态的通道被选中
public abstract int select() throws IOException;
// 使得尚未返回的第一个选择操作立即返回
public abstract Selector wakeup();
// 关闭该多路复用器
public abstract void close() throws IOException;
}
多路复用器中有三个 select 方法 , 它们都是用来选择一组已经处于 I/O 就绪状态的 channel 。 通过 selectedKeys 可以获取到被选中的 channel 的 SelectionKey (选择键)。
SelectionKey APIs 简介 :
public abstract class SelectionKey {
protected SelectionKey() { }
// 获取选择键对应的 channel
public abstract SelectableChannel channel();
// 获取选择键对应的多路复用器
public abstract Selector selector();
// 检查该选择键是否有效
public abstract boolean isValid();
// 取消该选择键 , 该选择键将被多路复用器移除
public abstract void cancel();
// 获取 channel 注册的感兴趣的事件
public abstract int interestOps();
// 设置感兴趣的事件
public abstract SelectionKey interestOps(int ops);
// channel 处于就绪状态的事件
public abstract int readyOps();
// 读操作
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;
// channel 当前的就绪事件是否是读操作
public final boolean isReadable();
// channel 当前的就绪事件是否是写操作
public final boolean isWritable();
// channel 当前的就绪事件是否是连接操作
public final boolean isConnectable();
// channel 当前的就绪事件是否是接受连接操作
public final boolean isAcceptable();
// 将给定对象附加到此键
public final Object attach(Object ob);
// 获取此选择键的附加对象
public final Object attachment();
}
通过 SelectionKey 可以获取到处于就绪状态的 channel , 也可以知道 channel 当前处于那种操作的就绪状态下 , 就可以进行对应的处理。也可以给 channel 注册新的感兴趣的事件类型。基于 Selector, SelectionKey , SelectableChannel 就可以编写 多路复用 I/O 模型的服务端和客户端程序了。具体使用的是 ServerSocketChannel , SocketChannel , ServerSocketChannel 需要绑定本机的某个端口以实现网络 I/O 。SocketChannel 也需要绑定本机的某个端口进行 I/O 不过这个端口是随机的不需要自己指定,SocketChannel 需要关心的是与服务端建立连接的过程,因为 TCP 协议是面向连接的传输协议。另外 ServerSocketChannel 只支持 Accept 操作 , SocketChannel 支持 Read 、 Write、 Connect 操作。
SocketChannel
非阻塞模式下的 connect() :
如果此通道处于非阻塞模式,则调用方法启动非阻塞连接操作。如果连接立即建立,就像本地连接可能发生的那样此方法返回 true。否则,此方法返回false,连接操作必须在稍后通过调用 finishConnect 方法完成。
阻塞模式下的 connect() :
如果此通道处于阻塞模式,则调用方法将阻塞,直到建立连接或I/O错误发生。
finishConnect() :
完成连接套接字通道的过程。通过在非阻塞模式中放置套接字通道,然后调用其连接方法来启动非阻塞连接操作。一旦建立连接,或者尝试失败,套接字通道将成为可连接的,并且可以调用此方法来完成连接序列。如果连接操作失败,那么调用此方法将导致适当的 IOException 被抛出。如果此通道已连接,则此方法不会阻塞并立即返回true。如果该通道处于非阻塞模式,那么如果连接过程尚未完成,该方法将返回false。如果此通道处于阻塞模式,则该方法将阻塞,直到连接完成或失败,并且总是返回true或抛出描述失败的检查异常。此方法可随时调用。如果在该方法的调用过程中调用该信道上的读或写操作,则该操作将首先阻塞,直到完成该调用。如果连接尝试失败,也就是说,如果该方法的调用抛出了检查异常,则该通道将被关闭。
isConnectionPending() :
检查该通道上的连接操作是否正在进行。
NIO 客户端服务端示例代码 :
package net.j4love.nio.channels;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* @author he peng
* @create 2018/6/4 18:14
* @see
*/
public class SocketChannelTest {
final SocketAddress socketAddress = new InetSocketAddress("127.0.0.1" , 9999);
public SocketChannelTest() throws IOException {}
// 开启客户端
@Test
public void openClientTest0() throws Exception {
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
Selector selector = Selector.open();
sChannel.register(selector , SelectionKey.OP_CONNECT);
boolean connected = sChannel.connect(socketAddress);
try {
while (sChannel.isOpen() && selector.isOpen()) {
selector.select();
// select() 返回值不表示选中的处于就绪状态的通道数量,而是处于就绪状态的通道中就绪状态已经更新的通道数量 ,
// 所以不能以这个返回值是否为 0 来判断是否有处于就绪状态的通道被选中
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (! sk.isValid()) {
System.out.println("selection key is invalid");
continue;
} else if (sk.isAcceptable()) {
System.out.println("selection key is Acceptable");
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
if (! connected) {
SocketChannel channel = (SocketChannel) sk.channel();
if (! channel.isConnected()) {
channel.finishConnect();
channel.register(selector , SelectionKey.OP_WRITE);
System.out.println("connect finished");
}
}
} else if (sk.isReadable()) {
System.out.println("selection key is Readable");
SocketChannel channel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from server message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
channel.register(selector , SelectionKey.OP_WRITE);
} else if (sk.isWritable()) {
System.out.println("selection key is Writable");
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello server".getBytes());
buffer.flip();
channel.write(buffer);
channel.register(selector , SelectionKey.OP_READ);
}
} else {
throw new IllegalStateException("Unknown readyOps");
}
}
}
} finally {
sChannel.close();
selector.close();
}
}
// 开启服务端
@Test
public void openServerTest0() throws Exception {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ssChannel.bind(socketAddress);
System.out.println("server start in " + socketAddress);
try {
while (ssChannel.isOpen() && selector.isOpen()) {
System.out.println("Select .......");
selector.select();
// select() 返回值不表示选中的处于就绪状态的通道数量,而是处于就绪状态的通道中就绪状态已经更新的通道数量 ,
// 所以不能以这个返回值是否为 0 来判断是否有处于就绪状态的通道被选中
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
if (! sk.isValid()) {
try {
System.out.println("selection key is invalid");
continue;
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isAcceptable()) {
try {
System.out.println("selection key is Acceptable");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) sk.channel();
SocketChannel sChannel = serverSocketChannel.accept();
if (Objects.nonNull(sChannel)) {
sChannel.configureBlocking(false);
sChannel.register(selector , SelectionKey.OP_READ);
}
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
} else if (sk.isReadable()) {
try {
System.out.println("selection key is Readable");
SocketChannel sChannel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from client (" + sChannel.getRemoteAddress() + ") message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
sChannel.register(selector , SelectionKey.OP_WRITE);
} catch (Exception e) {
e.printStackTrace();
}
} else if (sk.isWritable()) {
try {
System.out.println("selection key is Writable");
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello client".getBytes());
buffer.flip();
channel.write(buffer);
channel.register(selector , SelectionKey.OP_READ);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
throw new IllegalStateException("Unknown readyOps");
}
}
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
ssChannel.close();
selector.close();
}
}
}
以上代码是一个粗略的简单测试 , 没有处理客户端断开的情况 , 以及当多个客户端都断开后服务端有时会产生空轮询的情况 , 也就是 select() 方法并没有阻塞, 这是 Java NIO 一直存在的 bug :
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719,
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933 ,
https://www.cnblogs.com/JAYIT/p/8241634.html

出现这种情况最终导致了机器 CPU 被跑满, 机器变得巨慢。查阅了一些资料都说是在 linux 平台上出现, jdk 1.7 已经修复了。但是我是用的是 windows 系统 , jdk 1.8 还是出现这个问题,看来问题并没有被修复。

“NIO 空轮询bug” 内容勘误
由于本人的技术能力有限,对一些技术理解、掌握上有误造成了对大家的误导(关于 NIO 空轮询 bug 部分的内容),十分抱歉。在这里及时修复错误,和大家一起交流学习。Selector 的 select() 函数是会阻塞的,并且一直会阻塞到至少有一个处于就绪状态的 channel 为止。 select() 会返回一个 int 类型的值 , 之前我认为返回的这个值表示选中的 channel 的个数。我在代码中判断了这个值是否为 0 , 如果为 0 就继续外层循环, 于是导致了空轮询的问题 。问题代码如下 :
while(true) {
System.out.println("Select ......");
int selectedNum = selector.select();
if (selectedNum == 0) {
continue;
}
}
事实上 select() 函数的返回值并不表示选中的 channel 的个数 , 而是选中的 channel 中就绪状态更新了的个数。所以不能以这个返回值是否为 0 来判断是否有处于就绪状态的通道被选中。 应该以 selectedKeys() 函数的返回值为准。 另外 selectedKeys() 函数返回的 Set 是线程不安全的 , 需要自己进行同步的处理 。在通信过程中如果客户端异常断开,服务端如果不做任何处理(比如说关闭客户端的通道 , channel.close()) , 那么服务端会依然认为这个客户端的 channel 是可用的、存活状态,再每次选择时依然会选中它,只不过在进行 write 、 read 操作的时就会抛出异常 (因为客户端实际上已经断开了)。
查阅了一些资料,根据 netty , jetty 关于这个问题的解决方案做了尝试,对测试代码做了改进,尝试应用 Reactor 模式编写 NIO 客户端和服务端代码 ,因为是测试代码为了看起来直观点,我将所有的代码全都写在了一起 , 没有进行拆分。
package net.j4love.nio.test;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author he peng
* @create 2018/6/5 16:07
* @see
*/
public class NioServerTest {
// nio server 测试
static final SocketAddress socketAddress = new InetSocketAddress("127.0.0.1" , 9999);
final Object serverSelectedKeysLock = new Object();
static final int JVM_BUG_THRESHOLD = 5;
final SelectorHolder serverSelectorHolder = new SelectorHolder();
static class SelectorHolder {
private Selector selector;
public synchronized SelectorHolder setSelector(Selector selector) {
this.selector = selector;
return this;
}
public synchronized Selector getSelector() {
return selector;
}
}
// 打开服务端
@Test
public void reactorModelServerTest() throws Exception {
Set<SocketAddress> connectedClients = new HashSet<>();
AtomicInteger bossThreadCount = new AtomicInteger(1);
AtomicInteger workerThreadCount = new AtomicInteger(1);
final ThreadPoolExecutor bossThreadPool = new ThreadPoolExecutor(1, 2,
30, TimeUnit.SECONDS,
new LinkedBlockingQueue() ,
r -> {
Thread t = new Thread(r , "NioBossThread-" + bossThreadCount.getAndIncrement());
t.setDaemon(true);
return t;
} ,
new ThreadPoolExecutor.AbortPolicy());
final ThreadPoolExecutor workerThreadPool = new ThreadPoolExecutor(4, 6,
30, TimeUnit.SECONDS,
new LinkedBlockingQueue() ,
r -> {
Thread t = new Thread(r , "NioWorkerThread-" + workerThreadCount.getAndIncrement());
t.setDaemon(true);
return t;
} ,
new ThreadPoolExecutor.AbortPolicy());
Selector selector = Selector.open();
serverSelectorHolder.setSelector(selector);
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector , SelectionKey.OP_ACCEPT);
ssChannel.bind(socketAddress);
System.out.println("[" + Thread.currentThread().getName() + "]" + "server start in " + socketAddress);
bossThreadPool.execute(() -> {
int jvmBug = 0;
try {
while (ssChannel.isOpen() && serverSelectorHolder.getSelector().isOpen()) {
System.out.println("[" + Thread.currentThread().getName() + "] Select .........");
// select 是线程安全的
int selectedNum = serverSelectorHolder.getSelector().select();
/*if (selectedNum == 0) {
// 解决 nio 空轮训 bug
jvmBug++;
if (jvmBug > JVM_BUG_THRESHOLD) {
Selector newSelector = Selector.open();
for (SelectionKey sk : serverSelectorHolder.getSelector().keys()) {
if (! sk.isValid() || sk.interestOps() == 0) {
continue;
}
SelectableChannel channel = sk.channel();
if (Objects.nonNull(channel) && channel.isOpen()) {
channel.register(newSelector, sk.interestOps());
}
}
Selector oldSelector = serverSelectorHolder.getSelector();
serverSelectorHolder.setSelector(newSelector);
if (oldSelector.isOpen()) {
oldSelector.close();
}
System.out.println("[" + Thread.currentThread().getName() + "]" + "Fix Nio epoll empty polling bug .......");
jvmBug = 0;
}
continue;
}*/
/*Set<SelectionKey> selectionKeys;
synchronized (serverSelectedKeysLock) {
// selectedKeys 是线程不安全的
// 仔细考虑了一下这里对线程安全的理解不够透彻 , 这是在函数内是否真的涉及到了线程安全的问题?
// 这里 Selector 会被多个线程并发的访问 , Selector 中存储了通道
selectionKeys = serverSelectorHolder.getSelector().selectedKeys();
}*/
Set<SelectionKey> selectionKeys = serverSelectorHolder.getSelector().selectedKeys();
if (Objects.isNull(selectionKeys) && selectionKeys.isEmpty()) {
continue;
}
Iterator<SelectionKey> iterator = serverSelectorHolder.getSelector().selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
synchronized (serverSelectorHolder) {
iterator.remove();
}
// work thread
if (! sk.isValid()) {
workerThreadPool.execute(() -> System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is invalid"));
continue;
} else if (sk.isAcceptable()) {
Thread.sleep(100);
workerThreadPool.execute(() -> {
try {
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Acceptable");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) sk.channel();
SocketChannel sChannel = serverSocketChannel.accept();
if (Objects.nonNull(sChannel)) {
connectedClients.add(sChannel.getRemoteAddress());
sChannel.configureBlocking(false);
sChannel.register(serverSelectorHolder.getSelector() , SelectionKey.OP_READ);
System.out.println("[" + Thread.currentThread().getName() + "] Accept" + sChannel.getRemoteAddress() + " Connect");
}
} catch (Exception e) {
e.printStackTrace();
}
});
} else if (sk.isConnectable()) {
workerThreadPool.execute(() -> System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Connectable"));
} else if (sk.isReadable()) {
workerThreadPool.execute(() -> {
SocketChannel sChannel = (SocketChannel) sk.channel();
if (Objects.isNull(sChannel) || ! sChannel.isOpen()) {
return;
}
SocketAddress remoteAddress = null;
try {
remoteAddress = sChannel.getRemoteAddress();
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Readable");
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("[" + Thread.currentThread().getName() + "] " +
"receive from client (" + sChannel.getRemoteAddress() + ") message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
workerThreadPool.execute(() -> {
SocketChannel channel = (SocketChannel) sk.channel();
if (Objects.isNull(channel) || ! channel.isOpen()) {
return;
}
SocketAddress remoteAddress1 = null;
try {
remoteAddress1 = channel.getRemoteAddress();
System.out.println("[" + Thread.currentThread().getName() + "] " + "write to -> " + remoteAddress1);
if (channel.isConnected()) {
ByteBuffer buffer1 = ByteBuffer.allocate(1000);
buffer1.put("hello client".getBytes());
buffer1.flip();
channel.write(buffer1);
}
} catch (Exception e) {
// 异步关闭有点问题 ,channel 没有被真正关闭掉 ,应该是对线程使用的有问题
// 异步的关闭通道 , 因为有可能会阻塞
/*SocketAddress remoteAddress2 = remoteAddress1;
workerThreadPool.execute(() -> {
System.out.println("Client (" + remoteAddress2 + ") Error ");
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
});*/
System.out.println("Client (" + remoteAddress2 + ") Error ");
try {
channel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
});
} catch (Exception e) {
/*SocketAddress remoteAddress1 = remoteAddress;
workerThreadPool.execute(() -> {
System.out.println("Client (" + remoteAddress1 + ") Error ");
try {
sChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
});*/
System.out.println("Client (" + remoteAddress1 + ") Error ");
try {
sChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
});
} else if (sk.isWritable()) {
System.out.println("[" + Thread.currentThread().getName() + "] " + "selection key is Writable");
} else {
workerThreadPool.execute(() -> System.err.println("Unknown readyOps -> " + sk.readyOps()));
}
}
}
} catch (Exception e) {
System.err.println("[" + Thread.currentThread().getName() + "] Server Exception , " +
"Connected Client -> " + connectedClients.size() +
" Client Address -> " + connectedClients);
e.printStackTrace();
} finally {
if (Objects.nonNull(ssChannel)) {
workerThreadPool.execute(() -> {
try {
System.out.println("server channel close");
ssChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
});
}
if (Objects.nonNull(serverSelectorHolder.getSelector())) {
try {
System.out.println("server selector close");
serverSelectorHolder.getSelector().close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.err.println("Exception Terminates the Java Virtual Machine");
System.exit(1);
}
});
System.out.println(Thread.currentThread().getName() + " blocking");
Thread.sleep(Integer.MAX_VALUE);
}
// 打开客户端
@Test
public void openClientTest() throws Exception {
final SelectorHolder clientSelectorHolder = new SelectorHolder();
SocketChannel sChannel = SocketChannel.open();
sChannel.configureBlocking(false);
Selector oldSelector = Selector.open();
clientSelectorHolder.setSelector(oldSelector);
sChannel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_CONNECT);
boolean connected = sChannel.connect(socketAddress);
int jvmBug = 0;
try {
while (sChannel.isOpen() && clientSelectorHolder.getSelector().isOpen()) {
// 测试 nio 空轮询 bug
System.out.println("[" + Thread.currentThread().getName() + "] Select .........");
int selectedNum = clientSelectorHolder.getSelector().select();
/*if (selectedNum == 0) {
// 解决 nio 空轮训 bug
jvmBug++;
if (jvmBug > JVM_BUG_THRESHOLD) {
Selector newSelector = Selector.open();
for (SelectionKey sk : clientSelectorHolder.getSelector().keys()) {
if (! sk.isValid() || sk.interestOps() == 0) {
continue;
}
SelectableChannel channel = sk.channel();
if (Objects.nonNull(channel) && channel.isOpen()) {
channel.register(newSelector, sk.interestOps());
}
}
oldSelector = clientSelectorHolder.getSelector();
clientSelectorHolder.setSelector(newSelector);
if (oldSelector.isOpen()) {
oldSelector.close();
}
System.out.println("Fix Nio epoll empty polling bug ....... keys -> " + newSelector.keys().size());
jvmBug = 0;
}
continue;
}*/
Iterator<SelectionKey> iterator = clientSelectorHolder.getSelector().selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sk = iterator.next();
if (! sk.isValid()) {
System.out.println("selection key is invalid");
continue;
} else if (sk.isAcceptable()) {
System.out.println("selection key is Acceptable");
} else if (sk.isConnectable()) {
System.out.println("selection key is Connectable");
if (! connected) {
SocketChannel channel = (SocketChannel) sk.channel();
if (! channel.isConnected()) {
channel.finishConnect();
channel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_WRITE);
System.out.println(channel.getLocalAddress() + " connection to -> " + channel.getRemoteAddress());
}
}
} else if (sk.isReadable()) {
System.out.println("selection key is Readable");
SocketChannel channel = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
sChannel.read(buffer);
buffer.flip();
System.out.println("receive from server message -> " +
new String(buffer.array() , 0 , buffer.limit() , Charset.forName("utf-8")) +
" ,time -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
if (channel.isConnected()) {
System.out.println("write message to server");
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
buffer1.put(("(" + channel.getLocalAddress() + ") say hello server").getBytes());
buffer1.flip();
channel.write(buffer1);
}
} else if (sk.isWritable()) {
SocketChannel channel = (SocketChannel) sk.channel();
if (channel.isConnected()) {
System.out.println("write message to server");
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(("(" + channel.getLocalAddress() + ") say hello server").getBytes());
buffer.flip();
channel.write(buffer);
channel.register(clientSelectorHolder.getSelector() , SelectionKey.OP_READ);
}
} else {
System.err.println("Unknown readyOps -> " + sk.readyOps());
}
iterator.remove();
}
}
} finally {
Thread t = new Thread("SocketChannelCloseThread") {
@Override
public void run() {
try {
sChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
t.setDaemon(true);
t.start();
clientSelectorHolder.getSelector().close();
}
}
}

老实说我不太确认上面编写的代码是否符合 Reactor 模式 , 希望有大佬可以交流,希望发现问题的老师指正。测试代码中有很多的网络通信过程中的问题都没有解决 , 比如 TCP 粘包、拆包问题 , 数据的编码、解码问题 , 优雅停机问题 , 合适优化的线程模型 等等问题,可以体会到编写一个商用级别的网络通信框架类似 netty , mina 还是很复杂的。
Selector 的并发性 :
选择器对象是线程安全的,但它们包含的键集合不是。通过 keys( )和 selectKeys( )返回的键的集合是 Selector 对象内部的私有的 Set 对象集合的直接引用。这些集合可能在任意时间被改变。已注 册 的 键 的 集 合 是 只 读 的 。 如 果 试 图 修 改 它 , 那 么 会 得 到 的 一 个java.lang.UnsupportedOperationException,但是当观察它们的时候,它们可能发生了改变的话,仍然会遇到麻烦。Iterator 对象是快速失败的(fail-fast):如果底层的 Set 被改变了,它们将会抛出 java.util.ConcurrentModificationException,因此如果在多个线程间共享选择器和/或键,请对此做好准备。可以直接修改选择键,但请注意这么做时可能会彻底破坏另一个线程的 Iterator。如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,可以采取一些步骤来合理地同步访问。在执行选择操作时,选择器在 Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。已取消的键的集合也在选择过程的的第 1 步和第 3 步之间保持同步(当与已取消的键的集合相关的通道被注销时)。 Selector 类的 close( )方法与 slect( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对 close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。在后面的情况下,执行选择的线程将会在执行关闭的线程获得锁是立即被唤醒,并关闭选择器。
后记
对 NIO Channel 学习的感受是,理论理解起来或许能够很简单,也较为容易理解,但当根据这个理论去实现一个稳定可用的产品时却是困难重重,需要考虑到很多问题,排除很多阻碍,攻破自己的技术壁垒。要培养技术的广度和敏感性,在出了问题时能够快速的联想到一些解决方案或者是类似的遇到过的问题。比如我在编写测试代码时,遇到 Selector 空轮询问题的时候,我尝试了一个早晨的时间也没有能够解决问题 , 下午突然想起来好像在那一本书中看到过 nio epoll 空轮询 bug 的问题 , 随即查阅这方面的资料,结合尝试实验可以确定确实是 nio 空轮询的bug 导致的。想要构建上层产品一定要对底层技术、知识有很好的掌握程度,比如对多线程编程不熟、对网络传输协议不熟是不可能写出像 netty 这样的产品的。所以这就是我们应该重视所谓的基础技术、知识的训练和学习。希望通过对 Java NIO 的浅显学习建立了一点学习 netty 、mina 类似网络通信框架的基础。
原文地址:https://my.oschina.net/j4love/blog/1828336
本文围绕 Java NIO Channel 展开,介绍了 Channel 概念及结构,从 FileChannel 入门讲解其读写、锁定、内存映射等操作。还介绍了 AsynchronousFileChannel 异步操作和多路复用 Stream I/O Channel 原理,分析了 NIO 空轮询 bug 并勘误,最后强调掌握基础技术对构建上层产品的重要性。
5214

被折叠的 条评论
为什么被折叠?



