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应按字节长度计算,不是字符数量。
练习
- 用浏览器访问
/docs,观察服务端输出。 - 用命令行工具请求
/not-real,观察 404。 - 修改响应头的
Content-Type,看浏览器显示变化。 - 给路由增加一个
/health,返回OK。
造轮子任务
写一个迷你 HTTP 响应构造器 ResponseBuilder,支持设置状态码、内容类型、body,然后生成字节响应。目标是理解框架里的响应对象如何从便利 API 变成 HTTP 文本。
小结
Web 服务的底层并不神秘:监听 TCP、读取请求、匹配路由、生成响应。当前项目故意把这些步骤写出来,就是为了让你看到框架平时替你完成了什么。