1.2 Unix中IO的五种模型:
以网络IO为例:
当客户端发送的网络包经过路由器和交换器的转发后到达对应服务端的网络适配器(网卡),并存储在对应网络I/O的套接字文件中,然后操作系统会将该文件中的数据一般通过 DMA 复制到内存中供应用程序使用;
在 Unix网络编程 这本书中概述了完成上述操作的几种模型:
- 首先解释两组名词: 这两组名词其实只是对同一个场景的两种不同的描述方式:
(1)阻塞与非阻塞:阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成后CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。
(2)同步与非同步:同步与非同步主要是从程序方面来说的,同步指程序发出一个功能调用后,没有结果前不会返回。非同步是指程序发出一个功能调用后,程序便会返回,然后再通过回调机制通知程序进行处理。
- 同步阻塞IO(BIO):
注意,此时客户端与服务端已经通过三次握手建立了连接,即可以通过套接字文件进行数据的交换,所以在此模型下的服务端的用户进程阻塞在 recvfrom方法
等待客户端发送的数据发送到内存并返回;
这个模型最大的问题就是操作系统中最典型的CPU速度与外设速度不匹配的问题,网络适配器的速度相对于CPU的速度是极慢的,并且此时CPU却一直在阻塞。
- 同步非阻塞IO:
当用户线程调用 recvfrom 方法
后,如果此时套接字文件还没有准备好,则直接返回一个错误信息,然后CPU就会去做其他事情,而该线程会不断获取CPU时间片进行轮询,所以该模式下虽然是非阻塞,但其线程切换确实很频繁的,所以通过该方式增加的CPU使用时间与线程切换的成本还是需要好好评估的;
并且当数据准备好后,并且线程获取到时间片再次调用 recvfrom 时,线程还是需要等待数据拷贝至内存的。
- 多路复用IO:(Java NIO原理)该模型通过一个方法 select ,该方法一直会阻塞到IO事件的到来(即套接字文件准备好)再返回,这个时候我们再调用 recvfrom方法 就只需要等待数据拷贝至内存即可;并且 select方法 可以监听多个事件,所以联系到Java NIO中时,就是多个线程可以向同一个 Selector注册多个事件,从而达到了多路复用的效果。
- 异步IO(AIO):
该模型通过操作系统提供的异步IO方法 aio_read
,应用程序调用后便直接返回,并且不需要像前几种模型一样需要等待数据拷贝至内存;
但其内在的实现还是很复杂的,底层还是使用BIO实现的,就不展开描述了,因为对编程人员好像并没有太大的作用。
- 信号驱动IO:
其实笼统点讲,AIO和多路复用IO其实也是某种信号进行驱动的IO,即都不需要应用程序阻塞在 网络适配器(网卡)的数据准备好的这个过程中 ,而都是通发出种信号进行通知应用程序,虽然信号的实现方式或是用 select
或是用更底层的方式,但本质上还是很相似的;但信号驱动IO也是需要线程等待数据拷贝至用户空间的。
二、Java BIO:
2.1 简介:
注:《深入理解计算机系统》中定义,Linux将所有外设抽象成文件,与外设的通信被抽象成文件的读写;而网络也只是外设的一种;客户端与服务器端建立连接时互相交换了彼此的文件描述符,之后两端进行通信即为向这两个文件描述符对应的套接字文件中写值
Java中的Socket是对进行通信的两端的抽象,其封装了一系列TCP/IP层面的底层操作; 代码如下:
- 客户端:
//通过一个IP:PORT套接字新建一个Socket对象,确定要连接的服务器的位置和端口
Socket socket = new Socket(\"127.0.0.1\", 8089);
//通过Socket对象拿到OutputStream,可以将其理解通过其向服务器端对应的套接字文件写入数据
OutputStream outputStream = socket.getOutputStream();
//使用默认的字符集去解析outputStream的字节流
PrintWriter printWriter = new PrintWriter(outputStream, true);
/*向服务器发送一个HTTP1.1的请求*/
printWriter.println(\"GET /index.html HTTP/1.1\");
printWriter.println(\"Host: localhost:8080\");
printWriter.println(\"Connection Close\");
printWriter.println();
- 服务端:
//ServerSocket在该套接字上监听连接事件
ServerSocket serverSocket = new ServerSocket(8089, 1, InetAddress.getByName(\"127.0.0.1\"));
//服务端阻塞在accept()方法上,直到客户端的connect()请求,并返回一个Socket对象
socket = serverSocket.accept();
//从返回的Socket对象中获取该Socket对应的套接字文件的内容并进行读取
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
int i = 0;
while (i != -1) {
i = bufferedReader.read();
System.out.println(\"拿到的数据为:\"+(char)i);
}
socket.close();
其实Java BIO 即为对系统提供的网络I/O方法的封装;
2.2 Java BIO 带来的问题:
我们一般都是适用 Acceptor模型来进行BIO服务端的创建 ,即通过一个 ServerSocket() 监听来自客户端的连接,然后通过三次握手建立连接后便会创建一个子线程并通过线程池进行相应的逻辑处理;
而上述逻辑带来了一系列问题:
backlog
三、 Java NIO:
3.1 与BIO对比,改变了什么,又为什么要这么改变?
图片及实例代码参考来自: juejin.im/post/5d1acd…
- Java NIO 通过多路复用IO的模型实现了单个 Selector线程 管理了多个连接,解决了BIO最致命的一个问题;
- 无论是 In/OutputStream 还是Java NIO中的通道 channel 本质上都是对网络I/O文件的抽象,与前者不同, channel 是双通道的,既可以读又可以写。
所以按照 I/O多路复用 的模型,当 channel 中的数据准备好了的时候会返回一个可读的事件,并且通过selector进行处理,安排相应的Socket进行相应数据的读取,这是一个数据可读的事件,而Selector可监听的事件有四种:
SelectionKey.OP_CONNECT // 连接事件
SelectionKey.OP_ACCEPT //接收事件
SelectionKey.OP_READ //数据可读事件
SelectionKey.OP_WRITE //可写事件
- 为什么要引入Buffer机制? 在BIO的时候我们一般是通过类似于 socket.getInputStream.write() 方法来直接进行读写的,而NIO中向 channel 中写入数据必须从buffer中获取,而 channel 也只能向buffer写入数据,这样使得这样的操作更为接近操作系统执行I/O的方式;细一点讲,是因为在向 OutputStream中write() 数据即为向接收方Socket对象中的 InputStream 中的RecvQ队列中,而如果 write() 的数据大于队列中每个数据对象限定的长度,就需要进行拆分,而这个过程,我们是不可以控制的,而且涉及到用户空间与内核空间地址的转换;但是当我们使用Buffer后,我们可以控制Buffer的长度,是否扩容以及如何扩容我们都可以掌握。 参考文章: www.ibm.com/developerwo…
3.2 我们来看一段实例代码(服务端):
/**
* @CreatedBy:CVNot
* @Date:2020/2/21/15:30
* @Description:
*/
public class NIOServer {
public static void main(String[] args) {
try {
//创建一个多路复用选择器
Selector selector = Selector.open();
//创建一个ServerSocket通道,并监听8080端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//监听接收数据的事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
//拿到Selector关心的已经到达事件的SelectionKey集合
Set keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = (SelectionKey)iterator.next();
iterator.remove();
//因为我们只注册了ACCEPT事件,所以这里只写了当连接处于这个状态时的处理程序
if(selectionKey.isAcceptable()){
//拿到产生这个事件的通道
ServerSocketChannel serverChannel = (ServerSocketChannel)selectionKey.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
//并为这个通道注册一个读事件
clientChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
}
else if(selectionKey.isReadable()){
SocketChannel clientChannel = (SocketChannel)selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
long bytesRead = clientChannel.read(byteBuffer);
while(bytesRead > 0){
byteBuffer.flip();
System.out.printf(\"来自客户端的数据\" + new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
byteBuffer.clear();
byteBuffer.put(\"客户端你好\".getBytes(\"UTF-8\"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
/**
* @CreatedBy:CVNot
* @Date:2020/2/21/16:06
* @Description:
*/
public class NIOClient {
public static void main(String[] args) {
try {
Selector selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(8080));
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
//如果事件没到达就一直阻塞着
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()) {
/**
* 连接服务器端成功
*
* 首先获取到clientChannel,然后通过Buffer写入数据,然后为clientChannel注册OP_READ事件
*/
clientChannel = (SocketChannel) key.channel();
if (clientChannel.isConnectionPending()) {
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
byteBuffer.put(\"服务端你好,我是客户端\".getBytes(\"UTF-8\"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
//通道可以读数据
clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead > 0) {
byteBuffer.flip();
System.out.println(\"server data :\" + new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
} else if (key.isWritable() && key.isValid()) {
//通道可以写数据
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}