最简单的tcp服务器端程序
我们先来实现一个最简单的tcp服务器程序。
这个服务器程序简单地建立、绑定、监听套接字,然后逐一地accept来自客户端的请求。这是最简单的模型,只能串行地先处理一个请求,在对客户端write完之后,关闭客户端套接字,然后再accept下一个请求。在网络不好的情况下,在当前请求的处理中,由于write等函数可能被网络阻塞,导致执行异常缓慢,下一个请求可能被block很久。毫无疑问,这样的服务器模型是肯定不能满足需要应付大访问量的业务需求的。需要注意的是一些数据成员我们要先通过hton系列函数把它转换成网络字节序(网络字节序是大端表示,我们的PC大多数都是小端机器),否则会出现奇怪的现象,比如说没有把端口号9090通过htons转换成网络字节序的话,通过netstat -tln查看发现打开的并不是9090端口,而是33315端口(因为9090的小端表示是0x8223,在大端看来,0x8223等于十进制的33315)。下图是9090没有通过htons转换的结果:
telnet可以帮我们测试这个小程序。图中红框部分是server的响应消息。在发送完消息之后,服务器关闭了连接。
将其改造成http服务器
为了方便压力测试,我们将其改造成简单的http echo服务器,先定义对应的响应头和相应体,并初始化相应头。
在header_pattern中,预留了%d存放body的长度,第四行使用sprintf将body的长度写入其中。
然后在accept之后,先后把响应头和响应体写到客户端(注意:是分两次write而不是header和body合起来一起写)。
然后我们就可以通过浏览器访问我们的”服务器了“。
异步响应多客户端
要克服后面的请求会被block很久的毛病,我们必须想办法让服务器程序可以”并行“地处理正在排队等待处理的请求。多线程可以帮我们做到这一点。
多线程实现
我们用多线程方法改一下代码,得到以下程序。
思路很简单,每次accept了一个连接之后,启动单独启动一个额外的线程(设置其属性为脱离线程,这样我们就不用在主线程去join 了),由这个额外的线程去负责读写刚才accept的客户端套接字。由于主线程启动一个额外的线程之后可以立刻返回,进而可以继续accept,等待下一个连接的到来,下一个连接到来之后继续启动线程来处理。使用线程的好处是明显的,这样下一个连接就不用等待上一个连接完可以被服务器accept,并与其它请求“并行”地被服务器处理。但是多线程服务器模型有一个问题,那就是当来自客户端的请求量很大的时候,需要启动很多线程来负责处理请求,由于存在非常多的线程,系统仅仅调度这些线程便消耗了大量的的资源,所以此模型并不适合高访问量的场景。
抛弃线程,使用多路复用函数select
select函数可以让我们编写更加高性能的网络程序。我们可以把accept来的一堆客户端套接字放到select里面的套接字集,然后阻塞在select函数中,一旦有套接字可读可写,select会立刻返回,我们处理完相应的工作后(读写完可读可写的套接字之后),继续在select中阻塞,直到再次有请求的到来。这样就免去了系统调度一大堆线程的开支。我们用一个set来保存已有的套接字(源代码在下面给出),每一次都把set中的最大值加一放到select的第一个参数里面,下面的for循环进行套接字检查(从0开始到套接字的最大值遍历,用FD_ISINSET来检查当前套接字是否在套接字集合里面)的时候也直接用这个最大值作为上限,这样我们就不用每次都检查FD_SIZE那么多次了。
使用非阻塞
在select返回之后,便开始对可读可写的套接字进行读写,然后接着阻塞在select中等待下一次有套接字可读可写。这样做有一个小问题,在select返回之后,在对相应套接字的读写过程中,可能会出现网络不好的现象,这样的话一些write和read函数就会阻塞,排在后面的可读可写的套接字就会一直得不到处理。
我们可以用非阻塞读写来解决这个问题。我们把所有的套接字都用fcntl来设置成非阻塞。
这种情况下,无论读写成功或者失败,或者只读或者只写了一部分,read和write函数会立刻返回,这样的话就不会影响后面的请求的处理了。因为write在非阻塞的情况下是遵守“能写多少就写多少的情况”,所以需要额外的数据结构来保存对每一个套接字已经写了多少(保存写的进度),以程序方便select下次返回的时候对相应的套接字在上次的断点上继续写数据。具体实现如下:
使用更高性能的epoll代替select
epoll是linux2.5.44内核之后的产物,具有更高的性能。使用方法跟select大同小异,这里就不阐述了。
性能测试
以上程序性能的高低都还是理论上的,还没有进行相应的实际测试。我们使用ab压力测试工具来对以上几个程序进行性能测试。
可见,epoll的性能是最高的,select略逊色于epoll,而多线程性能是最低的。