深入探索网络IO模型
同步、异步、阻塞、非阻塞
同步和异步,是针对调用结果是如何返回给调用者来说的,即调用的结果是调用者主动去获取的(比如一直等待recvfrom或者设置超时等待select(),则为同步,而调用结果是被调用者在完成之后通知调用者的,则为异步(比如windows的IOCP)
- 同步通信是指:发送方和接收方通过一定机制,实现收发步调协调。如:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式
- 异步通信是指:发送方的发送不管接收方的接收状态,如:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
阻塞和非阻塞,是针对调用者所在线程是否在调用之后主动挂起来说的,即如果在线程中调用者发出调用之后,再被调用这返回之前,该线程主动挂起,则为阻塞,若线程不主动挂起,而继续向下执行,则为非阻塞。阻塞和非阻塞主要讨论的是被调用方在收到请求到返回结果之前的这段时间内,调用方是否一直在等待。
- 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
- 非阻塞:非阻塞调用指在结果返回前,该调用不会阻塞当前线程
首先一个IO操作其实分成了两个步骤:1.发起IO请求,2.实际的IO操作。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步骤是否阻塞。如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO;如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。
网络IO
当一个网络IO发生时,通常涉及两个对象,一个是调用IO的进程,一个是系统内核。
一个输入操作通常包括两个不同的阶段:
- 等待数据准备好;
- 从内核向进程复制数据。
对于网络IO的套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
四种网络IO模型:
- 阻塞IO模型
- 非阻塞IO模型
- 多路IO复用模型
- 异步IO模型
阻塞IO模型
阻塞IO是socket的默认设置,其模型如下图所示:
应用程序调用recvfrom产生一个系统调用,系统内核开启IO的第一个阶段:准备数据。 对于网络 IO 来说,很多时候数据在一开始还没到达时(比如还没有收到一个完整的 TCP 包),系统内核就要等待足够的数据到来。 此时整个用户进程会被阻塞。 当内核一直等到数据准备好了,它就会将数据从系统内核中拷贝到用户内存中,然后内核返回结果给用户进程,返回到用户空间,用户进程解除阻塞的状态,重新运行起来。
因此,阻塞 IO 模型的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据)都被阻塞了。
实际上,大多数socket接口都是阻塞型的,也就是指系统调用时(一般是 IO 接口) 却不返回调用结果,并让当前线程一直处于阻塞状态,只有当该系统调用获得结果或者超时出错时才返回结果。 这便引起了一个问题,即在调用 send() 的同时,线程处于阻塞状态,则在此期间,线程将无法执行任何运算或响应任何网络请求,这样,socket程序几乎失去了实际意义,也就是只能一对一并且固定发消息流程才行。
解决这个办法的最直接的方式就是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。因为进程的开销远大于线程,所以如果数量比较大的话不建议多进程。但是如果单个连接需要进行比较长时间或者大规模的CPU计算或者文件访问等,采用进程比较合适一些,因为进程更加稳定。
一个socket是可以accept多次的,accept的函数接口:
int accept(int fd, struct sockaddr *addr , socklen_t *addr len)
输入参数 fd 是从 socket()、 bind() 和 listen() 中沿用下来的 socket 句柄值。 执行完 bind() 和 listen()后,操作系统已经开始在指定的端口处监昕所有的连接请求,如果有请求,则将该连接请求加入请求队列(即全连接队列)。调用accept的作用就是从全连接队列中抽取第一个连接的信息,创建一个与 “同类的新的 socket 返回句柄,这个新的 socket 句柄即是后续 read() 和 recv() 的输入参数。 如果请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进 入队列。
因此采用多个线程去accept看起来可以解决为多个客户机提供连接的要求,但是如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据 系统资源,降低系统对外界响应的效率,而线程与进程本身也更容易进入假死状态。
线程池或连接池技术可以在一定程度上缓解频繁调用IO接口带来的资源占用情况。“线程池”旨在降低创建和销毁 线程的频率,使其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。 “连接池”是指维持连接的缓存池,尽量重用已有的连接,降低创建和关闭连接的频率。使用"池”必须考虑其面临的响应规模,并根据响应规模调整“池” 的大小。
多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞模型来尝试解决这个问题。