一.BIO(Blocking IO)(同步阻塞IO)
(1)从Java启动IO读的read系统调用开始,用户线程就进入阻塞状态。
(2)当系统内核收到read系统调用,就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这个时候内核就要等待。
(3)内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。
(4)直到内核返回后,用户线程才会解除阻塞的状态,重新运行起来。总之,阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了。
1.传统BIO
一请求一应答,前一个请求没有处理结束,其他请求阻塞。
Java应用程序从IO系统调用开始,直到系统调用返回,在这段时间内,Java进程是阻塞的。返回成功后,应用进程开始处理用户空间的缓存区数据。
2.伪BIO
N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),前m个没有处理结束,后边的n-m阻塞。
基于线程池的模拟实现
客户端:每隔两秒像3333端口发起一次请求,并传输时间戳+":hello world" 的字符串
public static void bioClient() {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(1,5,2,
TimeUnit.SECONDS,new LinkedBlockingQueue<>(1));
// TODO 创建多个线程,模拟多个客户端连接服务端
threadPoolExecutor.execute(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
System.out.println("Error");
}
}
} catch (IOException e) {
System.out.println("Error");
}
});
}
服务端:基于线程池实现,每次支持并行处理5个客户端请求,超过的部分阻塞。
public class BIOServer {
private static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 20, 2,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));
public static void bioServer()
{
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(3333);
} catch (IOException e) {
e.printStackTrace();
}
final ServerSocket finalServerSocket = serverSocket;
Socket socket = null;
try {
//监听请求
socket = finalServerSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
final Socket finalSocket = socket;
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
poolExecutor.execute(()->{
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = finalSocket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
System.out.println("Error");
}
});
}
}
3.总结
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
二.NIO(Non-blocking IO)(同步非阻塞)
(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户线程需要不断地发起IO系统调用。
(2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。
(3)用户线程读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户进程需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。
同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。
同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。
三.NIO(New IO)(IO多路复用)
1.NIO和IO的主要区别
NIO流是非阻塞的,IO流是阻塞的
使用Java NIO可以进行非阻塞式的IO操作,比如,单线程中从通道读取数据到buffer,同时可以继续处理别的事情,当数据读取到buffer后,线程再处理数据。当然写数据也一样,一个线程请求写入一些数据到通道,那么不需要等到完全写入,这个线程同时可以处理其他的事情。
Java IO的各种流是阻塞的。当一个线程调用read()或write()时,该线程阻塞,直到数据完全获取到,或者完全写入,该线程才能去处理其他事情。
2.NIO核心组件
(1)Buffer:IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)
(2)Channel(通道):NIO 通过Channel(通道) 进行读写
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
(3)Selector(选择器):选择器用于使用单个线程处理多个通道
在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。
在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。
举个例子来说明IO多路复用模型的流程。发起一个多路复用IO的read读操作的系统调用,流程如下:(1)选择器注册。在这种模式中,首先,将需要read操作的目标socket网络连接,提前注册到select/epoll选择器中,Java中对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。
(2)就绪状态的轮询。通过选择器的查询方法,查询注册过的所有socket连接的就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中。当用户进程调用了select查询方法,那么整个线程会被阻塞掉。
(3)用户线程获得了就绪状态的列表后,根据其中的socket连接,发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。
IO多路复用模型的特点:IO多路复用模型的IO涉及两种系统调用(System Call),另一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。
IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。