bigywater.tech
Rust · 142

HTTP Server 从 Socket 到 Response

一个 Web 框架最底层在做什么?这篇不用任何框架,只用 TcpListener 写出能被 curl 访问的 HTTP 服务端:读请求、解析头部、读正文、路由、响应、持久连接、错误处理、并发,每一步都用 curl 与原始 socket 验证。

约 60 行
这个标准库 HTTP 服务端的代码量
400 / 431 / 405 / 404
服务端对畸形、过大、不支持的方法与未知路径的状态码
Re-using
curl 对 keep-alive 连接的确认

路由与响应

实测 · 本机执行输出
  curl GET /                        → 'HTTP/1.1 200 OK'  正文 'hello, request #1'
  curl GET /nope                    → 'HTTP/1.1 404 Not Found'  正文 'not found'
  curl HEAD /(只有头)                  → 'HTTP/1.1 200 OK'  正文 ''
  curl POST /echo -d "payload"      → 'HTTP/1.1 200 OK'  正文 'payload'
  curl DELETE /                     → 'HTTP/1.1 405 Method Not Allowed'  正文 'method not allowed'

下面是服务端的完整代码(release 编译)。客户端侧的原始字节见HTTP 请求的底层实现。

server.rs use std::{net::{TcpListener, TcpStream}, io::{Read, Write, BufRead, BufReader}, thread, time::Duration, collections::HashMap, sync::{Arc, atomic::{AtomicUsize, Ordering::SeqCst}}}; struct Req { method: String, path: String, version: String, headers: HashMap<String, String>, body: Vec<u8> } fn read_request(r: &mut BufReader<TcpStream>) -> Result<Option<Req>, (u16, &'static str)> { let mut line = String::new(); if r.read_line(&mut line).map_err(|_| (400, "read error"))? == 0 { return Ok(None); } let parts: Vec<&str> = line.trim_end().split(' ').collect(); if parts.len() != 3 { return Err((400, "bad request line")); } let mut headers = HashMap::new(); let mut total = line.len(); loop { let mut h = String::new(); let n = r.read_line(&mut h).map_err(|_| (400, "read error"))?; total += n; if total > 8192 { return Err((431, "headers too large")); } if h == "\r\n" || n == 0 { break; } let (k, v) = h.trim_end().split_once(':').ok_or((400, "bad header"))?; headers.insert(k.trim().to_ascii_lowercase(), v.trim().to_string()); } let len: usize = headers.get("content-length").map(|v| v.parse().map_err(|_| (400, "bad content-length"))).transpose()?.unwrap_or(0); let mut body = vec![0u8; len]; r.read_exact(&mut body).map_err(|_| (400, "short body"))?; Ok(Some(Req { method: parts[0].into(), path: parts[1].into(), version: parts[2].into(), headers, body })) } fn respond(w: &mut TcpStream, code: u16, reason: &str, ctype: &str, body: &[u8], keep: bool, head_only: bool) { let head = format!("HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: {}\r\n\r\n", code, reason, ctype, body.len(), if keep { "keep-alive" } else { "close" }); let _ = w.write_all(head.as_bytes()); if !head_only { let _ = w.write_all(body); } } fn handle(stream: TcpStream, counter: Arc<AtomicUsize>) { stream.set_read_timeout(Some(Duration::from_secs(2))).ok(); let mut w = stream.try_clone().unwrap(); let mut r = BufReader::new(stream); loop { match read_request(&mut r) { Ok(None) => break, Err((code, why)) => { respond(&mut w, code, why, "text/plain", why.as_bytes(), false, false); break; } Ok(Some(req)) => { let n = counter.fetch_add(1, SeqCst) + 1; let keep = req.version == "HTTP/1.1" && req.headers.get("connection").map(|v| !v.eq_ignore_ascii_case("close")).unwrap_or(true); let head_only = req.method == "HEAD"; match (req.method.as_str(), req.path.as_str()) { ("GET" | "HEAD", "/") => respond(&mut w, 200, "OK", "text/plain; charset=utf-8", format!("hello, request #{}\n", n).as_bytes(), keep, head_only), ("POST", "/echo") => respond(&mut w, 200, "OK", "application/octet-stream", &req.body, keep, false), ("GET", "/headers") => { let mut ks: Vec<_> = req.headers.iter().collect(); ks.sort(); respond(&mut w, 200, "OK", "text/plain", ks.iter().map(|(k, v)| format!("{}={}\n", k, v)).collect::<String>().as_bytes(), keep, false) } ("GET" | "POST" | "HEAD", _) => respond(&mut w, 404, "Not Found", "text/plain", b"not found", keep, head_only), _ => respond(&mut w, 405, "Method Not Allowed", "text/plain", b"method not allowed", keep, false), } if !keep { break; } } } } } fn main() { let l = TcpListener::bind("127.0.0.1:0").unwrap(); println!("{}", l.local_addr().unwrap().port()); std::io::stdout().flush().unwrap(); let c = Arc::new(AtomicUsize::new(0)); for s in l.incoming() { let c = c.clone(); let s = s.unwrap(); thread::spawn(move || handle(s, c)); } }

请求头

实测 · 本机执行输出
    accept=*/*
    host=127.0.0.1:49764
    user-agent=curl/8.7.1

TCP 的基础见TCP Listener 与 TcpStream。

keep-alive

实测 · 本机执行输出
    * Connected to 127.0.0.1 (127.0.0.1) port 49764
    * Connection #0 to host 127.0.0.1 left intact
    * Re-using existing connection with host 127.0.0.1
    * Connection #0 to host 127.0.0.1 left intact
  两次响应的正文: ['hello, request #7', 'hello, request #8']

畸形请求

实测 · 本机执行输出
  请求行只有两段                                  → HTTP/1.1 400 bad request line
  头部没有冒号                                   → HTTP/1.1 400 bad header
  Content-Length 不是数字                      → HTTP/1.1 400 bad content-length
  头部超过 8 KiB                               → HTTP/1.1 431 headers too large
  Content-Length 声明 100 却只发 5 字节(服务端等 2 秒读超时后关闭) → HTTP/1.1 400 short body

这只是演示,没有处理分块请求体、Expect: 100-continue、HTTP/1.0 的 keep-alive 等,不要当作生产服务端。

并发

实测 · 本机执行输出
  20 个并发请求得到的请求序号:[9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28](没有重复、没有缺失:True),累计服务器已处理 28 个

异步版本见Tokio TCP Echo Server。

分步演示

HTTP 服务端
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。curl 8.7.1;全部使用 127.0.0.1 与系统分配的端口
    • 没有验证:chunked 请求体、压缩、HTTPS、路径遍历等安全问题、慢速攻击(slowloris)、大文件的流式响应、线程数上限下的行为、与 hyper 等框架的性能对比
    京ICP备2026067502号-1京公网安备11010502063487号