一、传统IO:面向流
- 字节流和字符流:
- 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据。
- 字符流:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。
- 输出流和输入流:
- 输出流:从内存读出到文件。只能进行写操作。
- 输入流:从文件读入到内存。只能进行读操作。
- 节点流和处理流:
- 节点流:直接与数据源相连,读入或读出。
- 处理流:与节点流一块使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。
二、BIO
BIO就是传统的socket编程。每有一个客户端连入,服务端就需要另起线程为其服务,很容易造成资源枯竭。
三、NIO:面向缓冲区
Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)
-
buffer演示:
public static void main(String[] args) {
// 创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 添加一些数据到缓冲区中
String s = "Java3y";
byteBuffer.put(s.getBytes());
//切换成读模式
byteBuffer.flip();
// 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
byte[] bytes = new byte[byteBuffer.limit()];
// 将读取的数据装进我们的字节数组中
byteBuffer.get(bytes);
// 输出数据
System.out.println(new String(bytes, 0, bytes.length));
}
-
channel演示
// 1. 通过本地IO的方式来获取通道
FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单.md");
// 得到文件的输入通道
FileChannel inchannel = fileInputStream.getChannel();
// 2. jdk1.7后通过静态方法.open()获取通道
FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单2.md"), StandardOpenOption.WRITE);
复制文件
使用内存映射文件的方式实现文件复制的功能(直接操作缓冲区):
通道之间通过transfer()
实现数据的传输(直接操作缓冲区):
-
网络IO模型
阻塞I/O:
非阻塞I/O:
I/O多路复用:
在Linux下对文件的操作是利用文件描述符(file descriptor)来实现的。
在Linux下它是这样子实现I/O复用模型的:调用select/poll/epoll/pselect
其中一个函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
- 当用户进程调用了select,那么整个进程会被block;
- 而同时,kernel会“监视”所有select负责的socket;
- 当任何一个socket中的数据准备好了,select就会返回;
- 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程(空间)。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
IO多路复用对应的设计模式:Reactor模式
https://www.cnblogs.com/crazymakercircle/p/9833847.html
-
零拷贝
一、普通拷贝
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
- 程序使用read()系统调用,系统由用户态转换为内核态,磁盘中的数据由DMA(Direct memory access)的方式读取到内核读缓冲区(kernel buffer)。DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。
- 系统由内核态转为用户态,当程序要读的数据已经完全存入内核读缓冲区以后,**程序会将数据由内核读缓冲区,写入到用户缓冲区,**这个过程需要CPU参与数据的读写。
- 程序使用write()系统调用,系统由用户态切换到内核态,数据从用户缓冲区写入到网络缓冲区(Socket Buffer),这个过程需要CPU参与数据的读写。
- 系统由内核态切换到用户态,网络缓冲区的数据通过DMA的方式传输到网卡的驱动(存储缓冲区)中(protocol engine)
可以看到,普通的拷贝过程经历了四次内核态和用户态的切换(上下文切换),两次CPU从内存中进行数据的读写过程,这种拷贝过程相对来说比较消耗系统资源。
二、零拷贝
内存映射方式(NIO中的map()就是用的这种):
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
- mmap()系统调用首先会使用DMA的方式将磁盘数据读取到内核缓冲区,然后通过内存映射的方式,使用户缓冲区和内核读缓冲区的内存地址为同一内存地址,也就是说不需要CPU再讲数据从内核读缓冲区复制到用户缓冲区。
- 当使用write()系统调用的时候,cpu将内核缓冲区(等同于用户缓冲区)的数据直接写入到网络发送缓冲区(socket buffer),然后通过DMA的方式将数据传入到网卡驱动程序中准备发送。
可以看到这种内存映射的方式减少了CPU的读写次数,但是用户态到内核态的切换(上下文切换)依旧有四次(调用map()是两次,调用write()是两次),同时需要注意在进行这种内存映射的时候,有可能会出现并发线程操作同一块内存区域而导致的严重的数据不一致问题,所以需要进行合理的并发编程来解决这些问题。
sendfile()方式(NIO中的transfer()就是用的这种):
sendfile(socket, file, len);
- sendfile()系统调用也会引起用户态到内核态的切换,与内存映射方式不同的是,用户空间此时是无法看到或修改数据内容,也就是说这是一次完全意义上的数据传输过程。
- 从磁盘读取到内存是DMA的方式,从内核读缓冲区读取到网络发送缓冲区,依旧需要CPU参与拷贝,而从网络发送缓冲区到网卡中的缓冲区依旧是DMA方式。
依旧有一次CPU进行数据拷贝,两次用户态和内核态的切换操作,相比较于内存映射的方式有了很大的进步,但问题是程序不能对数据进行修改,而只是单纯地进行了一次数据的传输过程。
四、select,poll,epoll
https://www.cnblogs.com/aspirant/p/9166944.html
===========================
NIO:
https://juejin.im/post/5af942c6f265da0b7026050c
ZeroCopy:
https://blog.youkuaiyun.com/cringkong/article/details/80274148
https://www.jianshu.com/p/8c6b056f73ce