HTTP

1096 字
5 分钟
HTTP

整个代码实现了一个经典的多进程并发 HTTP 静态 Web 服务器。它的核心思想是将**网络传输层(TCP)和应用层(HTTP)**解耦。

代码的运行逻辑可以划分为五个主要阶段,我们直接从程序的入口 main 函数开始梳理:

1. 启动与初始化阶段 (Main.cc -> Http.hpp -> TcpServer.hpp)#

一切从 [Main.cc](code-assist-path:d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\Main.cc "d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\Main.cc") 中的 main 函数开始:

  1. 获取端口号:程序启动时从命令行读取用户指定的端口号(例如 ./server 8080)。
  2. 实例化 Http 服务器:调用 std::make_unique<Http>(port) 创建 HTTP 服务器对象。
    • 在 Http 的构造函数中,它会立刻创建一个底层的 TcpServer 对象。
    • TcpServer 被创建时,会初始化自己的 _listensockptr(一个 TcpSocket 对象),并调用 BuildTcpSocketMethod(port),完成底层 TCP 的三大步:socket() 创建套接字、bind() 绑定端口、listen() 进入监听状态。
  3. 启动服务器:调用 httpsvr->Start()。

2. 注入回调与主循环 (Http.hpp -> TcpServer.hpp)#

当调用 Http::Start() 时:

  1. 它是通过调用底层 tsvrp->Start(...) 真正跑起来的,并且传入了一个 Lambda 表达式。这个 Lambda 实际上把 Http::HandlerHttpRquest 打包成了一个回调函数(callback)。这就做到了网络层不知道应用层在干嘛,只负责把收到的连接扔给回调。
  2. 随后程序进入 TcpServer::Start 的 while(_isrunning) 死循环中。
  3. 阻塞等待:主线程在这个循环里调用 _listensockptr->Accept(&client),程序会在这里挂起,直到有浏览器(客户端)发起 TCP 连接。

3. 多进程并发处理 (TcpServer.hpp)#

当一个浏览器连接进来后,Accept 成功返回一个专门用于和这个客户端通信的通信套接字 sock:

  1. Fork 子进程:TcpServer 调用 fork() 创建子进程。
  2. 孙子进程机制(孤儿进程):为了防止产生僵尸进程,这里用了一个非常经典的 Linux 系统编程技巧:
    • 父进程直接 waitpid 回收子进程。
    • 子进程里面再 fork() 一次产生孙子进程,然后子进程立刻 exit() 退出。
    • 孙子进程因为失去了父进程,会被系统的 init (或 systemd) 进程接管。
  3. 执行任务:这个孙子进程负责执行之前传进来的回调函数 callback(sock, client),也就是去处理具体的 HTTP 逻辑,处理完后自动关闭 socket 并退出。父进程则毫无负担地继续循环 Accept 下一个连接。

4. HTTP 报文处理与响应 (Http.hpp -> Util.hpp)#

逻辑现在来到了孙子进程执行的 Http::HandlerHttpRquest 函数:

  1. 读取请求:通过 sock->Recv(&httpreqstr) 从 TCP 接收缓冲区把浏览器发来的 HTTP 请求报文(一段纯字符串)读出来。
  2. 解析请求 (Request):
    • 实例化 HttpRequest 并调用 Deserialize 进行反序列化。
    • 利用 Util::ReadOneLine 提取出第一行(请求行),并用 stringstream 分割出方法(如 GET)、URI(如 / 或 /image.jpg)和协议版本。
    • 将请求的 URI 映射到本地磁盘的 ./wwwroot 目录下(例如 / 被转换为 [./wwwroot/index.html](code-assist-path:d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\wwwroot\index.html "d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\wwwroot\index.html"))。
  3. 构建响应 (Response):
    • 实例化 HttpResponse,把刚才映射好的文件路径喂给它。
    • 调用 MakeResponse():底层通过 Util::ReadFileContent 去读取磁盘文件。如果文件存在,状态码设为 200 OK 并附上 Content-Length 头;如果文件不存在,状态码设为 404 Not Found 并返回一段写死的错误 HTML 页面。
  4. 序列化与发送:
    • 调用 HttpResponse::Serialize(),将构建好的响应对象按照 HTTP 协议标准拼装成一段长字符串:状态行 + 响应头 + 空行 + 网页正文。
    • 调用 sock->Send(response_str) 将这段字符串通过 TCP 发送回浏览器。

5. 日志贯穿始终 (Log.hpp)#

在上述所有流程的运行过程中,你会发现穿插着大量的 LOG(LogLevel::xxx) << "...":

  • 这得益于 [Log.hpp](code-assist-path:d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\Log.hpp "d:\HuaweiMoveData\Users\HUAWEI\Desktop\http\Log.hpp") 中巧妙的重载 operator<< 机制。
  • 只要发生重要的状态变更(如 Bind 成功、Accept 成功、甚至发生错误退出),它都会在内存中利用 stringstream 将时间戳、日志等级、进程 ID、文件名和行号拼装成一行日志。
  • 并在 LogMessage 对象析构时(即这一行代码执行完毕离开作用域时),触发写入策略,将日志输出到控制台或写入 /var/log/my.log 中。

总体来说,整个架构非常清晰:Main 驱动,TcpServer 负责并发和网络 I/O,Http 层负责应用层协议编解码,Util 辅助文件操作,Log 负责监控运行状态。

Generated by Gemini 3.1 Pro Preview

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
HandlerOne
おかえり
公告
本站已整理 138 篇技术笔记,并按 8 条主线排成学习路线(141 站);站内搜索与桌宠对话均已上线。
分类
标签
最新动态
站点统计
文章
138
分类
12
标签
42
总字数
597,044
站点信息
构建平台
腾讯云 · Nginx
博客版本
v6.16.8
文章目录