这篇文章记录我从零实现一个 Linux C++ WebServer 的过程。项目从最小可运行服务器开始,逐步加入 HTTP/1.1 请求解析、响应构造和静态文件返回能力。

这个阶段的目标不是高性能,也不是完整复刻 TinyWebServer,而是先把基础链路和 HTTP 处理流程真正写明白。

第一阶段:最小 HTTP 服务器

最开始的目标很简单:

1
2
3
启动服务器:./server 8080
浏览器访问:http://127.0.0.1:8080
返回内容:Hello World

这一版虽然功能很小,但它跑通了 Linux 网络服务器最基础的流程:

1
socket -> bind -> listen -> accept -> recv -> send -> close

完成这个阶段后,我理解了几件事:

  • HTTP 响应不能只有正文,还必须有状态行、响应头、空行和响应体。
  • send 不一定一次发送完,应该根据返回值循环发送。
  • accept 得到的 client_fd 在请求处理结束后必须关闭。
  • 编译生成的 servermain 等二进制文件不应该提交到 Git。

第二阶段:静态页面和 HTTP 解析

固定返回 Hello World 之后,下一步是让服务器根据请求内容返回不同结果。

为此我添加了两个方向的内容:

一是静态页面目录:

1
2
3
4
5
static/
index.html
400BadRequest.html
404NotFound.html
405MethodNotAllowed.html

二是 HTTP/1.1 解析模块:

1
2
3
4
5
http-1.1/
request.hpp
request.cpp
response.hpp
response.cpp

其中 HttpRequest 用于保存请求信息:

  • method
  • path
  • version
  • header
  • query
  • body

HttpResponse 用于根据状态码、Header 和 body 构造完整 HTTP 响应。

这次实现中遇到的问题

1. 调试输出容易污染服务器行为

在实现请求解析时,我加了很多 cout 输出 method、path、header 等中间结果。

这对调试有帮助,但放在服务器主流程里会让终端输出变得很乱,也不利于后续测试。因此后面把这些调试输出删掉了。

更合理的做法是后续实现日志系统,用日志级别区分:

  • debug
  • info
  • warn
  • error

2. 请求解析要区分“没读完整”和“真的错误”

HTTP 请求从 socket 读出来时,不一定一次 recv 就完整。

所以解析结果不能只看错误类型,还要区分解析状态:

  • Completed:请求完整并解析成功。
  • Incompleted:请求还没读完整。
  • Error:请求格式确实错误。

当前代码已经有 ParseStatus,但主流程还需要更明确地使用它。

3. URL 路径和本地文件路径不能直接等同

客户端请求的是 URL:

1
GET /index.html HTTP/1.1

服务器真正读取的是本地文件:

1
static/index.html

这中间需要一个明确的映射步骤。

同时还要避免目录穿越,例如:

1
GET /../README.md HTTP/1.1

学习阶段可以先用简单规则处理:

  • / 映射到 static/index.html
  • /index.html 映射到 static/index.html
  • 包含 .. 的路径直接返回 400 Bad Request
  • 文件不存在返回 404 Not Found

4. Header 解析要注意下标来源

解析 Header 时,我把某一行切成了 line,但后续取 value 时仍然混用了原始 raw 的下标。

这类 bug 很常见:一旦字符串被切片,后续下标到底是相对于原始字符串,还是相对于当前行,必须统一。

更稳的思路是:

1
2
3
4
先拿到一整行 header line
在 line 里面找冒号
基于 line 截取 name 和 value
去掉 value 前后的空格

当前项目状态

当前服务器已经具备雏形:

  • 能监听端口。
  • 能接收 HTTP 请求。
  • 能初步解析 HTTP/1.1 请求。
  • 能构造 HTTP 响应。
  • 已经有静态错误页面。

但还不是一个稳定的静态文件服务器。

接下来需要先修复这些问题:

  • 静态文件路径构造。
  • query 解析。
  • Header value 截取。
  • body 长度判断。
  • ParseStatusParseErrorType 的处理边界。
  • Makefile 中不应把 .hpp 当作编译单元。

下一步计划

下一步目标是完成一个稳定的阻塞式静态文件服务器:

1
2
3
4
5
GET / HTTP/1.1               -> 200 OK + static/index.html
GET /index.html HTTP/1.1 -> 200 OK + static/index.html
GET /not-found.html HTTP/1.1 -> 404 Not Found
POST / HTTP/1.1 -> 405 Method Not Allowed
错误请求行 -> 400 Bad Request

完成这个阶段之后,再参考 TinyWebServer 的路线,进入:

  • HttpConnection 连接处理模块。
  • 非阻塞 socket。
  • epoll
  • 定时器。
  • 日志。
  • 线程池。

目前先不急着做并发。先把请求解析、文件映射和响应构造写稳,后面的 epoll 和线程池才有清楚的落点。

参考项目:https://github.com/qinguoyi/TinyWebServer