目录
[Socket、TCP 传输层和网络分层的关系](#Socket、TCP 传输层和网络分层的关系)
[四、Http.hpp 协议文件](#四、Http.hpp 协议文件)
[Util 类](#Util 类)
[ReadLine() 函数](#ReadLine() 函数)
[ReadFile() 函数](#ReadFile() 函数)
[HttpRequest 请求报文类](#HttpRequest 请求报文类)
[ParseReqLine() 函数](#ParseReqLine() 函数)
[ParseHeaderkv() 函数](#ParseHeaderkv() 函数)
[ParseText() 函数](#ParseText() 函数)
[Deserialize() 反序列化函数](#Deserialize() 反序列化函数)
[HttpResponse 响应报文类](#HttpResponse 响应报文类)
[CodeToDesc() 函数](#CodeToDesc() 函数)
[SetCode() 函数](#SetCode() 函数)
[SetBody() 函数](#SetBody() 函数)
[BodySize() 函数](#BodySize() 函数)
[AddHeader() 函数](#AddHeader() 函数)
[Serialize() 序列化函数](#Serialize() 序列化函数)
[MiniType 类](#MiniType 类)
[HttpProtocol 类](#HttpProtocol 类)
[RegisterService() 函数](#RegisterService() 函数)
[IsReqService() 函数](#IsReqService() 函数)
[HandlerHttpRequest() 主处理函数](#HandlerHttpRequest() 主处理函数)
[五、HttpServer.cc 文件](#五、HttpServer.cc 文件)
[Login() 函数](#Login() 函数)
[Main() 主函数](#Main() 主函数)
[六、TcpServer.hpp 网络层文件](#六、TcpServer.hpp 网络层文件)
一、HTTP的工作模式
HTTP 的工作模式就是 : 客户端主动请求,服务器被动应答,一问一答,无状态通信的模式。
我们知道 HTTP 是基于 TCP 的请求-响应式的应用层协议,采用客户端 - 服务器 (C/S) 工作模式。
- 客户端是主动发起请求的一方,常见的有浏览器、手机App等;
- 服务器是被动等待请求、处理业务并返回响应的一方,常见有 Nginx、Apache、Tomcat等。
HTTP的通信步骤
整个HTTP通信分为四个步骤 :
1. 建立TCP连接
客户端先和服务器通过三次握手建立TCP连接。HTTP本身不能单独传输数据,底层依赖TCP协议,保证数据传输可靠、不丢包、不乱序。
2. 客户端发送HTTP请求
客户端按照HTTP固定协议格式,把自身需求打包成HTTP请求报文,通过TCP通道发送给服务器。请求报文由四部分组成:请求行 (请求方法、URI、协议版本)、请求头、空行、请求正文。
3. 服务器返回HTTP响应
服务器接收请求后,解析报文、处理业务,比如读取网页文件、核对登录信息、数据库查询等;再按照HTTP协议格式,打包成HTTP响应报文,通过TCP回传给客户端。响应报文由四部分组成:状态行、响应头、空行、响应正文 (网页文本、图片、接口数据等)。
4. 关闭TCP连接
一次请求与响应交互完成后,HTTP/1.0 会直接断开TCP连接;HTTP/1.1 默认支持长连接,可以复用连接发起多次请求,但底层依旧保持一问一答的核心模式。
HTTP的两大特性
1. 无状态
服务器不会记录客户端的信息。哪怕是同一个设备,第一次登录、第二次访问页面,服务器默认识别不出是同一个客户端,每一次请求都是相互独立的。像保持登录、购物车记录这类功能,需要依靠 Cookie、Session 额外实现,HTTP本身不具备记忆能力。
2. 单向主动
整个通信里,只有客户端能主动发起请求,服务器永远不能主动向客户端推送数据。服务器只能被动等待客户端的请求,客户端不发起访问,服务器就不会产生交互。
客户端主动建立 TCP 连接、发送标准化HTTP请求,服务器被动接收请求并处理业务,再返回标准化响应;整个过程一问一答,单次通信相互独立、无状态,交互完成后断开连接。
二、自定义实现HTTP服务器
-
我们已经明白了 HTTP 的工作模式、通信流程。我们明白 HTTP 本质就是客户端主动请求、服务器被动应答,依托TCP做底层传输,依靠固定的请求响应格式完成对话,再通过 URI 定位具体资源。
-
所以我们自己动手自定义编写代码,从零实现一套简易版的HTTP服务器。实现 1:1 模拟复 HTTP 服务器的标准工作模式。
-
因为 HTTP 官方制定了一套完整通信规则,规定了客户端该怎么发送请求、服务器该如何接收回话、报文格式该怎样定义;而我们手写代码的过程,就是严格遵循这套规则,搭建出一个标准的HTTP服务器,并且符合行业标准、能被浏览器正常识别,和百度、淘宝后端底层逻辑保持一致的HTTP服务程序。
-
我们已经知道标准 HTTP 的工作流程:客户端(浏览器)发起TCP连接 → 客户端发送HTTP请求报文 → 服务器接收并解析处理请求 → 服务器构造并返回HTTP响应报文 → 通信完成断开TCP连接。而我们后续要写的每一部分代码,都会对标这个标准流程:
-
我们之前封装好的TcpServer网络服务类,通过socket创建套接字、bind绑定端口、listen开启监听、循环accept等待连接,就是标准流程里服务器被动等待客户端TCP连接的过程。
-
服务端通过通信套接字接收浏览器传输的数据报文,就是服务器接收HTTP请求报文的环节。
-
我们自定义HttpRequest类,实现反序列化逻辑,拆分请求行、解析请求头、提取请求正文,这正是模拟服务器按照HTTP协议格式解析请求,读懂客户端访问需求的核心步骤。
-
我们根据解析出的URI匹配对应业务逻辑,同时封装HttpResponse类序列化拼接标准响应报文,对标标准里服务器处理业务、按照HTTP规范构造响应内容的流程。
-
最后将构造好的响应数据通过套接字发送给浏览器,关闭套接字、释放资源,完整复刻请求响应完成后断开TCP连接的收尾流程。
这里一定要纠正一个误区:很多人认为 Nginx、Apache 才是真正的HTTP服务器,我们手写的只是简化版模拟程序。
其实二者只是体量和功能有区别,核心原理毫无差异,Nginx、Apache是工业级成熟服务器,具备高性能、高并发、功能全面、长期稳定运行的特点;而我们自己编写的代码,属于轻量化极简版本,但完全遵循HTTP协议标准。但对浏览器而言,它区分不出访问的是大型商用服务器,还是我们自己手写的服务端程序。不论我们访问百度域名,访问本地127.0.0.1:8080 ,底层都是基于TCP传输、遵循HTTP请求响应规则,所以浏览器的解析和渲染逻辑完全一致。
本质上都是遵守同一套规则,只是应用场景和规模不同,不存在真假、模拟与真实的区别。
总而言之,在吃透HTTP理论工作模式后,我们自定义编写服务端代码,并不是凭空开发,而是严格遵循HTTP协议标准,复刻客户端与服务器一问一答的通信逻辑。底层依托TCP完成连接与数据收发,上层通过自定义类实现HTTP报文的解析与构造,完整复刻标准HTTP服务器的每一个工作环节。通过亲手编码实现,我们能把抽象的协议理论落地成具体代码,真正从根源上搞懂HTTP服务器的底层工作原理,这也是我们动手手写代码的核心意义。
三、相关问题
我们需要知道的是 HTTP 的工作模式,一定是依赖下层协议层层接力完成,HTTP 是应用层协议,它自己没法独立在网络上传输,必须依赖下层传输层 (TCP)、网络层 (IP)、数据链路层、物理层四层协议协同工作,而现在我们只是聚焦应用层 HTTP,下层的细节暂时不展开讲。
那我们之前的 TCP网络计算器demo,它并没有设计到 HTTP,怎么理解?
我们之前实现的网络计算器和现在要实现的 HTTP 服务器,它们在下面四层 (物理层、链路层、网络层、传输层) 是一模一样的!唯一的区别就是应用层。
不管我们传的是 1+1 还是 GET / HTTP/1.1,底层的运输逻辑都一样,物理层都是电流通过网线跑。网络层都是用 IP 地址找到对方电脑。传输层 TCP 都要建立三次握手连接;保证数据不丢、不乱序;都用 recv() 和 send() 收发字节。只是我们在网络计算器中没用到 HTTP 协议而已。
最顶层的应用层就是核心区别,这一层是数据的 "格式" 和 "规矩"。网络计算器中我们是自定义协议的,客户端发 1+1\r\n,服务器必须分割计算。而 HTTP 服务器是全世界统一的 HTTP 标准,客户端发 GET / HTTP/1.1,服务器必须按请求行、请求头解析。也就是说我们在设计网络计算器时只是没有选用 HTTP 这个应用层协议,而是自己造了一个简易的、私有的应用层协议。
网址是什么?为什么我们要用网址,不用 IP?
我们之前写计算器的时候,是这样连接服务器的:
这是 IP + 端口。但我们在现实上网时绝对不会这样输,我们会输:
这是域名,是人类方便记的名字,浏览器会自动把翻译成 IP。所以我们输入网址,浏览器就会帮我们翻译成 IP + 端口,这和我们直接输 IP + 端口是一样的。
为什么网络计算器是自定义格式,而现在必须用 HTTP 格式?
一句话总结就是底层运输工具(TCP/IP)没变,变的是 "说话的语言"。网络计算器的说话语言是我们自己定的,客户端发 1+1,服务器就会知道,这是算式,需要计算,但如果是浏览器的话就无法计算,因为浏览器只会 HTTP。
而现在 HTTP 阶段,我们说话的语言是全世界统一的 HTTP 格式,客户端(浏览器)发 GET / HTTP/1.1,服务器就能懂这是 HTTP 请求,我就需要按 HTTP 格式回复,浏览器和服务器能对话,是因为语言统一了。
HTTP 是不是就代表网址?网址前面的 http:// 是啥?
HTTP ≠ 网址,http:// 只是告诉浏览器:"接下来用 HTTP 协议去访问这个网址",比如说
http:// 就是协议名,告诉浏览器 "我们去访问这个网站的时候,用 HTTP 格协议式说话"。
为什么网址前面几乎都是 http://?
因为浏览器默认遵守 HTTP 协议格式。就像我们打电话默认说中文,所以网址前面写 http://,就是明确告诉浏览器:请用 HTTP 协议访问这个地址。如果不写,浏览器默认也是 http://。
前面我们的网络计算器写了客户端 + 服务端,现在 HTTP 只写服务端,客户端是浏览器,对吗?
是的。对于自定义 HTTP 而言,服务端是我们写的;客户端是浏览器(Chrome、Edge)。
浏览器是不是只认识网址?浏览器是不是和网址强相关?
是的。浏览器是给人类用的,人类记不住枯燥的 IP(如 127.0.0.1),所以浏览器天生就是为网址设计的。浏览器的核心工作就是:接收人类输入的网址 → 翻译成机器懂的 IP → 建立连接 → 收发数据。
浏览器访问网址,就相当于我们计算器客户端填 IP 和端口,对吗?
是的。之前的网络计算器的客户端,我们手动填 127.0.0.1 8080,而现在对于浏览器客户端来说,我们填网址,浏览器自动帮我们通过 DNS 解析网址得到 IP,然后提取端口(默认 80 或指定的 8080),用这个 IP 和端口去连接服务器。
我们在语言层面上自定义写的 HTTP 协议格式?浏览器认不认?
浏览器完全认。浏览器不关心我们是百度的服务器,Nginx,Apache,还是我们自己用 C++ 写的简陋服务器,浏览器只看报文格式,符不符合 HTTP 协议标准。只要满足第一行是状态行 HTTP/1.1 200 OK,下面跟着标准响应报头 Content-Type、Content-Length...空行之后是正文,浏览器就会认为我们是 HTTP 服务器。所以取决于格式,格式对,就是 HTTP;格式不对,就不认。
那如果我们用 Telnet 输入请求,服务器页认吗?
认!而且完全一样。当我们用 Telnet 输入 GET / HTTP/1.1,我们写的服务器收到后解析第一行,发现格式是 方法 + URI + HTTP 版本,就会解析请求头,然后按 HTTP 格式返回响应,服务器就会认为"这是标准 HTTP 请求。所以不管是浏览器发的,还是 Telnet 发的,只要格式是 HTTP,服务器就认。
Socket、TCP 传输层和网络分层的关系
首先要明确的是,网络通信遵循标准的五层模型,从上至下依次为应用层、传输层、网络层、数据链路层与物理层,每一层各司其职、互不干扰,共同完成完整的数据传输流程。我们手写的所有业务代码,无论是自定义协议的网络计算器,还是遵循 HTTP 标准的服务器,都属于应用层范畴,这一层负责定义数据的交互格式与业务逻辑,区别仅在于是我们自主约定的私有协议,还是全球通用的 HTTP 标准协议。
而 TCP 协议作为传输层 的核心,是一套既定的数据传输规则,负责实现可靠传输、三次握手、四次挥手、丢包重传、数据排序等底层传输逻辑,它是看不见摸不着的协议规范,封装在操作系统内核的,我们无法直接操控。这里的关键在于,Socket 并非独立的协议层,也绝不等同于 TCP 协议,它是操作系统为应用层代码开放的编程接口,是连接应用层与传输层的唯一桥梁,我们编写代码时调用的 socket、bind、listen、accept、send、recv 等一系列系统调用函数,本质都是通过 Socket 这个 "工具窗口",间接调用操作系统内核中的 TCP 协议能力,实现客户端与服务端的连接建立、数据收发与连接断开。
我们写的应用层代码,本身无法直接触碰传输层、网络层 。TCP 的三次握手、滑动窗口、丢包重传,IP 的寻址、路由转发,链路层的帧封装,这些全部都由操作系统内核提前实现好了,属于内核态的工作。我们用户态的程序无权去操作这些底层细节,唯一能和传输层建立联系的入口,就是操作系统提供的 Socket API。我们调用 socket、bind、listen、send、recv 这些函数,本质就是通过系统调用,让内核帮我们启动 TCP、收发数据、管理连接。
所以整个网络通信确实是五层模型协同工作的结果:我们只负责最上层应用层的业务逻辑(比如计算器的运算规则、HTTP 的请求响应格式),通过 Socket 触发传输层工作;传输层 TCP 保证可靠传输;网络层 IP 负责跨网络寻址;链路层封装帧;物理层传输电信号。下面四层全部由操作系统和硬件完成,层层封装、层层接力,最终才把我们应用层的数据,完整地送到对端应用层程序手里。
现在我们知道浏览器就是客户端,所以我们代码中 Recv 读到、存放在 inbuffer 里的 HTTP 请求报文,是浏览器作为 HTTP 客户端,在其内部自动完成报文拼接、TCP 连接建立后,主动发送给服务器的。浏览器的报文拼接与发送逻辑封装在浏览器内部,我们无法查看,但可以通过 Recv 读取、终端打印,完整看到浏览器发送过来的全部原始请求数据。
下面我们就在浏览器上连接我们已经写好的服务端。当我们在浏览器地址栏输入 124.222.191.171:8080 并回车,浏览器会用这个 IP + 端口,和我们自定义写好的服务器建立 TCP 连接;并按照 HTTP 协议,生成一段请求报文;通过 TCP 的连接,把这段报文发送到我们的服务端。
连接成功后我们服务端就能收到浏览器发来的请求报文,现在我们将这个请求报文打印出来,如上,终端里打印出来的这一整段,就是浏览器发过来的完整原始请求报文。服务器里面的代码逻辑我们先不关心,代码我们已经写好了,我们先从现象过渡。
此时我们要清楚的是这个请求报文现在在服务器里的内存里,它就是一整块连续的 string 字符串,本质是一堆挨在一起的字节流。它里面的换行、空格、冒号,只是字符,对服务器来说一开始只是一堆没有任何结构的文本。类似于下面的形式 :

所以服务器收到时,只是一个长字符串,它不知道哪一行是请求方法、哪一行是头,只有当我们按 HTTP 协议规定的格式去拆分它时,它才有意义。因为 HTTP 协议规定了固定的格式 。也就是说浏览器和服务器之间约定好了 "我给你发的请求,必须是这种结构:先一行请求行,再若干行请求头,用 \r\n 分隔,空行结束。你也必须按同样的格式回我响应。" 所以浏览器必须按这个格式拼报文,否则服务器就不知道这是一个 HTTP 请求,就会把它当成乱数据扔掉。
我们把它打印出来,本质就是在确认一件事:"浏览器确实按 HTTP 协议,把完整的请求报文发过来了,我们服务端也确实通过 TCP 连接,把它完整收到了。"验证了基础通信是正常的,接下来才能放心去做解析。
所以我们要做的下一步就是对这个请求报文进行反序列化,按 HTTP 协议规定的请求报文格式,把这一整块字符串拆成请求行、请求头、空行、请求正文四部分。当我们通过反序列化,将浏览器发来的完整请求报文,拆解为请求行、请求头、空行、请求正文这几个结构化部分后,服务器才算真正 "读懂" 了浏览器的请求意图。作为服务端,我们就需要在自身代码内部,根据解析出来的这些关键信息,执行对应的业务逻辑处理:比如根据请求行中的请求方法判断是 GET 还是 POST、根据 URI 定位浏览器想要访问的服务器资源等等,从而做出正确的业务判断。
完成业务处理后,服务器还必须按照 HTTP 协议的固定格式,将处理结果重新序列化,组装成浏览器能够识别的完整响应报文,再通过 TCP 连接发回给浏览器。这一整套 "接收请求→解析反序列化→业务处理→序列化响应→返回客户端" 的流程,才是一个标准 HTTP 服务器的完整工作逻辑。这也和我们日常使用浏览器的体验完全一致:我们在浏览器中输入访问需求、发送请求,远端的服务器接收并解析我们的请求,处理完成后按照协议格式返回对应的资源,浏览器接收响应后完成解析与页面渲染,最终将结果呈现给我们。
下面我们看 Http.hpp 里的代码:
四、Http.hpp 协议文件
Util 类
ReadLine() 函数
ReadLine() 函数是 HTTP 请求解析的核心工具,它以 \r\n 为分隔符,从原始报文字符串中一行一行地读取内容,并在读取后从原字符串中删除已处理的部分。
ReadFile() 函数
ReadFile() 函数是HttpRequest 类的一个工具,被用在这一行:
当 HttpRequest 解析完 URI (比如 ./wwwroot/index.html),业务逻辑就会调用 RequestContent(),这个函数再调用 Util::ReadFile(_uri),把文件读出来。读出来的内容,会被塞进 HttpResponse 的响应正文里,最后发给浏览器。
这个函数很关键,因为它实现了 "读文件" 的能力:没有它,服务器只能返回固定的 hello world,没法返回真正的网页、图片。正是因为有了它,它的二进制模式支持所有文件类型,不管是 .html 文本,还是 .jpg、.png 图片,都能正确读取。后面我们解析出什么 URI,就用这个函数读对应的文件,实现了 "浏览器要什么,服务器就给什么" 的核心逻辑。
HttpRequest 请求报文类
ParseReqLine() 函数
这个函数是 HttpRequest 类里解析流程的第一步,负责解析 HTTP 的第一部分 : 请求行。然后将解析出来的几个内容分别赋值给 _method、_uri、_version、_suffix 这几个成员变量,同时 httpstr 也会被修改 (已读取的请求行被删掉),它的执行,是后面解析请求头、正文的前提。
- _method 会被 ParseText 用来判断是 GET 还是 POST 请求;
- _uri 会被业务逻辑用来读取本地文件;
- _suffix 会被 HttpResponse 用来设置响应头的 Content-Type。
ParseHeaderkv() 函数
这个函数是 HTTP 请求解析的第二步,负责把请求报头一行一行拆出来,解析成键值对,存进成员变量 _header 哈希表中。它的执行,必须在 ParseReqLine() 之后,ParseReqLine() 处理完之后,httpstr 里剩下的就是请求报头部分;ParseHeaderkv() 再负责把这些请求头解析出来。后续业务处理中,我们会用到 _header 里的信息。
ParseText() 函数
ParseText() 函数是 HTTP 请求解析的第三步,也是最后一步,负责解析请求正文。这个函数依赖 ParseReqLine() 函数解析出的 _method 来判断走哪个分支;也依赖 ParseHeaderkv() 函数解析出的 _header["Content-Length"],来读取 POST 请求正文得长度。如果是 GET 请求表示正文为空,但要解析 URI 里的查询参数 (?key=val),如果是 POST 请求就要根据 Content-Length 读取正文数据。但是无论是哪种请求方法,_text 里的内容最终要么是 GET 的查询参数,要么是 POST 正文,业务逻辑会用它处理请求 (比如登录校验、表单提交)。
Deserialize() 反序列化函数
Deserialize() 函数是 HttpRequest 类的 "总控调度器",把前面我们讲的 ParseReqLine() 解析请求行、ParseHeaderkv() 解析请求报头、ParseText() 解析请求正文三个步骤串起来,一次性完成整个 HTTP 请求的反序列化解析。将浏览器发来的一整坨字符串,变成我们后面能直接用的结构化数据。
整个 HTTP 请求解析链路,能实现层层递进、边读边删、按顺序解析,核心依赖就是全程使用引用传参。所有解析函数操作的,都是浏览器发来的同一个原始报文字符串,通过 ReadLine() 不断切割、修改原字符串,一步步剥离请求行、请求头、正文,最终完成完整解析。引用传参既保证了逻辑正确性,又避免了字符串拷贝,极大提升了效率。
请求头与正文之间的空行,是在请求报头解析循环的最后一次 ReadLine() 调用中,被一并读取并从原字符串中移除。空行本身不进入业务解析,仅作为请求头结束的分隔标记,在解析流程中被自动消耗,因此后续正文解析时,httpstr 中已不存在该空行。
对外接口函数
_uri 是在 ParseReqLine() 里被解析、处理过的路径;对于 GET 请求,ParseText() 还会把 URI 中的查询参数剥离,只保留纯路径部分;业务逻辑拿到这个路径,就可以调用 Util::ReadFile(_uri) 读取本地文件。
这个函数依赖前面的 Uri() 类,本质是调用 Util::ReadFile();把 "获取路径" 和 "读文件" 两个步骤合并成一个接口,业务逻辑直接调用就能拿到文件内容;直接返回浏览器要的资源内容。如果文件不存在,ReadFile 会返回空字符串,业务逻辑可以据此返回 404 页面。
_suffix 是在 ParseReqLine() 里,从 _uri 中提取出来的;后续 HttpResponse 类会用这个后缀,设置响应头的 Content-Type,比如 .html 对应 text/html,.jpg 对应 image/jpeg;浏览器拿到正确的 Content-Type,才能正确渲染资源。
这个函数返回的是解析出的请求正文或 GET 查询参数。_text 是在 ParseText() 里解析的,GET 请求存的是 URI 中的查询参数,POST 请求存的是请求正文;业务逻辑处理 POST 请求(比如登录、表单提交)时,会直接通过 Text() 获取用户提交的数据;也可以用来处理 GET 请求的查询参数。
把所有解析结果完整打印出来,用于调试。它把 HttpRequest 类里的所有私有成员变量(_version、_uri、_method、_header、_blank_line、_text)全部输出;可以快速验证解析是否正确,比如 _uri 是否拼接了正确的本地路径、_header 是否完整解析了所有请求头。
私有成员变量
这些变量就是把浏览器发来的一整坨字符串,拆解后存起来的所有信息,是后续业务处理、文件读取、响应头设置的全部依据。
常量定义
到这里,HTTP 请求报文的解析类 HttpRequest 我们就讲完了,它的核心作用就是将服务端接收到的、客户端发来的原始 HTTP 请求字符串,完成反序列化解析,让服务端能够读懂客户端的请求内容;而当服务端成功解析完请求、获取到结构化的请求数据之后,就需要去执行对应的业务任务,其中最典型、最核心的任务就是根据解析出来的请求 URI,去服务端本地找到客户端想要访问的对应资源文件,比如网页、图片等,把文件内容读取出来,完成请求对应的业务处理;在业务任务执行完毕后,服务端并不能直接将读取到的文件数据发送给客户端,必须要遵循 HTTP 协议规定的响应报文标准格式,将处理好的响应数据重新进行序列化封装,拼接成符合协议规范的响应报文,才能让客户端正常识别、解析并渲染结果,而负责完成这一步响应数据序列化、构建标准 HTTP 响应报文的,就是我们接下来要重点讲解的 HttpResponse 响应报文类。
HttpResponse 响应报文类
那我们接下来就要讲这个响应报文类,而且先提前搞懂它大体要干什么、怎么一步步拼出完整响应报文。它要做的核心工作有以下几个部分 :
-
拼装状态行 : 手动或默认设置 HTTP 版本、状态码、状态描述,比如 HTTP/1.1 200 OK、HTTP/1.1 404 Not Found;
-
设置并拼装响应头 : 比如内容类型 Content-Type、内容长度 Content-Length 这些,根据文件后缀自动匹配类型,再把每一条响应头拼成 key: value 的格式;
-
自动拼接协议规定的空行 : 响应头结束必须加 \r\n 空行,分隔响应头和响应正文;
-
挂上响应正文 : 把我们业务中读取到的 html、图片文件内容,放在空行后面当作响应正文;
-
整体整合输出 : 把状态行、所有响应头、空行、响应正文按顺序拼在一起,形成完整符合 HTTP 格式的响应报文字符串,最后直接通过 send 发给客户端就行。
私有成员变量

_version 是 HTTP 协议版本号,比如 "HTTP/1.0" 或 "HTTP/1.1"。是响应报文的状态行开头部分,响应报文的格式是:版本 状态码 描述\r\n。在构造函数里被初始化为我们之前定义的全局常量 g_http_version,序列化时会被拼在响应报文最前面,决定整个响应使用的 HTTP 版本。
_code 是HTTP 响应状态码,比如 200、404、500 等。是响应报文的状态行中间部分。通过 SetCode() 设置,序列化时会被转为字符串拼进状态行,浏览器根据它判断请求是否成功。
_code_desc 是状态码对应的文本描述,比如 "OK"、"Not Found"、"Internal Server Error"。
是响应报文的状态行结尾部分。在调用 SetCode() 时,自动通过 CodeToDesc() 函数从 _code 生成,序列化时和 _code 一起拼进状态行。
_header 是响应报头的键值对集合,比如 Content-Type: text/html、Content-Length: 1024。是状态行之后、空行之前的所有响应头部分,格式是:Key: Value\r\n。它是通过 AddHeader() 一个个添加,序列化时会循环遍历这个 map,把每一对键值拼成一行响应头,全部输出到报文里。
_blank_line 是HTTP 协议规定的空行,也就是 "\r\n"。是响应头和响应正文之间的分隔标记,HTTP 标准规定响应头结束必须有一个空行,浏览器才知道后面是正文。在构造函数里被初始化为全局常量 linesep,序列化时会被拼在所有响应头之后、正文之前,保证响应格式合规。
_text 是响应正文,也就是服务器返回给浏览器的实际内容,比如 HTML 页面、图片二进制数据、JSON 字符串等。是空行之后的响应正文部分,没有固定格式,就是纯数据。通过 SetBody() 设置(比如把 ReadFile 读到的文件内容放进来),序列化时会直接拼在空行后面,作为浏览器最终渲染的内容。
CodeToDesc() 函数
这是 HttpResponse 类里的一个私有工具函数,专门做HTTP 状态码到文本描述的映射,它是一个简单的 switch-case 表,输入一个 HTTP 状态码数字(比如 200、404),返回对应的标准文本描述 (比如 "OK"、"Not Found")。不认识的状态码,统一返回 "Unknown Status Code"。因为 HTTP 响应的状态行格式有严格要求,必须是:HTTP版本 状态码 状态描述\r\n。状态码数字本身是不够的,浏览器、客户端需要完整的状态行才能正确解析响应结果。这个函数的作用,就是帮我们自动补全状态描述,保证状态行的格式合规。
构造函数
用初始化列表进行构造:
- _version(g_http_version) : 把响应的 HTTP 版本初始化为全局常量 g_http_version(也就是 "HTTP/1.0"),保证响应报文的版本号是固定的。
- _code(0) : 状态码初始化为 0,表示一个 "空响应",后续需要通过 SetCode() 接口来设置具体的状态码(比如 200/404)。
- _blank_line(linesep) : 把响应头和正文之间的空行,初始化为全局常量 linesep(也就是 "\r\n"),这个值是固定不变的,提前初始化好,序列化时直接用就行。
它帮我们创建了一个合法的初始状态的响应对象,版本号和空行已经是标准值,不用每次创建都手动设置;业务代码创建 HttpResponse 对象后,只需要关注设置状态码、响应头、响应体这些可变的部分,不用管这些固定不变的默认值;这和 HttpRequest 的构造逻辑是对称的,保证了整个通信闭环的初始化一致性。
SetCode() 函数
SetBody() 函数
函数接收一个 const std::string & 类型的参数 content,表示要设置的响应正文内容;用常引用传参,避免拷贝大字符串 (比如大图片、大文件),提升效率。然后把传入的内容赋值给私有成员变量 _text;_text 就是 HTTP 响应报文里,空行后面的那部分响应正文,比如我们业务中读取到的 HTML 文件、图片二进制数据等。在 Serialize() 序列化时,会直接把 _text 拼在空行后面,作为响应报文的最后一部分,发送给浏览器;
BodySize() 函数
直接返回成员变量 _text(响应正文)的字节长度。它的存在,是为了设置 HTTP 响应头中的 Content-Length 字段。HTTP 协议的要求浏览器收到响应后,必须知道正文有多长,才能完整接收并正确解析。Content-Length 是 HTTP 响应中必须的字段,它的值就是响应正文的字节数。
AddHeader() 函数
接收两个常引用参数:响应报头的键 key (比如 "Content-Type") 和值 value (比如 "text/html") ;用常引用传参,避免字符串拷贝,提升效率。利用 std::unordered_map 的 [] 运算符特性:如果 key 不存在,会自动插入新的键值对;如果 key 已存在,会直接覆盖原来的值。
所以后面的业务逻辑中我们只需要调用 AddHeader("key", "value"),就能给响应添加任意响应头,比如 Content-Type、Content-Length 等;
Serialize() 序列化函数
Serialize() 函数是 HttpResponse 类的核心灵魂,和 HttpRequest::Deserialize() 是对称的,负责把前面设置的所有结构化数据,拼出一整个可以直接发给浏览器的标准 HTTP 响应报文。设置好状态码、响应头、正文的 HttpResponse 对象。序列化之后的内容就符合 HTTP 规范的响应报文字符串,可以直接通过 send 发给浏览器。
MiniType 类
MiniType 类是 HTTP 服务器里的一个工具型辅助类,解决的是文件后缀和 MIME 类型的映射问题,是前面 HttpResponse 类的一个重要支撑。根据文件后缀,自动给出正确的 Content-Type 响应头值。浏览器收到服务器返回的文件内容时,必须通过 Content-Type 响应头知道这是什么类型的数据,才能正确渲染:
- .html → text/html(告诉浏览器按网页解析)
- .jpg → image/jpeg(告诉浏览器按图片解析)
- .css → text/css(告诉浏览器按样式表解析)
MiniType 类就是把这种映射关系封装起来,让业务代码不用手动写死这些类型字符串,自动匹配即可。
这是对 MiniType 类里静态成员 _mime_map 的初始化代码,定义了文件后缀到 HTTP 标准 Content-Type 的映射规则。因为 HTTP 协议要求服务器在响应中,必须通过 Content-Type 报头,告诉浏览器「我发给你的是什么类型的数据」,浏览器才知道该怎么处理它:
是 .html → 浏览器就按网页解析渲染
是 .jpg → 浏览器就按图片显示
是 .css → 浏览器就按样式表解析
是 .mp4 → 浏览器就按视频播放
这张表就是把我们服务器支持的所有文件类型,和它们对应的标准 MIME 类型一一对应起来,让前面讲的 Suffix2MimeType 函数能快速查到正确的值。
HttpProtocol 类
前面我们讲完了 HttpRequest、HttpResponse 和 MiniType 这些独立的模块,它们分别解决了「如何解析请求」「如何拼装响应」「如何自动匹配文件类型」的问题,但这些模块彼此之间是分散的,需要一个核心类把它们全部串起来,形成一个完整的请求处理闭环 ------ 而 HttpProtocol 类,就是我们整个 HTTP 服务器的业务调度中枢,它负责把客户端发来的请求,从解析、路由分发、业务处理到最终生成响应的全流程串起来,是所有前面讲过的工具类的最终应用场景。
动态路由功能
什么是动态路由功能?
动态路由功能,说白了就是让我们的服务器能根据用户访问的不同网址,去执行不一样的处理逻辑。不像静态文件那样只是读文件返回,而是可以给每一个接口地址绑定一段我们自己写的代码,用户一访问这个地址,就自动跑对应的逻辑,而且这些绑定关系都是程序运行时动态添加的,不用写死在框架里,非常灵活。
为什么我们必须要有动态路由?
我们之所以一定要做动态路由,就是因为光靠返回静态文件根本不够用。如果没有路由,我们的服务器就只能发发网页、图片、样式表,做不了登录、注册、表单提交、数据计算这些需要实时处理的业务。有了动态路由,服务器就不再是一个简单的文件工具,而是变成了真正能处理业务、支持交互的完整 Web 服务器,这也是它能商用、能扩展的关键。
没有动态路由会怎样?
放到我们现在写的代码里,动态路由就是整套 HttpProtocol 最核心的扩展能力。我们用 service_t 规定好业务函数的格式,用 _http_services 存好地址和逻辑的对应关系,再通过注册、判断、处理这几个函数,把整套动态处理流程串起来。这样一来,我们想加新接口、新功能,直接注册就行,完全不用改底层的请求处理代码,让整个服务器变得好用、好扩展、好维护。
函数类型别名和私有成员变量
service_t 是一个函数类型别名,它是一个可以接收 HttpRequest 和 HttpResponse 引用的函数对象 (回调函数);所有被注册成 service_t 类型的业务函数,参数格式必须是一个 HttpRequest 引用和一个 HttpResponse 引用,这两个参数都是引用传参,避免拷贝且能直接修改响应对象;其中传入的 HttpRequest 对象是已经完成反序列化、解析好的请求数据,而 HttpResponse 对象是空白未序列化的,需要我们在业务函数里填充完内容后,再回到主处理函数中进行序列化并返回给客户端。
也就是说所有我们注册的自定义服务 (比如 /login、/query),都必须是这个格式的函数。我们的服务器不能只返回静态文件,还得支持动态接口 (比如登录验证、数据查询)。service_t 就是这些动态接口的统一标准:每个接口函数,都必须接收解析好的请求 req,和一个空的响应 resp;函数内部根据请求内容,设置 resp 的状态码、响应头、正文;这样一来,服务器处理动态请求的逻辑,就和处理静态文件的逻辑完全统一了。后面的 _http_services 这个 map,存的就是 service_t 类型的函数;RegisterService 注册的,也是 service_t 类型的回调;HandlerHttpRequest 中调用的,也是 service_t 函数。
是一个 unordered_map,专门存「请求 URI → 服务回调函数」的键值对。Key 请求的 URI 路径,比如 "/login"、"/query";Value 对应的 service_t 类型回调函数。它是我们服务器的路由中心,实现了根据请求路径,分发到不同业务逻辑的功能。比如客户端请求 "/login",就调用登录验证的回调;请求 "/query",就调用数据查询的回调;普通静态文件请求,就走默认的文件读取流程。后面的代码中,比如说 RegisterService 把 URI 和回调函数,插入到这个 map 里;IsReqService 判断请求的 URI 是否在这个 map 里;HandlerHttpRequest 如果在 map 里,就取出回调函数并调用。
RegisterService() 函数
函数的第一个参数 uri 是客户端要访问的服务路径,比如 /login,使用常引用传递,避免字符串拷贝,提高效率。第二个参数 service 是对应的业务处理函数,这个函数必须符合我们前面定义的service_t 格式,能够接收 HttpRequest 和 HttpResponse 对象,在函数内部完成动态逻辑处理。
RegisterService() 是 HttpProtocol 类提供的动态服务注册接口,它的作用就是将一个自定义的业务处理函数,绑定到一个指定的请求 URI 路径上。当客户端访问这个路径时,服务器就不会再去读取本地静态文件,而是直接执行我们注册好的业务逻辑,这是整个动态路由功能中最关键的一步。
IsReqService() 函数
这个函数是 HttpProtocol 类提供的路由判断工具函数,核心作用是:判断当前请求的 URI,是不是我们注册过的动态服务路径。它就像一个 "问路" 的环节,服务器拿到请求后,先问一句:"这个路径是要走动态业务,还是直接读静态文件?",这个函数就是回答这个问题的。所以后续就是根据这个函数的返回值,来决定走动态服务逻辑,还是走静态文件逻辑。
参数 uri 是传入要判断的请求 URI 路径,使用常引用传参,避免字符串拷贝,提升效率。
HandlerHttpRequest() 主处理函数
HandlerHttpRequest() 函数是整个 HttpProtocol 类的核心入口函数,也是服务器收到客户端请求后,处理流程的总调度。它把前面讲过的 HttpRequest、HttpResponse、MiniType、动态路由功能,全部串成了一条完整的请求处理闭环,是我们 HTTP 服务器的 "大脑"。它是前面所有模块的最终应用场景:HttpRequest 负责解析,HttpResponse 负责拼装,MiniType 负责类型匹配,动态路由负责分发;它是服务器和客户端之间的桥梁:收到请求后交给它处理,处理完直接返回响应,业务代码不用关心底层细节;它实现了请求处理的闭环:从解析请求到返回响应,全程由它调度,结构清晰,扩展性强。
这里再强调一下动态路由的核心执行步骤,_http_services 是我们存服务的哈希表,[ ] 运算符会根据传入的 URI,在表里找到对应的 service_t 函数对象。然后再传入http_req 和http_resp这两个参数,http_req 是已经反序列化好的请求对象,里面有浏览器发过来的所有信息,http_resp 是一个空的响应对象,业务函数里会给它设置状态码、响应头、正文。
举个例子 :
假设我们注册了一个登录服务:
当浏览器访问 /login 时:IsReqService 判断这是个动态服务;
就会执行这行代码:
函数执行完,http_resp 就被填好了登录校验的结果,之后再被序列化发给浏览器。
返回值就调用 HttpResponse::Serialize(),把前面设置好的状态码、响应头、正文,拼成一整个符合 HTTP 格式的响应字符串;返回的字符串可以直接通过 send 发给客户端,完成一次完整的请求响应流程。
这里我们再说一下关于状态码等的设置内容,因为静态文件的请求处理逻辑是高度固定且统一的,服务器只需要判断请求的文件是否存在,如果存在就统一返回 200 状态码,同时自动配置好文件长度、文件类型等必要的响应头,并将文件内容作为响应正文返回;如果文件不存在,则统一返回 302 重定向状态码跳转到404页面,正因为这套判断和响应设置的逻辑不会随业务变化而改变,所以可以直接写死在 HttpProtocol 框架的处理函数内部,由框架统一完成所有响应信息的设置,无需上层额外干预。而动态业务请求则完全不同,因为登录、注册、数据查询等各类业务场景的判断条件和返回结果千差万别,框架无法预设一套通用的响应逻辑,所以就需要我们在上层单独编写对应的业务处理函数,框架只会将解析完成的请求对象和空白的响应对象传递给这些业务函数,后续我们会在这些上层业务函数的内部,根据具体的业务执行结果,灵活地自行设置不同的响应状态码、自定义响应头以及对应的响应正文,以此来适配不同业务场景下的多样化返回需求。
五、HttpServer.cc 文件
下面我们接着将服务端主函数的入口文件 HttpServer.cc 文件,这个文件是整个 HTTP 服务器的程序入口与应用组装层,它和我们之前讲的 HttpProtocol 类协议处理模块是清晰的上下层关系:HttpProtocol 类作为底层框架,只负责通用的 HTTP 报文解析、路由分发和响应拼装,不关心具体业务和网络细节;而主函数文件作为上层应用,会先将 Login、Register 等业务处理函数通过RegisterService 接口注册给 HttpProtocol 类,再创建 TCP 服务器并绑定其 HandlerHttpRequest处理逻辑,最终启动监听服务,完成从业务配置、模块组装到服务运行的全流程,让底层的协议处理能力和上层的业务逻辑结合起来,形成一个完整可运行的 HTTP 服务器。
Login() 函数
Login() 函数是我们自定义的上层业务处理函数,专门用于处理用户登录请求。它严格遵循我们定义的 service_t 函数类型规范,由 HttpProtocol 框架在匹配到对应路由时自动调用。这个函数属于上层业务层,不属于底层框架。是由我们自己编写,灵活、可扩展、可随时修改。会通过 RegisterService() 函数注册到路由表中。当客户端访问 /login 时,HandlerHttpRequest() 会自动调用它。
与静态文件处理的区别就是静态逻辑固定写死在框架里。而动态业务逻辑写在独立函数里。并且状态码、响应头、返回内容全部由我们自己控制。业务变化不需要修改框架代码,只需要新增 / 修改业务函数。
业务处理函数
这几个函数,本质上和上面的 Login() 函数是一个性质的,它们都是遵循 service_t 规范的上层业务处理函数,只是这里我们就不实现具体的函数逻辑了。最终会通过 RegisterService() 注册到 HttpProtocol 框架中,和 Login 一样,作为动态路由的 "目标执行单元"。当客户端访问对应的 URI 时,框架会自动调用这些函数,由它们来处理请求、设置响应。
Main() 主函数
main 函数是整个 HTTP 服务器程序的入口函数与组装函数,也是我们所有模块的 "总调度中心"。它不负责具体的协议处理或业务逻辑,只负责把前面讲过的 HttpProtocol 协议框架、业务处理函数和 TcpServer 网络模块,按正确的方式组装起来,最终启动并运行整个服务器。
- 底层模块:HttpProtocol 负责 HTTP 协议处理,TcpServer 负责网络通信,两者都是通用的、独立的框架模块。
- 上层业务:我们写的 Login、Register 等业务函数,是具体的业务逻辑实现。
- 入口组装层:main 函数是最高层,它把底层框架和上层业务组合起来,配置好端口、注册好接口、绑定好回调,最终启动整个服务器。
main 函数依赖 HttpProtocol 和 TcpServer,而 HttpProtocol 和 TcpServer 不依赖 main,是典型的 "上层调用下层" 的层级关系,实现了业务和框架的完全解耦。
六、TcpServer.hpp 网络层文件
这个 TcpServer.hpp 文件封装了 "网络数据收发" 的底层问题,我们之前的网络计算器也是用到了,它的底层还调用了 Socket.hpp 文件,这里我们就不做过多讲解了,对于这个 TcpServer.hpp文件而言,它不关心 HTTP 协议、也不关心具体的业务逻辑,它只做三件事:
- 启动监听:创建 socket、绑定端口、循环等待客户端连接;
- 数据收发:通过 socket 接收客户端的原始请求,再把处理后的响应发回去;
- 进程管理:采用多进程模型,每来一个连接就 fork 子进程处理,保证服务器能同时服务多个客户端。
更关键的是,它通过 Handler_t 回调函数,和上层完美解耦:它定义好了统一的接口,只要上层传入一个 "接收字符串、返回字符串" 的处理函数,它就能自动完成网络收发,把网络层和协议层彻底分割。

这里我们重点说一下 inbuffer 和 outbuffer :
inbuffer:
inbuffer 是 TCP 网络层定义的局部字符串缓冲区,它的生命周期始于 TcpServer 服务端通过 socket 从浏览器客户端接收原始 HTTP 请求报文,网络层将接收到的完整请求数据存入 inbuffer 后,会通过提前注册的回调函数,将这个存有原始报文的 inbuffer 传递到上层 HttpProtocol 协议层,作为 HandlerHttpRequest 函数的入参;协议层会对 inbuffer 内的请求报文执行反序列化、路由判断、业务分发等一系列解析处理,在整个上层协议与业务处理的过程中,inbuffer 始终承载着客户端的原始请求信息,待处理流程结束后,inbuffer 会随着单次请求的 service 函数执行完毕而被销毁,等待下一次客户端连接时重新创建、接收新的请求数据。
outbuffer:
outbuffer 同样是 TCP 网络层 service 函数内定义的局部字符串缓冲区,它的生命周期始于上层 HttpProtocol 协议层的 HandlerHttpRequest 函数执行完毕后,该函数将处理完成、经过序列化拼装的 HTTP 响应报文作为回调函数的返回值,赋值给 outbuffer;此时 outbuffer 承载着协议层处理后的完整响应数据,随后网络层会调用 socket 的发送接口,将 outbuffer 内的响应报文原封不动发送回浏览器客户端,完成一次请求响应闭环;数据发送完成后,outbuffer 会被清空,随 service 函数结束完成本次生命周期,在下一次请求处理时,等待接收上层协议层新的响应返回值。
短连接和长连接
我们当前实现的代码是标准的 HTTP 短连接模型,一次 TCP 连接建立后,仅完成一次请求读取、业务处理、响应发送,随后立刻关闭连接释放资源,整个生命周期完全遵循 "一请求一连接一关闭" 的流程。短连接的优点是逻辑简单、无需处理复杂的数据包边界问题,天然规避了 TCP 粘包,开发难度低、调试直观,契合入门级 HTTP 服务器的开发需求;但缺点也十分明显,频繁创建和销毁 TCP 连接会带来大量三次握手、四次挥手的性能开销,在高并发场景下会严重拖慢服务器处理效率。
如果将服务器升级为长连接模式,就需要在服务端收发逻辑外层包裹持续循环,让一条 TCP 连接能够复用,连续处理客户端多次请求,长连接的核心优势是减少连接建立销毁的损耗,大幅提升服务器并发处理能力和响应效率,是工业级 HTTP 服务器的标配;但长连接也会引入新的问题,因为 TCP 是无边界的流式协议,客户端连续发送的多个请求会粘连成一条数据流,所以必须实现拆包逻辑,严格按照 HTTP 报文格式边界拆分出完整请求,而响应报文是服务端按序返回的,不存在主动连续发送的情况,无需额外封包,仅需保证单次响应报文格式完整即可。
针对 GET 和 POST 请求,长连接下的拆包处理逻辑有明确区分:GET 请求没有请求体,完整报文的边界是请求报头末尾的 \r\n\r\n,拆包时只需在 TCP 数据流中检索该结束符,提取分隔符前的完整内容即可作为单个请求报文;POST 请求携带请求体,拆包时需要先通过 \r\n\r\n 分割出请求头,再从请求头中解析 Content-Length 字段获取请求体字节长度,只有当缓冲区攒齐 "请求头 + 完整请求体" 的全部数据,才算拆分出一个合法完整的 POST 请求,才能交给上层协议层处理。
七、运行结果

我们当前的 HTTP 服务器共有两种测试运行方式,分别是浏览器访问测试与命令行工具测试,两种方式的底层逻辑一致,都是基于 TCP 连接完成 HTTP 请求与响应的交互,但操作形式和内部处理流程存在明显区别。
第一种是浏览器访问测试,这是最直观便捷的测试方式,我们只需在浏览器地址栏输入服务器的 IP 地址、端口号以及访问路径,浏览器便会在后台自动完成一系列操作,它会主动与服务器建立 TCP 连接,同时将我们输入的访问地址自动拼装为符合 HTTP 协议规范的 GET 请求报文,随后将报文发送至服务器,服务器处理完成后会返回对应的 HTTP 响应报文,浏览器接收后会自动解析响应报文,提取其中的响应正文内容并展示在页面上,整个过程中请求报文的拼装、发送以及响应报文的解析都由浏览器自动完成,我们无需关注底层报文格式。
第二种是命令行工具测试,可使用 Telnet 或 curl 工具,这种方式需要我们手动参与 HTTP 报文的构造,以 Telnet 为例,我们先通过命令与服务器建立 TCP 连接,之后需要严格按照 HTTP 协议格式手动输入完整的请求报文,包括请求行、请求头、空行以及可选的请求体,服务器接收并处理报文后,会直接将完整的响应报文原封不动返回至终端,我们能够清晰看到响应状态码、响应头和响应正文的全部内容,curl 工具则介于浏览器与 Telnet 之间,只需输入简洁的访问命令,无需手动编写完整报文,工具会自动拼装请求报文,同时直接在终端展示响应正文,不会隐藏底层交互细节。两种测试方式都能验证服务器的运行效果,且契合当前短连接的设计,服务器在完成一次请求与响应的交互后,都会主动关闭 TCP 连接,结束本次通信。
八、完整代码
Http.hpp
cpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <sstream>
#include <unordered_map>
#include <fstream>
#include <functional>
#include "Logger.hpp"
// #define DEBUG 1
const std::string linesep = "\r\n";
const std::string spacesep = " ";
const std::string headersep = ": ";
const std::string suffixsep = ".";
const std::string argsep = "?";
const std::string g_http_version = "HTTP/1.0";
const std::string g_first_page = "index.html";
const std::string g_wwwroot = "wwwroot";
const std::string page_404 = "./wwwroot/404.html";
using namespace NS_LOG_MODULE;
class Util
{
public:
// 1. 空:没有完整行
// 2. \r\n: 读取到了空行
// 3. 其他情况: 分析到了一行具体内容
static std::string ReadLine(std::string &str)
{
auto pos = str.find(linesep);
if (pos == std::string::npos)
return std::string();
std::string line = str.substr(0, pos);
str.erase(0, line.size() + linesep.size());
if (line.empty())
return linesep;
return line;
}
// 如果我们要读取二进制文件,vector<char>
static std::string ReadFile(const std::string &filename)
{
// 采用二进制方式读取数据
std::ifstream in(filename, std::ios::binary);
if (!in.is_open())
{
return std::string();
}
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
std::string content;
content.resize(filesize);
in.read((char *)content.c_str(), filesize);
in.close();
return content;
}
};
// 1. 你怎么知道你读取到的是完整的报文 --
class HttpRequest
{
private:
bool ParseReqLine(std::string &httpstr)
{
std::string req_line = Util::ReadLine(httpstr);
if (req_line.empty() || req_line == linesep)
return false;
std::stringstream ss(req_line);
ss >> _method >> _uri >> _version;
if (_uri == "/")
_uri += g_first_page; // uri: /-> uri: /index.html
_uri = g_wwwroot + _uri; // wwwroot/index.html: web 根目录 // wwwroot/Login
// wwwroot/index.html
// wwwroot/index.css
// wwwroot/index.js
// wwwroot/image/xxx.jpg,png...
// wwwroot/video/xxx.mpx
auto pos = _uri.rfind(suffixsep);
if (pos == std::string::npos)
_suffix = ".html";
else
_suffix = _uri.substr(pos);
return true;
}
bool ParseHeaderkv(std::string &httpstr)
{
std::string header_line;
do
{
header_line = Util::ReadLine(httpstr);
if (header_line.empty())
return false;
if (header_line != linesep)
{
auto pos = header_line.find(headersep);
if (pos == std::string::npos)
{
return false;
}
std::string key = header_line.substr(0, pos);
std::string value = header_line.substr(pos + headersep.size());
// _header[key] = value;
_header.insert(std::make_pair(key, value));
// _header.insert({key, value});
}
} while (header_line != linesep);
return true;
}
//
bool ParseText(std::string &httpstr)
{
// 1. 有没有正文??结合方法
if (strcasecmp(_method.c_str(), "GET") == 0)
{
auto pos = _uri.find(argsep);
if(pos == std::string::npos)
{
_text = std::string();
return true;
}
else
{
std::cout << "_URI: "<< _uri << std::endl;
std::string temp = _uri;
_uri = temp.substr(0, pos);
std::cout << "_URI: "<< _uri << std::endl;
_text = temp.substr(pos+argsep.size());
std::cout << "_text: "<< _text << std::endl;
}
}
else
{
if (_header.find("Content-Length") == _header.end())
{
_text = "";
}
// 2. 正文多大?从哪开始?
// POST方法
int content_len = std::stoi(_header["Content-Length"]);
_text = httpstr.substr(0, content_len);
httpstr.erase(0, content_len);
}
return true;
}
public:
// 反序列化
bool Deserialize(std::string &httpstr)
{
std::cout << httpstr;
// 1. 解析请求行
bool n = ParseReqLine(httpstr);
(void)n;
// LOG(LogLevel::DEBUG) << "_method# " << _method;
// LOG(LogLevel::DEBUG) << "_uri# " << _uri;
// LOG(LogLevel::DEBUG) << "_version# " << _version;
// std::cout << httpstr;
// 2. 解析报头
n = ParseHeaderkv(httpstr);
(void)n;
_blank_line = linesep;
// for(auto &header : _header)
// {
// std::cout << header.first << " ## " << header.second << "\r\n\r\n";
// }
// std::cout << "start|" << httpstr << "|end";
// 3. 解析正文部分
n = ParseText(httpstr);
(void)n;
return true;
}
std::string Uri()
{
return _uri;
}
std::string RequestContent()
{
return Util::ReadFile(_uri);
}
std::string Suffix()
{
return _suffix;
}
void DebugPrint()
{
std::cout << "_version: " << _version << std::endl;
std::cout << "_uri: " << _uri << std::endl;
std::cout << "_method: " << _method << std::endl;
for (auto &header : _header)
{
std::cout << "==>" << header.first << " # " << header.second << std::endl;
}
std::cout << _blank_line;
std::cout << _text << std::endl;
}
std::string Text()
{
return _text;
}
// 反序列化
private:
// 结构化字段
std::string _method;
std::string _uri; //
std::string _version;
std::unordered_map<std::string, std::string> _header;
std::string _blank_line;
std::string _text;
// 私有数据
std::string _suffix; // 后缀类型
};
class HttpResponse
{
private:
std::string CodeToDesc(int code)
{
switch (code)
{
// 1xx
case 100:
return "Continue";
case 101:
return "Switching Protocols";
// 2xx
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 204:
return "No Content";
case 206:
return "Partial Content";
// 3xx
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 304:
return "Not Modified";
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
// 4xx
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 413:
return "Payload Too Large";
case 429:
return "Too Many Requests";
// 5xx
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
// 默认情况
default:
return "Unknown Status Code";
}
}
public:
HttpResponse() : _version(g_http_version), _code(0), _blank_line(linesep)
{
}
void SetCode(int code)
{
_code = code;
_code_desc = CodeToDesc(_code);
}
void SetBody(const std::string &content)
{
_text = content;
}
int BodySize()
{
return _text.size();
}
void AddHeader(const std::string &key, const std::string &value)
{
// 如果存在就覆盖,如果不存在就添加
_header[key] = value;
}
std::string Serialize()
{
// 构建状态行
std::string respstr = _version;
respstr += spacesep;
respstr += std::to_string(_code);
respstr += spacesep;
respstr += _code_desc;
respstr += linesep;
// 构建报头
for (auto &header : _header)
{
std::string line = header.first + headersep + header.second + linesep;
respstr += line;
}
// 添加换行符
respstr += _blank_line;
// 添加正文
respstr += _text;
return respstr;
}
private:
std::string _version;
int _code;
std::string _code_desc;
std::unordered_map<std::string, std::string> _header;
std::string _blank_line;
std::string _text;
};
class MiniType
{
public:
static std::string Suffix2MimeType(const std::string &suffix)
{
auto iter = _mime_map.find(suffix);
if (iter != _mime_map.end())
return iter->second;
else
return "text/html";
}
private:
static std::unordered_map<std::string, std::string> _mime_map;
};
// 初始化10种常见的文件后缀到Content-Type的映射
std::unordered_map<std::string, std::string> MiniType::_mime_map = {
// 文本类型
{".txt", "text/plain"},
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
// 图片类型
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
// 应用类型
{".pdf", "application/pdf"},
{".json", "application/json"},
{".xml", "application/xml"},
// 脚本类型
{".js", "application/javascript"},
// 其他常用类型
{".zip", "application/zip"},
{".mp3", "audio/mpeg"},
{".mp4", "video/mp4"}
};
using service_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;
class HttpProtocol
{
public:
HttpProtocol()
{}
void RegisterService(const std::string &uri, service_t service)
{
std::string key = g_wwwroot + uri;
_http_services[key] = service; // key -> wwwroot/Login
}
bool IsReqService(const std::string &uri)
{
auto iter = _http_services.find(uri);
if (iter == _http_services.end())
return false;
else
return true;
}
std::string HandlerHttpRequest(std::string &req)
{
HttpRequest http_req;
http_req.Deserialize(req);
HttpResponse http_resp;
if (IsReqService(http_req.Uri()))
{
// 功能路由
_http_services[http_req.Uri()](http_req, http_resp);
}
else
{
std::string content = http_req.RequestContent();
if (content.empty())
{
// http_resp.SetCode(404);
// http_resp.SetBody(Util::ReadFile(page_404));
// http_resp.AddHeader("Content-Length", std::to_string(http_resp.BodySize()));
http_resp.SetCode(302);
http_resp.AddHeader("Location", "/404.html");
}
else
{
http_resp.SetCode(200);
http_resp.AddHeader("Content-Length", std::to_string(content.size()));
http_resp.AddHeader("Content-Type", MiniType::Suffix2MimeType(http_req.Suffix()));
http_resp.AddHeader("Connection", "close");
http_resp.AddHeader("Set-Cookie", "username=peter;");
http_resp.SetBody(content);
}
}
return http_resp.Serialize();
}
~HttpProtocol()
{
}
private:
std::unordered_map<std::string, service_t> _http_services; // 根据uri(服务路径)进行功能路由
};
HttpServer.cc
cpp
#include "TcpServer.hpp"
#include "Http.hpp"
#include <memory>
// 使用HTTP 实现restful风格的服务接口
// GET /Login HTTP/1.1
// GET /exec HTTP/1.1
//
// ls -a -l
// 可不可以是另一个http的client?
void Login(HttpRequest&req, HttpResponse &resp)
{
std::cout << "\nLogin functon been called!" << std::endl;
req.DebugPrint();
std::string data = req.Text();
std::cout << "data is: " << data << std::endl; // name=aaaaa&passwd=bbbbb
// 根据分隔符 name aaaaa passwd bbb
// 访问数据库, 查找数据库
resp.SetCode(200);
resp.AddHeader("Content-Type", MiniType::Suffix2MimeType(".txt"));
resp.SetBody("Login success!");
resp.AddHeader("Content-Length", std::to_string(resp.BodySize()));
}
void Connect(HttpRequest&req, HttpResponse &resp)
{
// 我们的http是浏览器或者其他客户端的服务端
// 但是我们的服务端,可不可以是其他server的客户端!
}
void OJ(HttpRequest&req, HttpResponse &resp)
{
// pipe();
// if(fork() == 0)
// {
// dup();
// execl("/usr/bin/gcc", ....);
// }
// 从管道中,读取python处理完成的结果!
}
void Register(HttpRequest&req, HttpResponse &resp)
{
}
void GetProductList(HttpRequest&req, HttpResponse &resp)
{
}
void Search(HttpRequest&req, HttpResponse &resp)
{
}
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t" << proc << " port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t port = std::stoi(argv[1]);
// 1. 定义HTTP协议
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
// GET /yuyinshibie HTTP/1.1
//
// {xxxxx}
protocol->RegisterService("/Login", Login);
protocol->RegisterService("/Register", Register);
protocol->RegisterService("/Search", Search);
protocol->RegisterService("/api/getprocutlist", GetProductList);
// 2. 定义网络对象
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
[&protocol](std::string &inbuffer)->std::string{
return protocol->HandlerHttpRequest(inbuffer);
}, port
);
// 3. 启动
tsvr->Loop();
return 0;
}
TcpServer.hpp
cpp
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "SSL.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>
static uint16_t gport = 8080;
using namespace NS_SOCKET_MODULE;
using Handler_t = std::function<std::string(std::string &)>;
class TcpServer
{
public:
TcpServer(Handler_t handler, uint16_t port = gport)
: _port(port),
_listensock(std::make_unique<TcpSocket>()),
_handler(handler)
{
_listensock->BuildTcpSocketMethod(_port); // 使用创建listensockfd的模版方法!
LOG(LogLevel::INFO) << "create listen socket success: " << _listensock->Sockfd();
}
void Loop()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
InetAddr clientaddr;
auto sockfd = _listensock->Accepter(clientaddr);
if (!sockfd)
continue;
LOG(LogLevel::DEBUG) << "get a new link, socket address: " << clientaddr.ToString() << " sockfd: " << sockfd->Sockfd();
if (fork() == 0)
{
// child
service(sockfd, clientaddr);
sockfd->Close();
exit(0);
}
}
}
~TcpServer()
{
}
private:
// 短服务
void service(std::shared_ptr<Socket> sockfd, InetAddr &clientaddr)
{
std::string inbuffer, outbuffer;
outbuffer.clear();
// while (1)
// {
int n = sockfd->Recv(&inbuffer); // 读到完整的HTTP请求
if (n <= 0)
{
LOG(LogLevel::WARNING) << "recv: client quit, " << clientaddr.ToString();
return;
}
// 解密工作
// inbuffer = ssl.解密(inbuffer);
if (_handler)
outbuffer = _handler(inbuffer);
// if (outbuffer.empty())
// return;
// 加密工作
// outbuffer = ssl.加密(outbuffer);
n = sockfd->Send(outbuffer);
if (n < 0)
{
LOG(LogLevel::WARNING) << "send: client quit, " << clientaddr.ToString();
}
// }
}
private:
uint16_t _port;
std::unique_ptr<Socket> _listensock;
Handler_t _handler;
};
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include "Logger.hpp"
#include "InetAddr.hpp"
namespace NS_SOCKET_MODULE
{
using namespace NS_LOG_MODULE;
static const int gbacklog = 16;
enum
{
OK = 0,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 模版方法模式!
class Socket
{
public:
~Socket()
{
}
protected:
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie() = 0;
// virtual ssize_t Recv() = 0;
// virtual void Send() = 0;
public:
virtual std::shared_ptr<Socket> Accepter(InetAddr &addr) = 0;
virtual int Sockfd() = 0;
virtual int Recv(std::string *out) = 0;
virtual int Send(const std::string &in) = 0;
virtual void Close() = 0;
virtual bool Connect(InetAddr &addr) = 0;
public:
void BuildTcpSocketMethod(uint16_t port) // 模版方法
{
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie();
}
void BuildTcpClientSockMethod()
{
CreateSocketOrDie();
}
// void BuildUdpSocketMethod()
// {
// CreateSocketOrDie();
// BindSocketOrDie();
// }
};
class TcpSocket : public Socket
{
public:
TcpSocket() : _sockfd(0)
{
}
TcpSocket(int sockfd): _sockfd(sockfd)
{}
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
}
void BindSocketOrDie(uint16_t port) override
{
InetAddr addr(port);
if (bind(_sockfd, addr.NetAddress(), addr.Len()) != 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
}
void ListenSocketOrDie() override
{
if (listen(_sockfd, gbacklog) != 0)
{
LOG(LogLevel::FATAL) << "listen socket error";
exit(LISTEN_ERR);
}
}
std::shared_ptr<Socket> Accepter(InetAddr &clientaddr) override
{
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int sockfd = accept(_sockfd, CONV(&addr), &len);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
return nullptr;
}
clientaddr = addr;
return std::make_shared<TcpSocket>(sockfd);
}
int Sockfd() override
{
return _sockfd;
}
int Recv(std::string *out) override
{
char inbuffer[40960];
ssize_t n = recv(_sockfd, inbuffer, sizeof(inbuffer)-1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*out = inbuffer;
}
return n;
}
int Send(const std::string &in) override
{
return send(_sockfd, in.c_str(), in.size(), 0);
}
void Close() override
{
if(_sockfd>=0)
{
close(_sockfd);
_sockfd = -1;
}
}
bool Connect(InetAddr &addr) override
{
int n = connect(_sockfd, addr.NetAddress(), addr.Len());
if(n < 0)
return false;
else
return true;
}
~TcpSocket() {}
private:
int _sockfd;
};
// class UdpSocket: public Socket
// {
// };
} // namespace name
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define CONV(address) ((struct sockaddr *)address)
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr()
{
}
InetAddr(const struct sockaddr_in &address) : _address(address), _len(sizeof(address))
{
// _ip = inet_ntoa(_address.sin_addr);
char ipstr[32];
inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
_ip = ipstr;
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0") : _ip(ip), _port(port)
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // h->n
//_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
inet_pton(AF_INET, ip.c_str(), &(_address.sin_addr));
_len = sizeof(_address);
}
bool operator==(const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
void operator=(struct sockaddr_in &addr)
{
_address = addr;
char ipstr[32];
inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
_ip = ipstr;
_port = ntohs(_address.sin_port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
struct sockaddr *NetAddress()
{
return CONV(&_address);
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{
}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
Loger.hpp
cpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <memory>
#include <sstream>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include "Mutex.hpp"
namespace NS_LOG_MODULE
{
enum class LogLevel
{
INFO,
WARNING,
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKNOWN";
}
}
// 1. 时间戳 2. 日期+时间
std::string GetCurrentTime()
{
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time); // r: 可重入函数
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec);
return timestr;
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志刷新策略, 日志将来要向显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl; // ??
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 文件策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
: _logpath(path),
_logfilename(name)
{
LockGuard lockguard(_mutex);
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex);
if (!_logpath.empty() && _logpath.back() != '/')
{
_logpath += "/";
}
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << "failed" << std::endl;
return;
}
out << message << "\n";
out.close();
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
// 交给大家
// const std::string defaultfilename = "log.info";
// const std::string defaultfilename = "log.warning";
// const std::string defaultfilename = "log.fatal";
// const std::string defaultfilename = "log.error";
// const std::string defaultfilename = "log.debug";
// 文件策略&&分日志等级来进行保存
// class FileLogLevelStrategy : public LogStrategy
// {
// public:
// private:
// };
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略,进行刷新
class Logger
{
// 日志的生成:
// 构建日志字符串
public:
Logger()
{
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类, 标识一条完整的日志信息
// 一条完整的日志信息 = 做半部分固定部分 + 右半部分不固定部分
// LogMessage RAII风格的方式,进行刷新
class LogMessage
{
public:
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 先构建出来左半部分
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage对象,方便下次继续进行<<
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo; // 一条完整的日志信息
// 一个引用,引用外部的Logger类对象
Logger &_logger; // 方便我们后续进行策略式刷新
};
// 这里已经不是内部类了
// 故意采用拷贝LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
#endif
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Ptr()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard // RAII风格代码
{
public:
LockGuard(Mutex &lock):_lockref(lock)
{
_lockref.Lock();
}
~LockGuard()
{
_lockref.Unlock();
}
private:
Mutex &_lockref;
};
Makefile
cpp
httpserver:HttpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f httpserver
九、总结
本文详细介绍了HTTP协议的工作模式及其自定义实现过程。HTTP采用客户端-服务器模式,基于TCP协议实现一问一答的无状态通信,包括建立连接、发送请求、返回响应和断开连接四个步骤。文章重点阐述了如何从零实现一个简易HTTP服务器,包括请求解析、响应构建、动态路由等核心功能模块。通过代码示例展示了HTTP报文解析、文件读取、MIME类型匹配等关键技术的实现,并比较了短连接和长连接的优缺点。最后提供了完整的服务器代码实现,支持浏览器和命令行工具测试,帮助读者深入理解HTTP服务器的底层工作原理。
谢谢大家的观看!
这是 IP + 端口。但我们在现实上网时绝对不会这样输,我们会输:

ReadLine() 函数是 HTTP 请求解析的核心工具,它以 \r\n 为分隔符,从原始报文字符串中一行一行地读取内容,并在读取后从原字符串中删除已处理的部分。
当 HttpRequest 解析完 URI (比如 ./wwwroot/index.html),业务逻辑就会调用 RequestContent(),这个函数再调用 Util::ReadFile(_uri),把文件读出来。读出来的内容,会被塞进 HttpResponse 的响应正文里,最后发给浏览器。

ParseText() 函数是 HTTP 请求解析的第三步,也是最后一步,负责解析请求正文。这个函数依赖 ParseReqLine() 函数解析出的 _method 来判断走哪个分支;也依赖 ParseHeaderkv() 函数解析出的 _header["Content-Length"],来读取 POST 请求正文得长度。如果是 GET 请求表示正文为空,但要解析 URI 里的查询参数 (?key=val),如果是 POST 请求就要根据 Content-Length 读取正文数据。但是无论是哪种请求方法,_text 里的内容最终要么是 GET 的查询参数,要么是 POST 正文,业务逻辑会用它处理请求 (比如登录校验、表单提交)。
_uri 是在 ParseReqLine() 里被解析、处理过的路径;对于 GET 请求,ParseText() 还会把 URI 中的查询参数剥离,只保留纯路径部分;业务逻辑拿到这个路径,就可以调用 Util::ReadFile(_uri) 读取本地文件。

这个函数返回的是解析出的请求正文或 GET 查询参数。_text 是在 ParseText() 里解析的,GET 请求存的是 URI 中的查询参数,POST 请求存的是请求正文;业务逻辑处理 POST 请求(比如登录、表单提交)时,会直接通过 Text() 获取用户提交的数据;也可以用来处理 GET 请求的查询参数。
把所有解析结果完整打印出来,用于调试。它把 HttpRequest 类里的所有私有成员变量(_version、_uri、_method、_header、_blank_line、_text)全部输出;可以快速验证解析是否正确,比如 _uri 是否拼接了正确的本地路径、_header 是否完整解析了所有请求头。
这些变量就是把浏览器发来的一整坨字符串,拆解后存起来的所有信息,是后续业务处理、文件读取、响应头设置的全部依据。

_version 是 HTTP 协议版本号,比如 "HTTP/1.0" 或 "HTTP/1.1"。是响应报文的状态行开头部分,响应报文的格式是:版本 状态码 描述\r\n。在构造函数里被初始化为我们之前定义的全局常量 g_http_version,序列化时会被拼在响应报文最前面,决定整个响应使用的 HTTP 版本。










用初始化列表进行构造:

直接返回成员变量 _text(响应正文)的字节长度。它的存在,是为了设置 HTTP 响应头中的 Content-Length 字段。HTTP 协议的要求浏览器收到响应后,必须知道正文有多长,才能完整接收并正确解析。Content-Length 是 HTTP 响应中必须的字段,它的值就是响应正文的字节数。
Serialize() 函数是 HttpResponse 类的核心灵魂,和 HttpRequest::Deserialize() 是对称的,负责把前面设置的所有结构化数据,拼出一整个可以直接发给浏览器的标准 HTTP 响应报文。设置好状态码、响应头、正文的 HttpResponse 对象。序列化之后的内容就符合 HTTP 规范的响应报文字符串,可以直接通过 send 发给浏览器。
MiniType 类是 HTTP 服务器里的一个工具型辅助类,解决的是文件后缀和 MIME 类型的映射问题,是前面 HttpResponse 类的一个重要支撑。根据文件后缀,自动给出正确的 Content-Type 响应头值。浏览器收到服务器返回的文件内容时,必须通过 Content-Type 响应头知道这是什么类型的数据,才能正确渲染:
这是对 MiniType 类里静态成员 _mime_map 的初始化代码,定义了文件后缀到 HTTP 标准 Content-Type 的映射规则。因为 HTTP 协议要求服务器在响应中,必须通过 Content-Type 报头,告诉浏览器「我发给你的是什么类型的数据」,浏览器才知道该怎么处理它:
是一个 unordered_map,专门存「请求 URI → 服务回调函数」的键值对。Key 请求的 URI 路径,比如 "/login"、"/query";Value 对应的 service_t 类型回调函数。它是我们服务器的路由中心,实现了根据请求路径,分发到不同业务逻辑的功能。比如客户端请求 "/login",就调用登录验证的回调;请求 "/query",就调用数据查询的回调;普通静态文件请求,就走默认的文件读取流程。后面的代码中,比如说 RegisterService 把 URI 和回调函数,插入到这个 map 里;IsReqService 判断请求的 URI 是否在这个 map 里;HandlerHttpRequest 如果在 map 里,就取出回调函数并调用。
函数的第一个参数 uri 是客户端要访问的服务路径,比如 /login,使用常引用传递,避免字符串拷贝,提高效率。第二个参数 service 是对应的业务处理函数,这个函数必须符合我们前面定义的service_t 格式,能够接收 HttpRequest 和 HttpResponse 对象,在函数内部完成动态逻辑处理。
这个函数是 HttpProtocol 类提供的路由判断工具函数,核心作用是:判断当前请求的 URI,是不是我们注册过的动态服务路径。它就像一个 "问路" 的环节,服务器拿到请求后,先问一句:"这个路径是要走动态业务,还是直接读静态文件?",这个函数就是回答这个问题的。所以后续就是根据这个函数的返回值,来决定走动态服务逻辑,还是走静态文件逻辑。
HandlerHttpRequest() 函数是整个 HttpProtocol 类的核心入口函数,也是服务器收到客户端请求后,处理流程的总调度。它把前面讲过的 HttpRequest、HttpResponse、MiniType、动态路由功能,全部串成了一条完整的请求处理闭环,是我们 HTTP 服务器的 "大脑"。它是前面所有模块的最终应用场景:HttpRequest 负责解析,HttpResponse 负责拼装,MiniType 负责类型匹配,动态路由负责分发;它是服务器和客户端之间的桥梁:收到请求后交给它处理,处理完直接返回响应,业务代码不用关心底层细节;它实现了请求处理的闭环:从解析请求到返回响应,全程由它调度,结构清晰,扩展性强。
当浏览器访问 /login 时:IsReqService 判断这是个动态服务;
函数执行完,http_resp 就被填好了登录校验的结果,之后再被序列化发给浏览器。
返回值就调用 HttpResponse::Serialize(),把前面设置好的状态码、响应头、正文,拼成一整个符合 HTTP 格式的响应字符串;返回的字符串可以直接通过 send 发给客户端,完成一次完整的请求响应流程。
Login() 函数是我们自定义的上层业务处理函数,专门用于处理用户登录请求。它严格遵循我们定义的 service_t 函数类型规范,由 HttpProtocol 框架在匹配到对应路由时自动调用。这个函数属于上层业务层,不属于底层框架。是由我们自己编写,灵活、可扩展、可随时修改。会通过 RegisterService() 函数注册到路由表中。当客户端访问 /login 时,HandlerHttpRequest() 会自动调用它。
这几个函数,本质上和上面的 Login() 函数是一个性质的,它们都是遵循 service_t 规范的上层业务处理函数,只是这里我们就不实现具体的函数逻辑了。最终会通过 RegisterService() 注册到 HttpProtocol 框架中,和 Login 一样,作为动态路由的 "目标执行单元"。当客户端访问对应的 URI 时,框架会自动调用这些函数,由它们来处理请求、设置响应。
main 函数是整个 HTTP 服务器程序的入口函数与组装函数,也是我们所有模块的 "总调度中心"。它不负责具体的协议处理或业务逻辑,只负责把前面讲过的 HttpProtocol 协议框架、业务处理函数和 TcpServer 网络模块,按正确的方式组装起来,最终启动并运行整个服务器。