Java1.4开始,提供了新的非阻塞IO操作的API,用来替代Java BIO和网络编程相关的API。Java NIO包括三个核心组件:Buffer缓冲区、Channel通道、Selector选择器。
缓冲区本质上是一个可以写入数据的内存块(可以是数组),然后可以从中读取数据。此内存块包含在NIO Buffer对象中,该对象提供了一组较方便地使用内存块的方法,相比较直接对数组操作,BufferAPI更容易操作和管理。
Buffer有三个重要属性:
使用Buffer进行数据写入与读取,需要如下四个步骤:
buffer.compact()是另一种转为写模式的方法,但只会清除读取过的数据。
ByteBuffer提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现,使用allocateDirect获取堆外内存。
堆外内存的优点:
使用堆外内存时,要注意通过虚拟机参数MaxDirectMemorySize限制大小,防止机器内存耗尽。
传统的BIO中,数据发送接收基于OutputStream和InputStream,网络编程需要用到Socket建立连接与Stream数据读写两组API。Channel的API包含了网络和文件相关的操作:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel。Channel不仅可以建立连接,同时还实现了数据读写,在一个Channel中可以进行读取和写入操作,而Stream流通常是单向的(input、output)。Channel始终是读取和写入缓冲区Buffer,可以通过非阻塞的方式读写。
SocketChannel用于建立TCP连接
//SocketChannel客户端使用方式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("localhost", 8080));
while(!socketChannel.finishConnect()) {
Thread.yield();
}
ByteBuffer byteBuffer = ByteBuffer.wrap("hello".getBytes());
while(byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
int readNum = socketChannel.read(byteBuffer);
socketChannel.close();
由于是非阻塞的IO:
write方法可能在尚未写入任何数据时就返回,需要循环调用write;
read方法也可能直接返回而不读取任何数据,需要根据int值判断读取的数据量。
ServerSocketChannel可以监听新的TCP连接通道,用于创建网络服务端
//创建服务端示例
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
while(true) {
SocketChannel socketChannel = ssc.accept();
if(socketChannel != null) {
while(socketChannel.isOpen() && socketChannel.read(buffer)!= -1) {
}
buffer.flip();
byte[] content = new byte[buffer.limit()];
buffer.get(content);
System.out.println(new String(content));
}
}
在非阻塞模式下,accept如果没有连接,会立即返回null,所以必须检查socketChannel是否等于null。在这个示例中,使用的是单线程,虽然是非阻塞的IO模型,但是由于使用while循环一直判断状态,不能同时去处理其他连接请求。为了解决这个问题,需要对服务器端进行改进,可能有的人会想到使用多线程,对于每个请求开一个线程去处理,但是这样的话和BIO有什么区别呢?既然是非阻塞的,我们可以用其他的设计方式,用列表存储所有SocketChannel,没有新连接时,处理IO读写。
private static List socketChannels = new ArrayList<>();
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
while(true) {
SocketChannel socketChannel = ssc.accept();
if(socketChannel != null) {
socketChannel.configureBlocking(false);
System.out.println("接收到新的连接:" + socketChannel.getRemoteAddress());
socketChannels.add(socketChannel);
}else {
Iterator iterator = socketChannels.iterator();
while(iterator.hasNext()){
//TODO 读取/响应
}
}
}
}
通过这种方式,实现了一个线程可以处理多个请求。但是这种方式不管有没有数据,每次都要对整个SockerChannel列表进行轮询检查,这在高并发的场景下是十分低效的,因此Java提供了Selector选择器,也称为多路复用器。
Selector可以检查多个NIO通道,并确定哪些通道已经准备好进行读取或写入,实现单个线程对多个通道的管理。Selector基于事件驱动机制:非阻塞的网络通道下,通过Selector注册感兴趣的事件类型,当有事件发生时,Selector会收到通知(由操作系统多路复用机制支持,虽然名称是selector,但是用的可能是epoll模型)。使用Selector后。监听方式由监听Channel变成了监听事件,极大地提高了效率。
Selector可以监听四种类型的事件,对应SelectionKey的四个常量:
OP_CONNECT:连接
OP_ACCEPT:准备就绪
OP_READ:可读
OP_WRITE:可写
//创建一个Selector
Selector selector = Selector.open();
//创建服务端,并且注册到Selector上
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
//ServerSocketChannel只支持ACCEPT事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器启动成功");
while(true){
//不再轮询Channel,select有阻塞效果,直到有感兴趣的事件发生才返回
selector.select();
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
//关注read和accept
if(selectionKey.isAcceptable()){
//将客户端连接注册到selector,感兴趣事件为read
SocketChannel socketChannel = ((ServerSocketChannel)selectionKey.channel()).accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("收到新连接:" + socketChannel.getRemoteAddress());
}
if(selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen() && socketChannel.read(buffer) != -1){
//TODO d读取/响应
}
buffer.flip();
byte[] content = new byte[buffer.limit()];
buffer.get(content);
System.out.println("接收到数据来自:" + socketChannel.getRemoteAddress());
System.out.println(new String(content));
selectionKey.cancel();
}
}
}
通过selector的3个方法select(阻塞选择,直到有关心的事件发生时退出阻塞),selectNow(不阻塞选择),select(long)(指定超时选择,超时到达或者有关心事件发生时退出阻塞),来获取关心事件的发生。其执行步骤分为以下3步:
1、将存在于canceled-key set中的key从所有的key set中移除,撤销注册的channel,清空canceled-key set。
2、底层操作系统检查是否有关心的事件发生,当有关心的事件发生时,首先检查channel的key是否已经存在于selected-key set中,如果不存在,则将其加入到selected-key set中去,同时修改key的ready-operation set来表明当前ready的操作,而以前存在于ready-operation set中的信息会被删除。如果对应的key已经存在于selected-key set中,这直接修改其ready-operation set来表明当前ready的操作,删除原来ready-operation set中的信息。
3、如果在第二步中有加入到canceled-key set中的key,在这一步会执行第一步的操作。
selector自身是线程安全的,而他的key set却不是。在一次选择发生的过程中,对于key的关心事件的修改要等到下一次select的时候才会生效。 另外,key和其代表的channel有可能在任何时候被cancel和close。因此存在于key set中的key并不代表其key是有效的,也不代表其channel是open的。如果key有可能被其他的线程取消或关闭channel,程序必须小心的同步检查这些条件。
在实际生产环境中,高并发、海量连接的情况下,使用一个线程处理所有的请求是不现实的。单个线程不仅资源利用率低,没有充分发挥服务器多核的特性,而且效率不高,无法及时处理请求。Doug Lea 在《Scalable IO in Java》中提出了一种NIO与多线程结合的方案,Reactor模式。
单Reactor模式:Reactor接收请求,分发给线程池处理请求。单Reactor模式下,将网络处理和耗时业务处理分开,一定程度上提升了效率。但是网络IO部分的Reactor仍然是单线程的,IO处理仍然会成为性能瓶颈。
多Reactor模式:将Reactor分为mainReactor和subReactor,mainReactor负责处理连接请求,注册给subReactor,subReactor可以是多个,让subReactor处理IO事件,业务处理仍然放在单独的线程里执行。