几乎所有的程序员第一次接触到的网络编程都是从
socket()
、listen()
、read()
、write()
等系统接口开始的,这些接口默认情况下都是阻塞型的,所以本文将详细探讨同步阻塞式网络 IO 模型。关于网络 IO 模型的理论知识请参考上一篇文章《五、网络编程之网络 IO 模型的本质》
同步阻塞式网络 IO 模型详解
同步阻塞 IO
原理
同步阻塞 IO(blocking IO):应用进程从发起 IO 系统调用时不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回用户空间。比如发生 IO 操作 read 时,它会经历两个阶段:
-
第一个阶段,等待数据准备就绪:
当用户进程调用了 read 这个系统调用,kernel 就开始准备数据了。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的数据包),这个时候 kernel 就要等待足够的数据到来,而在用户进程这边,整个进程会被阻塞。
-
第二个阶段,将数据从内核拷贝到进程或者线程中:
当 kernel 一直等到数据准备好了,它就会将数据从 kernel 中拷贝到用户内存,然后 kernel 返回结果,用户进程才解除 block 的状态,重新运行起来。
所以,blocking IO 的特点就是在 IO 执行的两个阶段(等待数据和拷贝数据两个阶段)都被 block 了。
代码实现
同步阻塞的代码实现比较简单,就是调用原始 Socket API,具体可以参考《三、网络编程之Socket编程》一文。
同步阻塞 + 多线程/多进程
原理
同步阻塞 IO 问题:如上文中所述,在调用 read()
的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这会导致应用程序阻塞,什么也不干(当然调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的)。
一个简单的改进方案是在服务器端使用多线程(或多进程),即一个客户端的连接对应一个独立的线程(或进程)。目的是让每个客户端的连接都拥有独立的线程(或进程),这样任何一个客户端连接的阻塞都不会影响其他的连接。具体使用多进程还是多线程,并没有一个特定的模式:
- 传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;
- 如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。
代码实现
代码示例请参考 GitHub 开源代码 “BIO/mul_pthread_server” 和 “BIO/mul_process_server”部分,下载链接
包裹函数:代码示例请参考 GitHub 开源代码 wrap 部分。
- 直接使用原始的 Socket API 虽然简单,但系统调用并不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
- 为使错误处理的代码不影响主程序的可读性,我把与 Socket 相关的一些系统函数加上错误处理代码包装成新的函数,即包裹函数。
服务端使用多进程并发时要考虑以下几点:
- 父进程最大文件描述个数(父进程中需要 close 关闭的文件描述符和 accept 返回的新文件描述符);
- 系统内创建进程个数(与内存大小相关);
- 进程创建过多是否降低整体服务性能(进程调度)。
服务端使用多线程并发时需考虑以下问题:
- 调整进程内最大文件描述符上限;
- 线程如有共享数据,考虑线程同步;
- 服务于客户端线程退出时,退出处理。(退出值,分离态);
- 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU。
同步阻塞 + 线程池/连接池
原理
同步阻塞 + 多线程/多进程问题:多线程(或多进程)的服务器模型似乎完美的解决了同步阻塞 IO 模型的问题,但其实并不尽然:线程和进程的创建和销毁都会消耗系统资源,如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。
进一步的改进方案是考虑使用“线程池”或“连接池”。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如 websphere、tomcat 和各种数据库等。
- “线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。
- “连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。
注意:“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
代码实现
代码示例参考 GitHub 开源代码 “BIO/threadpool_server"部分,下载链接
包裹函数:代码示例请参考 GitHub 开源代码 wrap 部分。
BIO 总结
- BIO 网络模型是网络编程的基础模型,所以大多数程序员都是从 BIO 网络模型入手网络编程的;
- BIO 网络模型在等待数据阶段和拷贝数据阶段都处于阻塞状态,所以适用于客户端连接规模不大的场景;
- 当然也可以使用“线程池”或“连接池”缓解部分客户端连接压力,但如果所面临的可能同时出现的上千甚至上万次的客户端请求的情况时并不能完全解决问题,总之,多线程(多进程)模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题,详细可以参考本系列文章的下一篇文章。