1 场景
对于通信模型来说,有服务器-客户端,也有点对点或者广播等方式。
对于网络来说,常用的模型是服务器-客户端,客户端主动发起请求,服务端接收请求并给予响应(request-response)。
根据请求/响应的应用场景不一样,服务器有不一样的名称。
获取Web页面的,称之为web服务器,使用的是http协议;
获取文件的,称之为FTP服务器,使用ftp协议;
或许自定义应用协议,定制一个专用服务器。不论是何种应用层协议的服务器,其共性在于传输层一般采用TCP协议, 而且服务器服务的客户端一般是多个。涉及到多个客户端时,就会有服务器并发的需求。
同时,不同的应用场景,对并发数的要求有所差异。
2 阻塞与非阻塞I/O
UNIX系统中一切且为文件,每个文件用文件描述符(fd)来标识,可以对文件进行open/close/read/write等操作。但是这里存在一个问题,比如说调用read函数读取文件中的数据时,这个文件有可能为空(没有数据),则有两种处理方式:
1)此时调用read函数的线程被挂起,直到文件非空,唤醒线程,则读取数据后正常返回。
2)立即返回,并标识错误状态。
前者线程被挂起,函数没有返回,相当于阻塞状态,这样的I/O称之为阻塞式I/O。
后者函数立即返回,相当于非阻塞状态,这样的I/O称之为非阻塞式I/O。
阻塞式I/O的编程逻辑较非阻塞式的简单,不用考虑程序执行的状态。
当一个线程被阻塞了,显然不能响应其他操作,那么就需要再启动另一个线程来响应其他操作,所以有多线程编程的需求。
3 多线程
使用多线程来处理多个客户端的请求,有两种方式,一种是有请求就建立一个线程,另一种是事先建立一定数量的线程(线程池)。
前者在建立,销毁线程时需要消耗内存资源以及占用CPU时间。两者都 会带来线程切换的性能开销,同时会带来线程安全的问题,然后就要加锁,加锁又会降低性能和增加编程复杂度。
多线程中可以使用阻塞I/O进行编程,阻塞时即挂起线程,不占用CPU资源。
当客户端的数量只有数十个时,可以采用多线程的方式进行处理。
部分代码:
/*
4 I/O多路复用
若不想使用多线程带来线程的切换开销和建立线程的内存花销,可以使用单线程的方式,那么此时显然就不能有阻塞式的I/O,否则就不能响应多个请求了。对于Socket编程而言,可以将accept/recv/send函数设置为非阻塞模式。对多个I/O进行并发处理,采用的是I/O复用技术,linux系统提供的接口有select/poll/epoll。
对于select/poll而言,分别将多个客户端socket fd放在数组中,然后进行轮询,检查是否有数据更新或者新的连接。因为是依次遍历轮询,且涉及用户态和内核态的切换,比较消耗时间,特别是当客户端数量较大时,依次轮询的时间复杂度为O(n)。但是没有多线程带来的缺点。
部分代码:
while
5 总结
对于少量的客户端来说,多线程和poll这两种服务器模型的CPU占用率差异并不大。但是多线程的内存消耗会更大。
对于大量的客户端来说,需要考虑使用epoll的机制,将轮询的O(n)复杂度降低至O(1)。
同时需要根据CPU的核心数,以及CPU的个数来进行多进程(线程),充分榨干硬件资源。不行就需要增加服务器的数量(集群),分布式处理。
6 完整源码
多次测试,稳定运行,可以支持1000以内的并发。源码详见 github地址 :
https://github.com/Boooooots/server-client-modelgithub.com测试中若有问题,可以交流~~~