章节目录

TCP、HTTP 与手写最小服务器

本章目标

这一章把系统编程接到 Web 后端。你会理解 TCP 连接是什么,HTTP 请求和响应的文本格式是什么,以及当前 RinBlog 为什么可以只用标准库处理浏览器请求。

它是什么

TCP 是可靠字节流协议。HTTP/1.1 可以跑在 TCP 上。浏览器访问服务器时,会建立 TCP 连接,然后发送类似这样的文本:

GET /docs HTTP/1.1
Host: 127.0.0.1:7878

服务器返回类似这样的文本:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 12
Connection: close

Hello world

为什么需要

框架会隐藏这些细节,但理解它们能让你知道:

  • 路由本质是 method + path 的匹配。
  • 请求体长度通常来自 Content-Length
  • 响应必须告诉浏览器内容类型和长度。
  • HTML、Markdown 渲染结果最终都只是响应 body。

怎么使用

监听端口:

let listener = std::net::TcpListener::bind("127.0.0.1:7878")?;

for stream in listener.incoming() {
    let mut stream = stream?;
    // 读取请求,写出响应
}

写响应:

let body = "Hello";
let response = format!(
    "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
    body.len(),
    body
);

逐行解释

let path = request.path.split('?').next().unwrap_or("/");
  • request.path 是浏览器发来的路径,可能包含 query string。
  • .split('?') 按问号切开。
  • .next() 取问号前面的路径部分。
  • unwrap_or("/") 表示如果没有取到,就用首页路径兜底。
  • 这样 /docs?from=home 会按 /docs 路由。
match (request.method.as_str(), path) {
    ("GET", "/") => render_index_response(),
    _ if request.method == "GET" && docs::is_docs_path(path) => docs::render_docs_response(path),
    _ => not_found(),
}
  • match 同时匹配方法和路径。
  • ("GET", "/") 匹配首页。
  • guard 分支判断是否是文档路径。
  • _ 兜底处理未知路径。

常见坑

  • TCP 是字节流,不保证一次 read 就读完整个 HTTP 请求。
  • 请求头和 body 之间用 \r\n\r\n 分隔。
  • 手写服务器不等于生产可用,真实场景要考虑 TLS、并发、超时、chunked、压缩等。
  • Content-Length 应按字节长度计算,不是字符数量。

练习

  1. 用浏览器访问 /docs,观察服务端输出。
  2. 用命令行工具请求 /not-real,观察 404。
  3. 修改响应头的 Content-Type,看浏览器显示变化。
  4. 给路由增加一个 /health,返回 OK

造轮子任务

写一个迷你 HTTP 响应构造器 ResponseBuilder,支持设置状态码、内容类型、body,然后生成字节响应。目标是理解框架里的响应对象如何从便利 API 变成 HTTP 文本。

小结

Web 服务的底层并不神秘:监听 TCP、读取请求、匹配路由、生成响应。当前项目故意把这些步骤写出来,就是为了让你看到框架平时替你完成了什么。