IO模型
IO模型可以根据是否是同步的分为同步和异步两种,POSIX的定义如下:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
An asynchronous I/O operation does not cause the requesting process to be blocked.
简单解释下,即同步IO将线程阻塞至IO完成,异步IO不阻塞线程。
但这里又有个问题,既然同步IO是阻塞线程至IO完成,为什么还分BIO和NIO。
这里所描述的IO不是IO的全过程,以socket为例,IO可分为两个阶段:准备阶段和就绪阶段。准备阶段是网卡还没有接收到数据,就绪阶段是网卡已经接收到数据了,但诗句还在内核空间中,用户线程还不能访问数据。
当数据在准备阶段时,NIO的线程并不会阻塞,只是不断轮询数据是否准备完毕;当数据在就绪阶段时,kernel需要将内核中的数据复制到用户空间中,此阶段是会阻塞线程的。
用《大型网站系统与Java中间件实践》中的话来区分下NIO和AIO
AIO和NIO的一个最大的区别是,NIO在有通知时可以进行相关操作,例如读或者写,而AIO在有通知时表示相关操作已经完成。
可以理解为,NIO是通知你某个socket已经准备好了,你自己去把要读或者写的数据处理一下,比如Java中的打开inputstream或者outputstream。而AIO是通知你,需要的数据已经处理好放在某个位置,想进行什么操作可以进行了。
同步阻塞IO BIO(Blocking IO)
同步阻塞模型是最形象的一种模型,也是初学者编程使用最多的模型。
利用上图对该模型加以说明,用户线程调用recvfrom()方法,线程阻塞,等待内核返回数据。内核需要经历两个阶段,准备数据和将数据拷贝到用户空间中。等两个阶段都结束了,用户线程才能解除阻塞状态。
使用Java简单实现BIO
public class SocketServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(52991);
System.out.println("正在监听本地端口:" + serverSocket.getLocalPort());
while (true) {
Socket client = serverSocket.accept();
System.out.println("新的连接" + client.getLocalSocketAddress());
InputStream inputStream = client.getInputStream();
byte[] buffer = new byte[1024];
inputStream.read(buffer);
OutputStream outputStream = client.getOutputStream();
outputStream.write(buffer);
}
}
}
public class SocketClient {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket()) {
socket.bind(new InetSocketAddress(52998));
socket.connect(new InetSocketAddress("localhost", 52991));
OutputStream outputStream = socket.getOutputStream();
byte[] buffer = "hello world".getBytes();
outputStream.write(buffer);
outputStream.flush();
InputStream inputStream = socket.getInputStream();
byte[] inputBuffer = new byte[1024];
inputStream.read(inputBuffer);
System.out.println(new String(inputBuffer));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
同步非阻塞IO NIO(Non-Blocking IO)
基于事件驱动思想,采用Reactor模式,是Java服务端系统采用比较多的方式,基于NIO实现了IO多路复用。
Reactor:将I/O事件分派给对应的Handler
Acceptor:处理客户端新连接,并分派请求到处理器链中
Handlers:执行非阻塞读/写任务
对于最基本的NIO模型, 在数据的准备阶段,用户线程是不阻塞的,循环使用recvfrom询问内核数据是否准备完毕。如果准备完毕,陷入阻塞,等待内核将数据复制进用户空间。
NIO的通知发生在动作之前,是在可写、可读的时候,Selector发现这些事件后调用Handler处理。
使用Java实现简单的NIO。
但是这段代码是有问题的,当client线程增加时,会产生ConcurrentModificationException,即还在遍历selector的keyset时,它就产生了修改。且client获得的数据不能正确显示。
public class SocketServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(52991));
Selector selector = Selector.open();
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int readyNum = selector.select();
System.out.println("已准备好的连接数:" + readyNum);
if (readyNum > 0) {
Set<SelectionKey> selectionKeys = selector.keys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel tmpSSC = (ServerSocketChannel) selectionKey.channel();
SocketChannel tmpSC = tmpSSC.accept();
tmpSC.configureBlocking(false);
tmpSC.register(selector, SelectionKey.OP_READ);
}
if (selectionKey.isConnectable()) {
System.out.println("建立连接");
}
ByteBuffer byteBuffer = ByteBuffer.allocate(15);
if (selectionKey.isReadable()) {
SocketChannel tmpSC = (SocketChannel) selectionKey.channel();
tmpSC.read(byteBuffer);
byte[] recvBytes = byteBuffer.array();
System.out.println("接收数据:" + new String(recvBytes));
byteBuffer.clear();
}
if (selectionKey.isWritable()) {
byte[] sendBytes = "data accepted".getBytes();
byteBuffer.put(sendBytes);
SocketChannel tmpSC = (SocketChannel) selectionKey.channel();
tmpSC.write(byteBuffer);
System.out.println("发送数据");
byteBuffer.clear();
}
// iterator.remove();
}
}
}
}
}
public class SocketClient {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
int localPort = 52998;
new Thread(() -> {
try (Socket socket = new Socket()) {
int random = (int)(Math.random() * 1000);
socket.bind(new InetSocketAddress(localPort + random));
socket.connect(new InetSocketAddress("localhost", 52991));
OutputStream outputStream = socket.getOutputStream();
byte[] buffer = "hello world".getBytes();
outputStream.write(buffer);
outputStream.flush();
InputStream inputStream = socket.getInputStream();
byte[] inputBuffer = new byte[20];
inputStream.read(inputBuffer);
System.out.println(new String(inputBuffer));
} catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
}
}
异步IO AIO(Asynchronous IO)
采用Proactor模式,该模式与Reactor模式大同小异主要区别在于,事件处理者发起一个异步读写请求,指定缓存地址、大小、回调函数之后,交给系统内核处理。
内核处理完成后通知用户线程。