搭建一个web服务器

构建一个web服务器

最近几天我根据一些参考书和网上的开源程序写了一个简单的HTTP服务器 链接,这个HTTP服务器尽量用简单的代码完成一个服务器基本的功能,比如分析请求行,分析请求头,并且根据不同的请求方法给出响应。网上的开源程序很多,但是有的定义了很多自定义的结构体,有的作为c语言结构的程序,参数要在各个文件中互相传递,有的实现很复杂,增加很多功能。我实现一个简单的服务器,不追求支持太多的功能,但是希望用OOP的思想把尽量多的参数封装在类里,调用成员来完成功能,同时也运用了一些c++11的功能,让整个程序可读性更高,而且易使用。整个程序借鉴了很多其他人程序中的思想,但是我也对他们进行了修改,满足需求。

一个基本的WEB服务器

作为一个基本的WEB服务器,它需要接收客户端的数据,并且解析客户端数据是否符合要求,如果符合要求是否能给出客户端需要的数据,如果不能应该如何告之客户端。并且用尽量少的开销完成。另一个需要注意的是,服务器一定面向多个客户端,可以接受多个客户端同时访问,给出每个客户端需要的内容。一个基本的HTTP请求如下
搭建一个web服务器
一个HTTP的响应如下
搭建一个web服务器
不考虑多个客户端同时向服务器请求,我们从客户端接收到的数据,第一个要做的是解析请求头,找到方法,uri,并且确认协议号。协议号之后就是请求头,请求头以空行结束,也就是我们只要找到这个空行就可以完成请求头的解析,并且继续解析请求体。但是很可能客户端一次不能发送完全部的数据,我们需要先解析一部分,有新的数据我们再解析后面的内容。这就可以用有限状态机解决,参考《Linux 高性能服务器编程》,我们有一个主状态机来记录解析的请求头还是请求行,从状态机记录是否得到完整的一行。如此,我们每次收到数据,如果得到完整的一行,把内容交给解析函数,如果还没有得到完整的一行,那么继续等待下次客户端的数据。
我们把每个客户端的所有信息封装到一个对象,这里面记录了这个客户端的所有信息,比如有限状态机的状态,请求的文件地址,请求头的内容等。
在这个封装连接信息的类里,我使用一个map建立了请求头和处理函数的映射,也就是只要在map的key中能查询到请求头,就可以快速执行处理,而且方便扩展,如果支持更多请求头,只需要增加处理函数并且加到map里。

Reactor模式和Proactor模式

我们以上考虑的都是单个客户端对服务器访问,如果有多个客户端,我们考虑多线程模式,一个经典的设计方法是Reactor,在这个模式下,我们使用IO复用,比如epoll,把sockfd注册到事件表,主线程发现sockfd可读,那么主线程接收新的连接,把连接分配给读线程,读线程读出内容,并且处理业务,并发送要的数据给写线程,写线程发送。
另一种经典的设计方法是Proactor,在这种模式下,主线程接收新的连接,并且将连接注册到事件表,同时等待读完成,在这种模式下,我们不需要自己读数据,只需要等待读完成,实现了异步IO。子线程负责业务。
在toy HTTP server中,我们用Reactor模式,但是读和写交给主线程,即主线程负责,接受新连接,把新的连接放到事件表。并且读出数据,并且发送给客户端数据。对于子线程来说,他们不知道读写操作,只关注业务。
在实现线程池中,我使用了c++11的thread类,可以很方便的创建线程,另外可以将类的非静态成员函数作为线程的handler,但是要显式的传入this指针。

定时器

定时器使用最小堆,也就是把下一个要触发的定时器放在顶部,每次触发ALARM,我们检查顶部的定时器是否到时,如果到就执行相应的函数。定时器是我认为不够完善的一个部分。如之前所说,每个连接都封装成一个类,类中拥有一个function类型成员timer_handler,指定这个连接的定时器的回调函数。这个成员可以通过bind函数绑定任意一个类成员函数。如果建立一个timer,我们就把这个连接的地址传入timer,定时以后,执行这个连接里的timer_handler绑定的函数,但是如果一个连接有多个定时器并且需要绑定不同的函数就不行,因为对于同一个实例timer_handler必须只能绑定的函数。同时,定时器类需要传入一个结构体,这个结构体包含了定时时间,连接对象指针一些信息,也就是说我们在连接的实例里,如果要建立一个定时器,需要建立一个定时器的结构体,同时把自己的指针传入,然后把这个结构体的指针传入到定时器的类。有两个问题,一个是如果连接的实例被释放(一般情况下不会)定时器绑定的结构体也会被释放,造成指针指向了一个释放的地址。第二个问题是,在连接实例里建立了定时器结构体,定时器类会调用定时器结构体绑定的实例的成员,这样循环绑定,感觉会造成很多不稳定的可能。参考其他程序的定时器,发现定时器类在完成定时后,会释放定时器结构体的内存,我修改为定时器结构体的内存由连接的实例释放,因为这个结构体是由连接的实例申请内存建立。

其他困难

在完成程序后,我使用webbench进行压力测试,但是发现不管多少客户端,全部访问都会失败,让我感觉很怪,使用HTTP调试发送都可以得到正确响应,但是放到webbench就出问题。我只能先理解以下webbench的程序,发现webench使用多进程来建立多个客户端,每个进程在规定时间内不停的访问服务器,并且看能不能得到响应,如果得到响应就进行下一次访问。我觉得问题不出在多客户端上,把客户端调整为1也会出问题。首先我把webbench所有关于多进程的代码删除,留下单进程观察,还是出现同样的问题,关键问题在,webbench不断读服务器发送的数据,直到读出长度为0代表成功,或者长度-1表示失败,然后关闭连接进行下一次连接。使用我的程序,webbench每次建立连接,第一次都能读数据,但是第二次就会失败。最后错误原因是在写程序时,使用setsockopt的SO_LINGER, 每次关闭socket,服务器会发送一个复位报文,马上关闭连接,这样不需要进行tcp断开连接的过程,也就是异常关闭,节省了断开连接的数据,新的连接可以使用这个资源,这样的异常关闭会让客户端认为错误,带来每次访问都失败,之后不使用setsockopt,恢复正常。

需要改进的地方

用更稳定的方法构建timer。
支持更多方法。
处理请求体。
支持更高的访问量。

reference

[1] 《Linux高性能服务器编程》
[2] 《图解HTTP》
[3] http://zyearn.com/blog/2015/05/16/how-to-write-a-server/
[4] https://blog.csdn.net/superbfly/article/details/72782264?utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.baidujs&dist_request_id=1332048.21087.16195157602338031&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromMachineLearnPai2~default-1.baidujs

搭建一个web服务器

上一篇:JS逆向简单剖析


下一篇:Http——HttpURLConnection详解