对程序语言设计者来说,设计一个令人满意的io系统,是件极艰难的任务。java io中最为核心的概念是流(Stream),面向流的编程。java中,一个流要么是输入流,要么是输出流,不可能同时既是输入流,又是输出流。传统的io受限制于并发数。
JDK1.4的java.nio.*包中引入了新的java io类库,其目的在于提高速度。事实上,速度的提高来自于所使用的结构更接近于操作系统执行io的方式:通道和缓冲区。
nio是同步非阻塞的I/O模型。
io复用机制需要用到事件分发器。事件分发器的作用是将那些读写事件源分发给各读写事件的处理者。开发人员需要在分发器注册感兴趣的事件(操作系统没有能力处理的事件,如对于写操作,就是写不进数据时会对写事件感兴趣),并提供相应的处理者,或者是回调函数,事件分发器在适当的时候会将这些时间分发给处理者或者回调函数。涉及到事件分发器的两种模式分别为:Reactor和Proactor,Reactor是基于同步io的,Proactor是基于异步io的
在Reactor中实现读:
- 注册读就绪事件和相应的事件处理器
- 事件分发器等待事件
- 事件到来,激活事件分发器,分发器调用事件相应的处理器
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权
在Proactor中实现读:
- 处理器发出异步读操作,这种情况下,处理器无视io就绪事件,只关注完成事件
- 事件分发器等待操作完成事件
- 在分发器等待的过程中,操作系统利用并行的内核线程执行相应的操作,并将结果数据存入用户自定义的缓冲区,然后通知事件分发器读操作完成。
- 事件分发器呼唤处理器
- 事件处理器处理用户自定义缓冲区的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。
java nio中拥有3个核心概念:Selector,Channel与buffer,在java nio中,我们是面向块或者缓冲区来编程的。nio中的buffer和channel是同时存在的两种属性,一般是通过channel读数据到buffer或者通过channel将buffer中的数据写到文件。
buffer
buffer是一种特定类型的容器。是一个线性的、有限长度的一个特定类型的元素序列。
buffer本身就是一种内存,底层实现上,它实际上是个数组,数据的读和写都是通过buffer来实现的。
除了数组之外,buffer海提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。
java中的7种原生数据类型都有各自对应的buffer类型,没有BooleanBuffer类型。
除了它本身的内容,buffer的基本属性是position、limit、capacity。
nio中三个状态属性的含义:
- position 一个buffer的position是指下一个需要被读或者写的元素的索引。一个buffer的position是不为负并且大小不超过limit
- limit 一个buffer中的limit属性指的是第一个不应该被读或者写的元素的索引,每个buffer的limit是不为负的并且大小不超过capacity。
- Capacity buffer所包含元素的个数,每个buffer的capacity是不为负和不可改变的,capacity的大小一般在分配buffer时通过方法
ByteBuffer.allocate(512)
或者ByteBuffer.allocateDirect(512)
创建,其中传人的参数512就是capacity的大小。
相对操作读或者写操作从position 的索引位置开始,position增加的大小就是读或者写操作的元素个数。如果一个请求传递的数据超过了limit限制,那么相对于get操作会抛出BufferUnderflowException
异常,相对于put操作会抛出BufferOverflowException
异常。
绝对操作直接获取明确的索引,不会影响position的值。如果索引超出了限制,那么绝对操作的put和get方法将会抛出IndexOutOfBoundsException
异常。
如下代码片段,首先定义了一个IntBuffer
,分配的底层数组大小为10,随机向buffer中添加数据,然后调用
buffer.flip()
,将buffer中的数据读取并打印出来。
import java.nio.IntBuffer;
import java.security.SecureRandom;
public class NioTest1 {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
for(int i=0;i<buffer.capacity();i++){
System.out.println(i+"===");
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
buffer.flip();
while (buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}
marking and resetting
一个buffer的mark是指当reset
方法执行时,position需要被重置的索引。mark不是总是被定义,但是一旦被定义,它将不会为负值并且不会大于position值,如果将position或者limit的值调整为一个比mark小的值,那么已被定义的mark值将会被抛弃。如果mark值没定义就执行reset
方法,那么将会产生 InvalidMarkException
异常。
四者之间存在如下关系:
0<=mark<=position<=limit<=Capacity
一个新创建的buffer总是会有一个为0的position和一个为定义的mark。初始时limit可以为0,或者说是其他某些取值,取决于缓冲区的类型和构造方式。
除了访问位置,限制和容量值以及标记和重置的方法之外,此类还在缓冲区上定义了以下操作:
clear()
使buffer为一个新的序列的读取或者put的相对操作做准备:它将limit设置为capacity大小,将position设置为0。
flip()
为一个新序列的写或者get的相对操作准备一个缓冲区:他将limit设置为当前position所在的索引,然后将position设置为0。
rewind()
使一个缓冲区做好准备重新读取它已经包含的数据:它不改变limit的值,将position的值设置为0。
read-only buffers
所有的buffer都是可读的,但并不是所有的buffer都是可写的。每个缓冲区上的变异的方法都是被指定为可选的操作,如果执行该操作会抛出ReadOnlyBufferException
异常。一个read-only的buffer不允许它内容被改变,但是它的mark,position,limit的值是不确定的。查看一个buffer是否是read-only可以执行它的isReadOnly
方法。
通过nio读取文件涉及三个步骤:
1.从FileInputStream货渠道FileChannel对象
2.创建buffer
3.将数据从Channel读取到buffer
绝对方法对相对方法对含义
1.相对方法:limit值与position值会在操作时被考虑到
2.绝对方法:完全忽略掉limit值与position值
channel
一个channel代表的是一个与实体间的开放连接,比如外部设备,文件,网络套接字或者能执行一个或者多个io操作的组件,例如读和写。channel要么是开放的要么是关闭的。channel是指可以向其写入数据或者从中读取数据的对象,它类似于java io中的stream。
所有数据的读写都是通过buffer来进行的,永远不会出现直接向channel写入数据的情况,或是直接从channel读取数据的情况。
与stream不同的是,channel是双向的,一个流只可能是InputStream或者是OutputStream,channel打开后则可以进行读取,写入或是读写。
由于channel是双向的,因此他能更好地反映出底层操作系统的真实情况;在linux系统中,底层操作系统的通道就是双向的。
如下代码片段,定义了一个输入流,读取文件,然后根据该输入流获取到channel,再定义了一个大小为512的byteBuffer
,将channel中的数据写入buffer中,调用fileChannel.read(byteBuffer)
方法。
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioTest2 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("Niotest2.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
fileChannel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
byte b = byteBuffer.get();
System.out.println("Character: " + (char) b);
}
fileInputStream.close();
}
}
Selector
nio采用的是多路复用io,而Selector则是nio中多路复用的实现。
Selector的创建可以通过调用open()
方法,它将会使用系统默认的选择器提供者创建选择器。也可以采用自定义选择器提供者的openSelector()
方法来创建。
一个自定义的选择器。在使用close()
方法关闭选择器之前,它将一直处于开放状态。
通过SelectionKey
对象来表示可选择通道到选择器的注册。它维护了三种选择键集:
keys
包含的键表示当前channel到此选择器的注册。此集合由keys
方法返回。(不能直接被移除,一个键只有被取消并且被注销以后才能被移除)selected-key
表示一种在前一次选择操作期间,检测每个键的channel是否已经至少为该键的相关操作所表示的一个操作准备就绪的集合。此集合由selectedKeys方法返回。selected-key
始终是keys的一个子集。(可以被移除,不能直接被添加)cancelled-key
是被已取消但是channel尚未注销的键的集合。不可直接访问此集合。cancelled-key
始终是keys的一个子集。
在新创建的集合中,三种集合都为空
通过某个channel的register
方法注册该channel时,会向选择器的键集中添加一个键。在选择操作期间从键集移除已取消的键。键集本身是不可修改的。
不管是通过关闭某个键的channel还是通过调用该键的cancel
方法来取消键,该键都会被添加到cancelled-key
中去。取消某个键会导致在下一次操作期间
注销该键的channel,而注销时将会从所有的键集中移除该键。
在每次选择器操作期间,都可以将键添加到selected-key或者从中移除,并且可以从keys或者cancelled-key中将其移除。
选择是由 select()、select(long) 和 selectNow() 方法执行的,执行涉及三个步骤:
-
将已取消键集中的每个键从所有键集中移除,并注销其通道。此步骤使已取消的键集成为空集。
-
在开始进行选择操作时,应查询基础操作系统来更新每个剩余通道的准备就绪信息,以执行由其键的相关集合所标识的任意操作。
对于已为至少一个这样的操作准备就绪的通道,执行以下两种操作之一:
- 如果该通道的键尚未在selected-key中,则将其添加到该集合中,并修改其准备就绪操作集,以准确地标识那些通道现在已报告为之准备就绪的操作。 丢弃准备就绪操作集中以前记录的所有准备就绪信息。
- 如果该通道的键已经在selected-key中,则修改其准备就绪操作集,以准确地标识所有通道已报告为之准备就绪的新操作。保留准备就绪操作集以前记录的所有准备就绪信息;换句话说,基础系统所返回的准备就绪操作集是和该键的当前准备就绪操作集按位分开 (bitwise-disjoined) 的。
如果在此步骤开始时键集中的所有键都有空的相关集合,则不会更新已选择键集和任意键的准备就绪操作集。
-
如果在步骤 (2) 的执行过程中要将任意键添加到已取消键集中,则处理过程如步骤 (1)。
是否阻塞选择操作以等待一个或多个通道准备就绪,如果这样做的话,要等待多久,这是三种选择方法之间的唯一本质差别。
并发性
选择器自身可由多个并发线程安全使用,但是其键集并非如此。
选择操作在选择器本身上、在key set上和在selected-key set上是同步的,顺序也与此顺序相同。
在执行上面的步骤 (1) 和 (3) 时,它们在selected-key set也是同步的。
在执行选择操作的过程中,更改选择器键的相关集合对该操作没有影响;进行下一次选择操作才会看到此更改。
可在任意时间取消键和关闭通道。因此,在一个或多个选择器的键集中出现某个键并不意味着该键是有效的,也不意味着其通道处于打开状态。
如果存在另一个线程取消某个键或关闭某个通道的可能性,那么应用程序代码进行同步时应该小心,并且必要时应该检查这些条件。
阻塞在 select() 或 select(long) 方法之一中的某个线程可能被其他线程以下列三种方式之一中断:
通过调用选择器的 wakeup 方法,
通过调用选择器的 close 方法,或者
在通过调用已阻塞线程的 interrupt 方法的情况下,将设置其中断状态并且将调用该选择器的 wakeup 方法。
close 方法在选择器上是同步的,并且所有三个键集都与选择操作中的顺序相同。
一般情况下,选择器的键和已选择键集由多个并发线程使用是不安全的。
如果这样的线程可以直接修改这些键集之一,那么应该通过对该键集本身进行同步来控制访问。
这些键集的 iterator 方法所返回的迭代器是快速失败 的:如果在创建迭代器后以任何方式(调用迭代器自身的 remove 方法除外)修改键集,则会抛出 ConcurrentModificationException。
nio实现的客户端与服务器端
使用nio实现了客户端和服务器端的通信,当客户端向服务店请求连接成功时,服务器端会将连接信息打印出来,客户端会接收来自键盘的输入事件,将输入的内容发送给服务器端,服务器端收到以后会将其广播到每一个客户端,然后在客户端打印出来,
代码如下所示
server端代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class NioServer {
private static Map<String, SocketChannel> clientMap = new HashMap<>();
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(8899);
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//使用异步io
serverSocketChannel.configureBlocking(false);
//获取socket
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
//创建selector
Selector selector = Selector.open();
//将channel注册到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
//获取准备就绪的channel
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历注册到selector到selectionKey
selectionKeys.forEach(selectionKey -> {
final SocketChannel client;
try {
//accept类型的channel
if (selectionKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
//server请求等待
client = server.accept();
client.configureBlocking(false);
//将获取到的channel注册到selector
client.register(selector,SelectionKey.OP_READ);
String key = "[" + UUID.randomUUID().toString() + "]";
clientMap.put(key, client);
//read类型到channel
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将channel的内容读进buffer
int count = client.read(byteBuffer);
if (count > 0) {
byteBuffer.flip();
Charset charset = Charset.forName("utf-8");
String receivedMessage = String.valueOf(charset.decode(byteBuffer).array());
System.out.println(client + ": " + receivedMessage);
String sendKey = null;
for (Map.Entry<String, SocketChannel> entry : clientMap.entrySet()) {
if (client == entry.getValue()) {
sendKey = entry.getKey();
break;
}
}
for (Map.Entry<String, SocketChannel> entry : clientMap.entrySet()) {
SocketChannel value = entry.getValue();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((sendKey+": "+receivedMessage).getBytes());
writeBuffer.flip();
value.write(writeBuffer);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
selectionKeys.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
client端代码:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NioClient {
public static void main(String[] args) {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);//false表示非阻塞io
Selector selector = Selector.open();
//注册一个连接的selector
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8899));
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
if (selectionKey.isConnectable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
if (client.isConnectionPending()) {
client.finishConnect();
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put((LocalDateTime.now() + "连接成功").getBytes());
writeBuffer.flip();
client.write(writeBuffer);
ExecutorService executorService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
executorService.submit(() -> {
while (true) {
writeBuffer.clear();
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(inputStreamReader);
String sendMessage = br.readLine();
writeBuffer.put(sendMessage.getBytes());
writeBuffer.flip();
client.write(writeBuffer);
}
});
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int count = client.read(readBuffer);
if (count > 0) {
String receiveMessage = new String(readBuffer.array(), 0, count);
System.out.println(receiveMessage);
}
}
}
selectionKeys.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}