最近几天用C++重新写了之前的HTTP服务器,对以前的代码进行改进。新的HTTP服务器采用Reactor模式,有多个线程并且每个线程有一个EventLoop,主程序将任务分发到每个线程,其中采用的是轮盘调度来均匀分配任务。

服务器的源代码放在Github。以前的旧版本也放在我的GitHub上,在Oh-Server仓库中。新代码又新建了一个仓库。

HTTP基础知识

写HTTP服务器当然要了解HTTP的基础知识。HTTP/1.1由RFC2616定义,它和TCP/IP协议族内的其他协议相同,是用于客户和服务器之

间的通信。请求访问资源的一端成为客户端,而提供资源响应的一端成为服务器端。我们要写的是服务端。

HTTP请求报文

HTTP协议规定,请求从客户端发出,然后服务器响应该请求。一个HTTP请求报文的例子如下所示:

GET / HTTP/1.1

Host: www.cnblogs.com

User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Connection: keep-alive

这是一个真实的HTTP请求的例子,其中每一行都以\r\n结尾。由于我们写的是简单的服务器,所以我们只关心其中的几行。

第一行称为请求行,GET是请求方法,表示获取资源,除此之外还有POST方法、PUT方法、HEAD方法、DELETE方法和OPTIONS方法等。由于我们写一个简单的服务器,所以暂时仅支

GET方法。/是URI,表示客户希望访问的资源的URI。HTTP/1.1是HTTP协议的版本,此例中表示1.1版本。我们需要解析请求行,需要解析出方法字段、URI和HTTP协议版本。

第二行是Host字段,表示所请求的资源所在的主机名和端口号。

第三行User-Agent是客户的浏览器的类型,此例是运行在Ubuntu上的Firefox浏览器。

第四行Accept表示客户接受的资源的类型。

第四行Accept-Language表示客户接受的语言类型。

第五行Connection表示服务器在发送完客户请求的数据之后是否断开TCP连接。keep-alive表示不断开,close表示断开。

HTTP应答报文

HTTP/1.1 200 OK

Server: Apache/2.2.22 (Debian)

Content-length: 1223

Content-Type: text/html

第一行为应答行,HTTP/1.1是协议版本,200是状态码,OK是状态短语,表示请求正常。

第二行Server表示服务器的类型,此例中是Apache服务器。

第三行Content-length表示实体的长度,单位字节。

第四行Content-Type表示实体的文件类型。

程序运行流程

编译完毕后在终端中输入 ./Servant 8080开始运行服务器程序,再打开浏览器输入localhost:8080访问我们写的HTTP服务器。

服务器在Servant.cpp中定义的mian函数中监听到了客户浏览器发送的连接请求,主函数accept客户连接并选择一个线程将客户的已连接套接字注册到此线程的EventLoop中。

EventLoop是一个事件循环,每个循环都是在试图从其中的epoll中获取活动的套接字描述符并交给Handler类中处理。

Handler类是处理客户HTTP请求的类,它首先将客户的原始请求转发给Parser类处理,从而获取客户请求的解析结果,Parser类将解析后的结果存入HTTPRequest类型的结构体中。Handler类根据解析后的结果首先测试客户请求的文件是否存在,如果不存在将返回错误。如果文件存在但客户的权限不允许那么也返回客户错误信息。

每个客户请求处理完毕后就关闭此套接字然后EventLoop继续循环。

解析HTTP请求

HTTP服务器的一个重要任务是解析HTTP请求,源代码中Parser.hParser.cpp文件中定义的Parser类就是干这个的。为了表示解析后的结果我们定义了

一个结构体HTTPRequest结构存储解析后的结果,定义如下:

// 解析请求后的数据存储在HTTPRequest结构体中
typedef struct
{
std::string method; // 请求的方法
std::string uri; // 请求的uri
std::string version; // HTTP版本
std::string host; // 请求的主机名
std::string connection; // Connection首部
} HTTPRequest;

各个字段的意义正如注释中所示。Parser类的构造函数接受一个字符串类型的参数,解析后的值存储在_parseResult结构体中,并提供一个接口getParseResult函数访问解析

后的结果。解析的顺序如下:

首先parseLine函数按照\r\n作为分隔解析出每一行请求,并把结果存储在_lines中,其中每一个元素是一行请求。

然后调用parseRequestLine函数解析请求行,函数按空格解析各个字段,并把解析得到的方法、URI和HTTP版本存入HTTPRequest类型的结构体中。

最后调用parseHeaders函数解析其他头部字段,并将结果存入HTTPRequest类型的结构体中。

线程池

要想提高服务器的性能,使用线程池是一种很好的方法,它避免了单线程的低效率,并且避免了每次创建一个线程的额外开销。源代码中EventLoopThreadPool.h文件定义了线程池的类EventLoopThreadPool。其中的每个线程都是一个EventLoopThreadEventLoopThreadEventLoopThread类的结合体,意思就是每个线程运行一个EventLoop。每个线程的运行函数就是EventLoop类中定义的loop函数。线程池中线程的数目由主函数设置,我设置为4。线程的数目不宜过多也不宜过少,过多的话会增加CPU的调度开销;过少的话不能发挥多核CPU的性能。所以线程池中常驻线程的数目应该等于CPU核心数,以尽量减少任务切换带来的额外开销并充分发挥处理器的性能。

具体实现请参考源代码。

其他模块的作用

由于我们写的是非阻塞的HTTP服务器,所以缓冲区是必须要有的。在读取客户浏览器的请求和发送服务器的响应时我们会先将不完整的请求和响应暂存到缓冲区中,等到数据全部读完或者写完后再一并发送或者交给其他模块处理。所以Buffer.cppBuffer.h文件中定义的Buffer类就显得非常有用了。

Buffer类有readFdsendFd函数分别用于读取客户的请求和发送服务器的响应。Buffer类底层存储的是一个char型的vector,每次添加数据就调用push_back将数据添加到字符数组末尾。_readIndex_writeIndex分别表示开始读的索引和开始写的索引,用这两个索引可以方便的读和写数据。

I/O复用

我们使用I/O复用技术的epoll系列函数来监听套接字并通知主函数。至于为什么选择epoll而不是select或者poll,是因为epoll采用回调的方式通知事件;而selectpoll采用的

都是轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的描述符返回给用户。因此它们的时间复杂度是O(n),而epoll由于采用回调的方式所以其时间复杂度为O(1)。

但是当活动连接比较多的时候epoll_wait的效率未必比selectpoll高多少,因为此时回调函数触发的比较频繁,所以epoll_wait用于连接数量多但是活动连接比较少的情况。

其他注意的地方

  1. 对于监听套接字设置SO_REUSEADDR套接字选项,以允许服务器在其派生的子进程正在处理客户请求的过程中重启服务器进程。

  2. 忽略SIGPIPE信号,当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号,该信号的默认动作是终止进程,因此进程必须捕获它以免被终止。

参考资料:

UNIX网络编程(第三版)卷一 人民邮电出版社

深入理解计算机系统(第二版) 机械工业出版社

Linux多线程服务端编程 电子工业出版社

Linux高性能服务器编程 机械工业出版社

如何写一个简单的HTTP服务器(重做版)的更多相关文章

  1. 如何用PHP/MySQL为 iOS App 写一个简单的web服务器(译) PART1

    原文:http://www.raywenderlich.com/2941/how-to-write-a-simple-phpmysql-web-service-for-an-ios-app 作为一个i ...

  2. 如何写一个简单的http服务器

    最近几天用C++写了一个简单的HTTP服务器,作为学习网络编程和Linux环境编程的练手项目,这篇文章记录我在写一个HTTP服务器过程中遇到的问题和学习到的知识. 服务器的源代码放在Github. H ...

  3. nodejs写一个简单的Web服务器

    目录文件如 httpFile.js如下: const httpd = require("http"); const fs = require("fs"); // ...

  4. 用Python写一个简单的Web框架

    一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...

  5. 一个简单的web服务器

    写在前面 新的一年了,新的开始,打算重新看一遍asp.net本质论这本书,再重新认识一下,查漏补缺,认认真真的过一遍. 一个简单的web服务器 首先需要引入命名空间: System.Net,关于网络编 ...

  6. (2)自己写一个简单的servle容器

    自己写一个简单的servlet,能够跑一个简单的servlet,说明一下逻辑. 首先是写一个简单的servlet,这就关联到javax.servlet和javax.servlet.http这两个包的类 ...

  7. Tomcat剖析(二):一个简单的Servlet服务器

    Tomcat剖析(二):一个简单的Servlet服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三) ...

  8. Tomcat剖析(一):一个简单的Web服务器

    Tomcat剖析(一):一个简单的Web服务器 1. Tomcat剖析(一):一个简单的Web服务器 2. Tomcat剖析(二):一个简单的Servlet服务器 3. Tomcat剖析(三):连接器 ...

  9. 自己模拟的一个简单的web服务器

    首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...

随机推荐

  1. 【[NOI2018]屠龙勇士】

    发现好像都是化掉系数之后套上\(ExCrt\)的板子 这好像是一个真正的扩展扩展中国剩余定理 我们要处理的方程是这样的形式 \[c_ix\equiv b_i(mod\ a_i)\] 其中\(c\)用一 ...

  2. 随手练——洛谷-P1002 过河卒(动态规划入门)

    题目链接:https://www.luogu.org/problemnew/show/P1002 题目还算良心,提醒了结果可能很大,确实爆了int范围, 这是一开始写的版本,用递归做的,先给地图做标记 ...

  3. 连接池中的maxIdle,MaxActive,maxWait等参数详解

    转: 连接池中的maxIdle,MaxActive,maxWait等参数详解 2017年06月03日 15:16:22 阿祥小王子 阅读数:6481   版权声明:本文为博主原创文章,未经博主允许不得 ...

  4. 【jQuery】结合accordion插件分析写插件的方法及注意事项

    1.jQuery插件的命名方式:jquery.[插件名].js 2.对象方法附加在jQuery.fn上,全局函数附加在jQuery对象本身上 3.插件内部this指向的是通过选择器获取的jQuery对 ...

  5. Unity3D-制作火焰效果

    1.插件的准备 随着官网上的迭代更新,连下载按钮都找了好久,今天制作的火焰效果要依赖一个插件,LowPoly Environment Pack 输入网址unity3d.com在Assert Store ...

  6. Dubbo实践(二)架构

    架构 节点角色说明 节点 角色说明 Provider 暴露服务的服务提供方 Consumer 调用远程服务的服务消费方 Registry 服务注册与发现的注册中心 Monitor 统计服务的调用次数和 ...

  7. 彻底弄懂JS原型与继承

    本文由浅到深,循序渐进的将原型与继承的抽象概念形象化,且每个知识点都搭配相应的例子,尽可能的将其通俗化,而且本文最大的优点就是:长(为了更详细嘛). 一.原型 首先,我们先说说原型,但说到原型就得从函 ...

  8. Web—08-移动端库和框架

    移动端js事件 移动端的操作方式和PC端是不同的,移动端主要用手指操作,所以有特殊的touch事件,touch事件包括如下几个事件: 1.touchstart: //手指放到屏幕上时触发 2.touc ...

  9. [iOS]CIDetector之CIDetectorTypeFace人脸识别

    - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typica ...

  10. js数组定义和方法 (包含ES5新增数组方法)

    数组Array 1. 数组定义 一系列数据的集合成为数组.数组的元素可以为任何类型的数据(包括数组,函数等),每个元素之间用逗号隔开,数组格式:[1,2,3]. 2. 数组创建方式 (1) 字面量方法 ...