Administrator
Published on 2022-12-29 / 438 Visits
0
0

java网络编程模型

1. 同步阻塞IO模型

在Linux中,对于一次读取IO的操作,数据并不会直接复制到程序的缓冲区。通常包括两个不同阶段:

  1. 等待数据准备好,到达内核缓冲区。
  2. 从内核向进程复制数据。

对于一个套接字上的输入操作:

  • 第一步通常涉及等待数据从网络中到达。当所有等待数据报到达时,它被复制到内核中的某个缓冲区。
  • 第二步就是把数据从内核缓冲区复制到应用程序缓冲区。

同步阻塞IO模型是常用、简单的模型。在Linux中,默认情况下,所有套接字都是阻塞的。下面我们以阻塞套接字的recvfrom的调用图来说明阻塞。

图片

进程调用一个recvfrom请求,但是它不能立刻收到回复,直到数据返回。
然后将数据从内核复制到用户空间。在IO执行的两个阶段中,进程都处于阻塞状态,在等待数据返回的过程中不能做其他的工作,只能阻塞地等在那里。

  • 优点:简单,实时性高,响应及时,无延时。
  • 缺点:需要阻塞等待,性能差。

2. 同步非阻塞IO模型

与阻塞IO不同的是,非阻塞的recvfrom系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error(EAGAIN或EWOULDBLOCK)。进程在返回之后,可以处理其他的业务逻辑,过会儿再发起recvfrom系统调用。采用轮询的方式检查内核数据,直到数据准备好,再复制数据到进程,进行数据处理。在Linux下,可以通过设置套接字选项使其变为非阻塞。

图片-1672278513680

如图所示:
前3次调用recvfrom请求并没有数据返回,所以内核返回error(EWOULDBLOCK),并不会阻塞进程。

当第4次调用recvfrom时,数据已经准备好了,然后将它从内核复制到用户空间,处理数据。

在非阻塞状态下,IO执行的等待阶段并不是完全阻塞的,但是第二个阶段依然处于阻塞状态(调用者将数据从内核复制到用户空间,这个阶段阻塞)。

  • 优点:能够在等待任务完成的时间里干其他活(包括提交其他任务,也就是“后台”可以有多个任务在同时执行)。
  • 缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成,这会导致整体数据吞吐量降低。

3. 同步多路复用IO模型

多路复用IO的好处在于单个进程可以同时处理多个网络连接的IO。它的基本原理是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。

以select函数为例,当用户进程调用了select,那么整个进程会被阻塞,同时内核会“监视”所有select负责的套接字,当任何一个套接字中的数据准备好时,select函数就会返回。这时用户进程再调用read操作,将数据从内核复制到用户进程。

图片-1672278979756

这里需要使用两个系统调用(select和recvfrom),而阻塞IO只调用了一个recvfrom。所以,如果处理的连接数不是很高的话,使用IO复用的服务器并不一定比使用多线程+非阻塞IO的性能好,可能延迟更大。多路复用IO的优势并不是对于单个连接能处理得更快,而是单个进程可以同时处理多个网络连接的IO。

在实际使用时,对于每一个套接字,都可以设置为非阻塞。整个用户的进程其实是一直被阻塞的,只不过进程是被select这个函数阻塞,而不是被IO操作阻塞。所以IO多路复用是阻塞在select、epoll这样的系统调用之上,而没有阻塞在真正的IO系统调用(如recvfrom)中。

  • 优势:系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

其主要应用场景如下:

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
  • 服务器需要同时处理多种网络协议的套接字,如同时处理TCP和UDP请求。
  • 服务器需要监听多个端口或处理多种服务。
  • 服务器需要同时处理用户输入和网络连接。

4. 同步信号驱动IO模型

该模型允许套接字进行信号驱动IO,并注册一个信号处理函数,进程并不阻塞而继续运行。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用IO操作函数处理数据。

图片-1672279554789

5. 异步IO模型

相对于同步IO,异步IO不是顺序执行的。用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到用户空间,然后从内核向进程发送通知,此时数据已经在用户空间,可以对数据进行处理了。

在Linux中,通知的方式是“信号”,分为3种情况:

  • 如果这个进程正在用户态处理其他逻辑,那就强行打断,调用事先注册的信号处理函数,这个函数可以决定何时以及如何处理这个异步任务。由于信号处理函数是突然“闯”进来的,因此跟中断处理程序一样,有很多事情是不能做的,为了保险起见,一般是把事件“登记”一下放进队列,然后返回该进程原来在做的事。
  • 如果这个进程正在内核态处理,例如以同步阻塞方式读写磁盘,就把这个通知挂起来,等到内核态的事情忙完了,快要回到用户态的时候,再触发信号通知。
  • 如果这个进程现在被挂起了,例如陷入睡眠,就把这个进程唤醒,等待CPU调度,触发信号通知。

图片-1672279808074

可以看到,IO两个阶段的进程都是非阻塞的。


Comment