NIO概念
最开始的阻塞式IO,它在每一个连接创建时,都需要一个用户线程来处理,并且在IO操作没有就绪或者结束时,线程被挂起,进入阻塞等待状态,阻塞式IO就成为导致性能瓶颈的根本原因。
阻塞式发生在那些环节呢?
- 首先,应用程序通过系统调用socket创建一个套接字,他是分配给应用程序的一个文件描述符
- 其次,应用程序会通过系统调用bind,绑定地址和端口号,给套接字命名一个名称
- 然后,系统会调用listen创建一个队列用于存放客户端进来的请求
- 最后,应用服务会通过系统嗲用accept来监听客户端的连接请求
当有一个客户端连接到服务端之后,服务端就会调用fork创建一个子进程,通过系统调用read监听客户端发来的消息,再通过write向客户端返回信息。
1. 阻塞式IO
在整个socket通信工作流程中,socket的默认状态是阻塞的。也就是说,当发出一个不能立即完成的套接字调用时,其进程将被阻塞,被系统挂起,进入睡眠状态,一直等待响应的操作响应。
connect阻塞:
当客户端发起TCP请求,通过系统调用connect函数,TCP连接的建立需要完成三次握手过程,客户端需要等待服务端发送过来的ACK以及SYN信号,同样服务端也需要阻塞等待客户端连接的ACK信号,这就意味着会阻塞等待,直到确认连接。
accept阻塞:
一个阻塞的socket通信的服务端接收外来连接,会调用accept函数,如果没有新的连接到达,调用进程将被挂起,进入阻塞状态。
read、write阻塞
当一个socket连接创建成功之后,服务端用fork函数创建一个子进程,调用read函数等待客户端的数据写入,如果没有数据写入,调用子进程将被挂起,进入阻塞状态。
2. 非阻塞式IO
- 使用fcntl可以把以上三个操作都设置为非阻塞操作。如果没有数据返回,就会直接返回一个EWOULDBLOCK或EAGAIN错误,此时进程将不会一直被阻塞。
- 当我们把以上操作设置为了非阻塞状态,我们需要设置一个线程对该操作进行轮询检查,这是最传统的非阻塞IO模型。
3. IO复用
- 如果使用用户线程轮询查看一个IO操作的状态,在大量请求的情况下,这对于CPU的使用率是灾难。
- linux提供了IO复用函数select/poll/epoll,进程将一个或多个读操作通过系统调用函数,阻塞在函数操作上。这样,系统内核就可以帮助我们侦测多个读操作是否处于就绪状态。
select()函数
- 在超时时间内,监听用户感兴趣的文件描述符上的可读可写和异常事件的发生。
- linux操作系统的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符fd。
- select() 函数监视的文件描述符分 3 类,分别是 writefds(写文件描述符)、readfds(读文件描述符)以及 exceptfds(异常事件文件描述符)。遍历所有文件描述符寻找符合这三种情况的交给应用程序
- 调用后select()函数会阻塞,直到有描述符就绪或者超时,函数返回。当select函数返回后,可以通过函数FD_ISSET比那里fdset,来找到就绪的描述符。
- fd_set可以理解而我一个集合,这个集合中存放的是文件描述符。
epoll()函数
-
select是顺序描述fd是否就绪,而且支持fd数量不宜过大。
-
epoll使用事件驱动的方式代替轮询扫描fd。
-
epoll实现通过epoll_ctl来注册一个文件描述符,将文件描述符存放到内核的一个事件表中,这个事件是基于红黑树实现的,所以在大量IO请求的场景下, 插入和删除的性能比select/poll的数组fd_set要好,因此epoll的性能更胜一筹,而且不会受到fd数量的限制。
-
一旦某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知,之后进程将完成相关 I/O 操作。
-
和select相比优点在于:
select是对加进去的所有fd进行轮询,返回之后也要对整个fd进行一次轮询,才能找到准备好的fd。 epoll采用事件触发的方式,当某个fd准备好后会触发事件,这样减少了内核的轮询。同时,epoll返回的是那 些 准备好的fd,避免程序员进行全部的轮询
另外,select的fd数上限一般是1024.但是epoll没有上限。
在 NIO 服务端通信编程中,首先会创建一个 Channel,用于监听客户端连接;接着,创建多路复用器 Selector,并将 Channel 注册到 Selector,程序会通过 Selector 来轮询注册在其上的 Channel,当发现一个或多个 Channel 处于就绪状态时,返回就绪的监听事件,最后程序匹配到监听事件,进行相关的 I/O 操作。
5. 异步IO
特性:读取数据之后,再通知我 NIO是同步非阻塞的 AIO是异步非阻塞的 由于NIO的读写过程依然在应用线程里完成,所以对于那些读写过程时间长的,NIO就不太适合。(NIO的真正读写还是交给了应用程序,select或者eproll仅仅通知应用程序有可读事件,应用程序还需要自己进行read操作) 而AIO的读写过程完成后才被通知,所以AIO能够胜任那些重量级,读写过程长的任务。 与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可(入参是回调执行,不阻塞)。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,
java NIO
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。本系列教程将有助于你学习和理解Java NIO。
NIO 与 IO 的区别
NIO 重要知识点详细介绍
1通道和缓冲区
Java NIO 系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输,Buffer 负责存储
面向流和面向缓冲区解释
- 传统 IO 流
对上图说明一下: ①我们需要把磁盘文件或者网络文件中的数据读取到程序中来,我们需要建立一个用于传输数据的管道,原来我们传输数据面对的直接就是管道里面一个个字节数据的流动(我们弄了一个 byte 数组,来回进行数据传递),所以说原来的 IO 它面对的就是管道里面的一个数据流动,所以我们说原来的 IO 是面向流的 ②我们说传统的 IO 还有一个特点就是,它是单向的。解释一下就是:如果说我们想把目标地点的数据读取到程序中来,我们需要建立一个管道,这个管道我们称为输入流。相应的,如果如果我们程序中有数据想要写到目标地点去,我们也得再建立一个管道,这个管道我们称为输出流。所以我们说传统的 IO 流是单向的
解释一下上图: ①我们说只要是 IO ,那么就是为了完成数据传输的。 ②即便你用 NIO ,它也是为了数据传输,所以你要想完成数据传输,你也得建立一个用于传输数据的通道,这个通道你不能把它理解为之前的水流了,但是你可以把它理解为铁路,铁路本身是不能完成运输的,铁路要想完成运输它必须依赖火车,说白了这个通道就是为了连接目标地点和源地点。所以注意通道本身不能传输数据,要想传输数据必须要有缓冲区,这个缓冲区你就可以完全把它理解为火车,比如说你现在想把程序中的数据写到文件中,那么你就可以把数据都写到缓冲区,然后缓冲区通过通道进行传输,最后再把数据从缓冲区拿出来写到文件中,你想把文件中的数据传数到程序中,也是一个道理,把数据写到缓冲区,缓冲区通过通道进行传输,到程序中把数据拿出来。所以我们说原来的 IO 单向的现在的缓冲区是双向的,这种传输数据的方式也叫面向缓冲区。总结一下,就是通道只负责连接,缓冲区才负责存储数据。
3缓冲区的数据存取
缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。 1、缓冲区的类型 缓冲区(Buffer):在 Java NIO 中负责数据的存取。缓冲区就是数组。用于存储不同类型的数据。根据数据类型的不同(boolean 除外),提供了相应类型的缓冲区: ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer 上述缓冲区管理方式几乎一致,都是通过 allocate() 来获取缓冲区 2、缓冲区存取数据的两个核心方法
- put():存入数据到缓冲区中
- get():获取缓冲区中的数据
3、缓冲区中的四个核心属性
- capacity: 容量,表示缓冲区中最大存储数据的容量。一旦声明不能更改。
- limit: 界限,表示缓冲区中可以操作数据的大小。(limit 后的数据不能进行读写)
- position: 位置,表示缓冲区中正在操作数据的位置。
- mark: 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置。
注: 0 <= mark <= position <= limit <= capacity
具体代码案例:
package nio;
import java.nio.ByteBuffer;
public class BufferTest1 {
public static void test1() {
String str = "abcde";
//分配一个指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("---------allocate-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //0
//利用 put() 存入数据到缓冲区中
byteBuffer.put(str.getBytes());
System.out.println("---------put-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //5
//切换到读数据模式
byteBuffer.flip();
System.out.println("---------flip-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5,limit 表示可以操作数据的大小,只有 5 个字节的数据给你读,所以可操作数据大小是 5
System.out.println(byteBuffer.position()); //0,读数据要从第 0 个位置开始读
//利用 get() 读取缓冲区中的数据
byte[] dst = new byte[byteBuffer.limit()];
byteBuffer.get(dst);
System.out.println(new String(dst,0,dst.length));
System.out.println("---------get-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5,可以读取数据的大小依然是 5 个
System.out.println(byteBuffer.position()); //5,读完之后位置变到了第 5 个
//rewind() 可重复读
byteBuffer.rewind(); //这个方法调用完后,又变成了读模式
System.out.println("---------rewind-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //5
System.out.println(byteBuffer.position()); //0
//clear() 清空缓冲区,虽然缓冲区被清空了,但是缓冲区中的数据依然存在,只是出于"被遗忘"状态。意思其实是,缓冲区中的界限、位置等信息都被置为最初的状态了,所以你无法再根据这些信息找到原来的数据了,原来数据就出于"被遗忘"状态
byteBuffer.clear();
System.out.println("---------clear-----------");
System.out.println(byteBuffer.capacity()); //1024
System.out.println(byteBuffer.limit()); //1024
System.out.println(byteBuffer.position()); //0
}
public static void test2() {
String str = "abcde";
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
byte[] bytearray = new byte[byteBuffer.limit()];
byteBuffer.get(bytearray,0,2);
System.out.println(new String(bytearray,0,2)); //结果是 ab
System.out.println(byteBuffer.position()); //结果是 2
//标记一下当前 position 的位置
byteBuffer.mark();
byteBuffer.get(bytearray,2,2);
System.out.println(new String(bytearray,2,2));
System.out.println(byteBuffer.position()); //结果是 4
//reset() 恢复到 mark 的位置
byteBuffer.reset();
System.out.println(byteBuffer.position()); //结果是 2
//判断缓冲区中是否还有剩余数据
if (byteBuffer.hasRemaining()) {
//获取缓冲区中可以操作的数量
System.out.println(byteBuffer.remaining()); //结果是 3,上面 position 是从 2 开始的
}
}
public static void main(String[] args) {
// test1();
test2();
}
}
直接内存和堆内存
DirectBuffer 属于堆外存,那应该还是属于用户内存,而不是内核内存? DirectByteBuffer 自身是(Java)堆内的,它背后真正承载数据的buffer是在(Java)堆外——native memory中的。这是 malloc() 分配出来的内存,是用户态的。
所以使用DirectByteBuffer还是免不了用户态到内核态的copy 执行write操作,如果写的HeapByteBuffer,该方法也会创建一个DirectByteBuffer用于写数据,写完后释放 为什么要将HeapByteBuffer的数据拷贝到DirectByteBuffer呢?不能将数据直接从HeapByteBuffer拷贝到文件中吗? 并不是说操作系统无法直接访问jvm中分配的内存区域,显然操作系统是可以访问所有的本机内存区域的,但是为什么对io的操作都需要将jvm内存区的数据拷贝到堆外内存呢?是因为jvm需要进行GC,如果io设备直接和jvm堆上的数据进行交互,这个时候jvm进行了GC,那么有可能会导致没有被回收的数据进行了压缩,位置被移动到了连续的存储区域,这样会导致正在进行的io操作相关的数据全部乱套,显然是不合理的,所以对io的操作会将jvm的数据拷贝至堆外内存,然后再进行处理,将不会被jvm上GC的操作影响。
DirectByteBuffer是相当于固定的内核buffer还是JVM进程内的堆外内存? 不管是Java堆还是直接内存,都是JVM进程通过malloc申请的内存,其都是用户空间的内存,只不过是JVM进程将这两块用户空间的内存用作不同的用处罢了
将HeapByteBuffer的数据拷贝到DirectByteBuffer这一过程是操作系统执行还是JVM执行? 在问题2中已经回答,DirectByteBuffer是JVM进程申请的用户空间内存,其使用和分配都是由JVM进程管理,因此这一过程是JVM执行的.也正是因为JVM知道堆内存会经常GC,数据地址经常移动,而底层通过write,read,pwrite,pread等函数进行系统调用时,需要传入buffer的起始地址和buffer count作为参数,因此JVM在执行读写时会做判断,若是HeapByteBuffer,就将其拷贝到直接内存后再调用系统调用执行步骤2. 代码在sun.nio.ch.IOUtil.write()和sun.nio.ch.IOUtil.read()中,我们看下write()的代码:
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if (var1 instanceof DirectBuffer) {
return writeFromNativeBuffer(var0, var1, var2, var4);
} else {
int var5 = var1.position();
int var6 = var1.limit();
assert var5 <= var6;
int var7 = var5 <= var6 ? var6 - var5 : 0;
ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
int var10;
try {
var8.put(var1);
var8.flip();
var1.position(var5);
int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
if (var9 > 0) {
var1.position(var5 + var9);
}
var10 = var9;
} finally {
Util.offerFirstTemporaryDirectBuffer(var8);
}
return var10;
}
}
如果是直接内存,直接调用底层直接写入,否则, 创建一个临时直接内存。存入数据,发送完毕后,再释放掉 为什么在执行网络IO或者文件IO时,一定要通过堆外内存呢? 如果是使用DirectBuffer就会少一次内存拷贝。如果是非DirectBuffer,JDK会先创建一个DirectBuffer,再去执行真正的写操作。这是因为,当我们把一个地址通过JNI传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效。然而,在GC管理下的对象是会在Java堆中移动的。也就是说,有可能我把一个地址传给底层的write,但是这段内存却因为GC整理内存而失效了。所以我必须要把待发送的数据放到一个GC管不着的地方。这就是调用native方法之前,数据一定要在堆外内存的原因。可见,DirectBuffer并没有节省什么内存拷贝,只是因为HeapBuffer必须多做一次拷贝,才显得DirectBuffer更快一点而已。
在将数据写到文件的过程中需要将数据拷贝到内核空间吗? 需要.在步骤3中,是不能直接将数据从直接内存拷贝到文件中的,需要将数据从直接内存->内核空间->文件,因此使用DirectByteBuffer代替HeapByteBuffer也只是减少了数据拷贝的一个步骤,但对性能已经有提升了.
使用堆外内存的优点
- 减少了垃圾回收 因为垃圾回收会暂停其他的工作。
- 加快了复制的速度 堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
使用DirectByteBuffer的注意事项
同样任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。 java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。
通道
1)、介绍
- 通道(channel):由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
- 通道用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。
2)、主要实现类 java.nio.channels.Channel 包下:
- FileChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
3)、获取通道 1、Java 针对支持通道的类提供了 getChannel() 方法 本地 IO: FileInputStream/FileOutputStream RandomAccessFile
网络 IO: Socket ServerSocket DatagramSocket
以上几个类都可以通过调用 getChannel() 方法获取通道
2、在 JDK1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
3、在 JDK1.7 中的 NIO.2 的 Files 工具类的 newByteChannel() 方法
通道数据传输和内存映射文件(零拷贝)
1)、使用通道完成文件的复制(非直接缓冲区)
public static void test1() throws Exception {
// 利用通道完成文件的复制(非直接缓冲区)
FileInputStream fis = new FileInputStream("a.txt");
FileOutputStream fos = new FileOutputStream("b.txt");
// 获取通道
FileChannel fisChannel = fis.getChannel();
FileChannel foschannel = fos.getChannel();
// 通道没有办法传输数据,必须依赖缓冲区
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 将通道中的数据存入缓冲区中
while (fisChannel.read(byteBuffer) != -1) { // fisChannel 中的数据读到 byteBuffer 缓冲区中
byteBuffer.flip(); // 切换成读数据模式
// 将缓冲区中的数据写入通道
foschannel.write(byteBuffer);
byteBuffer.clear(); // 清空缓冲区
}
foschannel.close();
fisChannel.close();
fos.close();
fis.close();
}
2)、使用直接缓冲区完成文件的复制(内存映射文件(零拷贝方式)) 方式一:
public static void test2() throws Exception {
// 使用直接缓冲区完成文件的复制(内存映射文件)
/**
* 使用 open 方法来获取通道
* 需要两个参数
* 参数1:Path 是 JDK1.7 以后给我们提供的一个类,代表文件路径
* 参数2:Option 就是针对这个文件想要做什么样的操作
* --StandardOpenOption.READ :读模式
* --StandardOpenOption.WRITE :写模式
* --StandardOpenOption.CREATE :如果文件不存在就创建,存在就覆盖
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("c.txt"), StandardOpenOption.WRITE,
StandardOpenOption.READ, StandardOpenOption.CREATE);
/**
* 内存映射文件
* 这种方式缓冲区是直接建立在物理内存之上的
* 所以我们就不需要通道了
*/
MappedByteBuffer inMapped = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapped = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMapped.limit()];
inMapped.get(dst); // 把数据读取到 dst 这个字节数组中去
outMapped.put(dst); // 把字节数组中的数据写出去
inChannel.close();
outChannel.close();
}
方式二:
public static void test3() throws Exception {
/**
* 通道之间的数据传输(直接缓冲区的方式)
* transferFrom
* transferTo
*/
FileChannel inChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("d.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
// 或者可以使用下面这种方式
//outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
selector
概念
Selector是NIO中最为重要的组件之一,我们常常说的多路复用器就是指的Selector组件。Selector组件用于轮询一个或多个NIO Channel的状态是否处于可读、可写。通过轮询的机制就可以管理多个Channel,也就是说可以管理多个网络连接。
轮询机制
- 首先,需要将Channel注册到Selector上,这样Selector才知道需要管理哪些Channel
- 接着Selector会不断轮询其上注册的Channel,如果某个Channel发生了读或写的时间,这个Channel就会被Selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的IO操作。
属性操作
1.创建Selector 通过open()方法,我们可以创建一个Selector对象。
Selector selector = Selector.open();
2.注册Channel到Selector中 我们需要将Channel注册到Selector中,才能够被Selector管理。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
某个Channel要注册到Selector中,那么该Channel必须是非阻塞,所有上面代码中有个configureBlocking()的配置操作。 在register(Selector selector, int interestSet)方法的第二个参数,标识一个interest集合,意思是Selector对哪些事件感兴趣,可以监听四种不同类型的事件:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << ;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
- Connect事件 :连接完成事件( TCP 连接 ),仅适用于客户端,对应 SelectionKey.OP_CONNECT。
- Accept事件 :接受新连接事件,仅适用于服务端,对应 SelectionKey.OP_ACCEPT 。
- Read事件 :读事件,适用于两端,对应 SelectionKey.OP_READ ,表示 Buffer 可读。
- Write事件 :写时间,适用于两端,对应 SelectionKey.OP_WRITE ,表示 Buffer 可写。
Channel触发了一个事件,表明该时间已经准备就绪:
- 一个Client Channel成功连接到另一个服务器,成为“连接就绪”
- 一个Server Socket准备好接收新进入的接,称为“接收就绪”
- 一个有数据可读的Channel,称为“读就绪”
- 一个等待写数据的Channel,称为”写就绪“
- 当然,Selector是可以同时对多个事件感兴趣的,我们使用或运算即可组合多个事件:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Selector其他一些操作
选择Channel
public abstract int select() throws IOException;
public abstract int select(long timeout) throws IOException;
public abstract int selectNow() throws IOException;
当Selector执行select()方法就会产生阻塞,等到注册在其上的Channel准备就绪就会立即返回,返回准备就绪的数量。 select(long timeout)则是在select()的基础上增加了超时机制。selectNow()立即返回,不产生阻塞。 有一点非常需要注意: select 方法返回的 int 值,表示有多少 Channel 已经就绪。 自上次调用select 方法后有多少 Channel 变成就绪状态。如果调用 select 方法,因为有一个 Channel 变成就绪状态则返回了 1 ; 若再次调用 select 方法,如果另一个 Channel 就绪了,它会再次返回1。
获取可操作的Channel
Set selectedKeys = selector.selectedKeys();
当有新增就绪的Channel,调用select()方法,就会将key添加到Set集合中。
代码示例
前面铺垫了这么多,主要是想让大家能够看懂NIO代码示例,也方便后续大家来自己手写NIO 网络编程的程序。创建NIO服务端的主要步骤如下:
1. 打开ServerSocketChannel,监听客户端连接
2. 绑定监听端口,设置连接为非阻塞模式
3. 创建Reactor线程,创建多路复用器并启动线程
4. 将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
5. Selector轮询准备就绪的key
6. Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路
7. 设置客户端链路为非阻塞模式
8. 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
9. 异步读取客户端消息到缓冲区
10.对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
11.将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端
NIOServer.java :
public class NIOServer {
private static Selector selector;
public static void main(String[] args) {
init();
listen();
}
private static void init() {
ServerSocketChannel serverSocketChannel = null;
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NioServer 启动完成");
} catch (IOException e) {
e.printStackTrace();
}
}
private static void listen() {
while (true) {
try {
selector.select();
Iterator keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next();
keysIterator.remove();
handleRequest(key);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
private static void handleRequest(SelectionKey key) throws IOException {
SocketChannel channel = null;
try {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
channel = serverSocketChannel.accept();
channel.configureBlocking(false);
System.out.println("接受新的 Channel");
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
if (count > 0) {
System.out.println("服务端接收请求:" + new String(buffer.array(), 0, count));
channel.register(selector, SelectionKey.OP_WRITE);
}
}
if (key.isWritable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("收到".getBytes());
buffer.flip();
channel = (SocketChannel) key.channel();
channel.write(buffer);
channel.register(selector, SelectionKey.OP_READ);
}
} catch (Throwable t) {
t.printStackTrace();
if (channel != null) {
channel.close();
}
}
}
}
NIOClient.java:
public class NIOClient {
public static void main(String[] args) {
new Worker().start();
}
static class Worker extends Thread {
@Override
public void run() {
SocketChannel channel = null;
Selector selector = null;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT);
channel.connect(new InetSocketAddress(9000));
while (true) {
selector.select();
Iterator keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next();
keysIterator.remove();
if (key.isConnectable()) {
System.out.println();
channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) {
channel.finishConnect();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("你好".getBytes());
buffer.flip();
channel.write(buffer);
}
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len > 0) {
System.out.println("[" + Thread.currentThread().getName()
+ "]收到响应:" + new String(buffer.array(), 0, len));
Thread.sleep(5000);
channel.register(selector, SelectionKey.OP_WRITE);
}
}
if(key.isWritable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("你好".getBytes());
buffer.flip();
channel = (SocketChannel) key.channel();
channel.write(buffer);
channel.register(selector, SelectionKey.OP_READ);
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally{
if(channel != null){
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
打印结果:
// Server端
NioServer 启动完成
接受新的 Channel
服务端接收请求:你好
服务端接收请求:你好
服务端接收请求:你好
// Client端
[Thread-0]收到响应:收到
[Thread-0]收到响应:收到
[Thread-0]收到响应:收到