HTTP (HyperText Transfer Protocol) 是应用层协议,基于 TCP/IP 协议栈。它定义了客户端和服务器之间的通信格式。
协议栈层次:
┌─────────────────────┐
│ 应用层 (HTTP) │ ← 我们实现的层
├─────────────────────┤
│ 传输层 (TCP) │ ← socket API 封装的层
├─────────────────────┤
│ 网络层 (IP) │
├─────────────────────┤
│ 链路层 │
└─────────────────────┘
- 无状态协议:每个请求都是独立的,服务器不保存之前的请求信息
- 基于请求-响应模型:客户端发起请求,服务器返回响应
- 文本协议:HTTP/1.1 使用纯文本格式(易于调试)
- 默认端口:HTTP 使用 80 端口,HTTPS 使用 443 端口
在 HTTP 通信开始前,必须先建立 TCP 连接:
客户端 服务器
│ │
│─────── SYN ──────────────────>│ 1. 客户端发起连接
│ │
│<────── SYN-ACK ──────────────│ 2. 服务器确认
│ │
│─────── ACK ──────────────────>│ 3. 客户端确认
│ │
│ 连接建立,可以发送数据 │
对应代码:
- 服务器端:
bind()→listen()→accept() - 客户端:
connect()
连接关闭过程:
客户端 服务器
│ │
│─────── FIN ──────────────────>│ 1. 客户端请求关闭
│ │
│<────── ACK ──────────────────│ 2. 服务器确认
│ │
│<────── FIN ──────────────────│ 3. 服务器请求关闭
│ │
│─────── ACK ──────────────────>│ 4. 客户端确认
│ │
对应代码: close() 或 shutdown()
GET / HTTP/1.1\r\n ← 请求行 (方法 路径 协议版本)
Host: localhost\r\n ← 请求头
Connection: close\r\n ← 请求头
\r\n ← 空行 (标记 headers 结束)
[请求体] ← 可选的请求体 (POST/PUT 等)重要组成部分:
-
请求行:
- 方法:GET, POST, PUT, DELETE 等
- 路径:
/,/api/users,/index.html - 版本:
HTTP/1.1或HTTP/1.0
-
请求头 (Headers):
Host: 必须字段(HTTP/1.1)Connection:keep-alive或closeContent-Length: 请求体长度User-Agent: 客户端标识
-
空行:
\r\n\r\n标记 headers 结束 -
请求体 (Body):可选,POST/PUT 等携带数据
HTTP/1.1 200 OK\r\n ← 状态行 (协议版本 状态码 状态描述)
Content-Type: text/plain\r\n ← 响应头
Content-Length: 13\r\n ← 响应头
\r\n ← 空行 (标记 headers 结束)
Hello, World! ← 响应体状态码分类:
1xx:信息性响应2xx:成功 (200 OK, 201 Created)3xx:重定向 (301 Moved, 302 Found)4xx:客户端错误 (400 Bad Request, 404 Not Found)5xx:服务器错误 (500 Internal Server Error)
// C 实现
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP socket
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 允许端口复用
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 绑定地址
listen(server_fd, 10); // 开始监听,backlog=10# Python 实现
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
server.listen(5)关键点:
SO_REUSEADDR:避免 "Address already in use" 错误listen()的 backlog:等待队列的最大长度
问题:为什么需要循环接收?
TCP 是流式协议,数据可能:
- 分成多个 TCP 包到达
- 一次
recv()可能只读取部分数据 - 请求大小超过 buffer 容量
错误实现:
# ❌ 只调用一次 recv,可能丢失数据
request = client.recv(4096)正确实现:
# ✅ 循环接收,直到找到 headers 结束标记
request = b""
while True:
chunk = client.recv(1024)
if not chunk: # 连接关闭
break
request += chunk
if b"\r\n\r\n" in request: # 找到 headers 结束
breakC 语言:
// http_c/server.c:38-52
while ((bytes = read(client_fd, buffer + total_bytes, BUFFER_SIZE - total_bytes - 1)) > 0) {
total_bytes += bytes;
buffer[total_bytes] = '\0';
if (strstr(buffer, "\r\n\r\n") != NULL) {
break;
}
}- 优点:高性能,直接操作内存
- 缺点:需要手动管理内存,容易出错
Go 语言:
// http_go/server.go:29-44
var requestBuffer bytes.Buffer
tempBuffer := make([]byte, 1024)
for {
n, err := conn.Read(tempBuffer)
if err != nil {
break
}
requestBuffer.Write(tempBuffer[:n])
if bytes.Contains(requestBuffer.Bytes(), []byte("\r\n\r\n")) {
break
}
}- 优点:安全,自动内存管理
- 缺点:相比 C 略慢
Rust 语言:
// http_rust/server.rs:10-25
let mut request_buffer = Vec::new();
let mut temp_buffer = [0u8; 1024];
loop {
match stream.read(&mut temp_buffer) {
Ok(n) => {
request_buffer.extend_from_slice(&temp_buffer[..n]);
if request_buffer.windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
Err(e) => return,
}
}- 优点:内存安全 + C 级性能
- 缺点:学习曲线陡峭
Python 语言:
# http_python/server.py:15-23
request = b""
while True:
chunk = client.recv(1024)
if not chunk:
break
request += chunk
if b"\r\n\r\n" in request:
break- 优点:简洁易读
- 缺点:性能相对较低
# 必须包含所有必要的 headers
request = (
b"GET / HTTP/1.1\r\n"
b"Host: localhost\r\n" # HTTP/1.1 必须
b"Connection: close\r\n" # 告诉服务器完成后关闭连接
b"\r\n" # 空行标记结束
)
sock.sendall(request) # 确保发送完整方法一:循环直到连接关闭
# http_python/client.py
response = b""
while True:
chunk = sock.recv(1024)
if not chunk: # 连接关闭 = 数据接收完毕
break
response += chunk方法二:read_to_end (Rust)
// http_rust/client.rs
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).unwrap(); // 内部实现了循环问题原因:
服务器关闭后,端口处于 TIME_WAIT 状态(默认 2 分钟)
解决方法:
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));问题原因:
只调用一次 recv()/read(),数据可能分片到达
解决方法:
使用循环接收,检查结束标记 \r\n\r\n
问题原因: 服务器还在发送,客户端就停止接收
解决方法:
# ✅ 循环接收直到连接关闭
while True:
chunk = sock.recv(1024)
if not chunk:
break
response += chunk问题原因: 固定大小的 buffer 无法容纳大请求
解决方法:
// 检查 buffer 是否满
if (total_bytes >= BUFFER_SIZE - 1) {
// 返回 413 Payload Too Large
break;
}常见错误:
// ❌ 忘记 null 终止符
char buffer[1024];
read(fd, buffer, 1024);
printf("%s", buffer); // 可能读取越界
// ✅ 正确做法
buffer[bytes_read] = '\0';
printf("%s", buffer);问题原因: 客户端不知道数据何时结束
解决方法: 使用以下之一:
Connection: closeheader(简单)Content-Lengthheader(精确)- Chunked Transfer Encoding(流式)
注意事项:
// 网络字节序是大端,需要转换
server_addr.sin_port = htons(8080); // host to network short当前实现都是阻塞 I/O:
- 优点:简单直观
- 缺点:一个慢客户端会阻塞整个服务器
改进方向:
- 多线程(Go 和 Rust 示例已实现)
- 多进程
- 非阻塞 I/O + select/poll/epoll
- 异步 I/O (async/await)
生产代码必须检查所有返回值:
// ❌ 危险
int sock = socket(AF_INET, SOCK_STREAM, 0);
bind(sock, ...);
// ✅ 安全
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
if (bind(sock, ...) < 0) {
perror("bind");
close(sock);
return 1;
}缓冲区安全:
// ❌ 不检查边界
while ((bytes = read(fd, buffer + total, SIZE)) > 0) {
total += bytes; // 可能溢出
}
// ✅ 检查边界
while ((bytes = read(fd, buffer + total, SIZE - total - 1)) > 0) {
total += bytes;
if (total >= SIZE - 1) break;
}输入验证:
- 检查 HTTP 方法是否合法
- 验证 headers 格式
- 限制请求大小
- 防止路径遍历攻击 (
../../etc/passwd)
# 手动发送 HTTP 请求
nc localhost 8080
GET / HTTP/1.1
Host: localhost
# (按两次回车)# 显示详细信息
curl -v http://localhost:8080/
# 显示原始 HTTP 内容
curl --trace-ascii - http://localhost:8080/# 抓取 8080 端口的数据包
sudo tcpdump -i lo -A port 8080# 追踪服务器的系统调用
strace -e trace=network,read,write ./server主要区别:
| 特性 | HTTP/1.0 | HTTP/1.1 |
|---|---|---|
| 持久连接 | 默认关闭 | 默认开启 (keep-alive) |
| Host header | 可选 | 必须 |
| 管道化 | 不支持 | 支持 |
| 缓存控制 | 基础 | 增强 |
我们的实现:
- 使用
HTTP/1.1协议版本 - 通过
Connection: close禁用持久连接(简化实现)
复用 TCP 连接发送多个请求:
客户端 服务器
│── Request 1 ────>│
│<─── Response 1 ──│
│── Request 2 ────>│ (同一个 TCP 连接)
│<─── Response 2 ──│
- 二进制协议(非文本)
- 多路复用(一个连接多个请求)
- 服务器推送
- Header 压缩 (HPACK)
HTTP + TLS/SSL:
应用层:HTTP
安全层:TLS/SSL ← 加密层
传输层:TCP
核心要点:
✅ HTTP 是基于 TCP 的文本协议
✅ 必须使用循环接收数据(避免丢失)
✅ \r\n\r\n 标记 HTTP headers 结束
✅ 正确处理错误和边界情况
✅ 理解阻塞 vs 非阻塞 I/O
推荐阅读:
- RFC 7230-7235 (HTTP/1.1 规范)
- Unix Network Programming (Stevens)
- Beej's Guide to Network Programming
下一步学习:
- 实现 HTTP 路由
- 添加静态文件服务
- 支持 POST 请求体解析
- 实现连接池
- 使用 epoll/kqueue 实现高性能服务器
是的,传统的 HTTP/1.0、HTTP/1.1 和 HTTP/2 都是建立在 TCP 之上的。
传统上不行,但现在可以:
- HTTP/1.x 和 HTTP/2:必须使用 TCP
- HTTP/3:使用 UDP!基于 QUIC 协议(QUIC 是在 UDP 之上构建的)
应用层: HTTP (网页、API 等)
↓
传输层: TCP (可靠传输) 或 UDP (快速传输)
↓
网络层: IP (寻址和路由)
↓
链路层: 以太网、WiFi 等
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(保证数据到达、顺序正确) | 不可靠(可能丢包、乱序) |
| 速度 | 较慢(有握手、确认等开销) | 较快(无额外开销) |
| 用途 | HTTP/1.x、HTTP/2、文件传输、邮件 | HTTP/3、视频直播、游戏、DNS |
- 可靠性:网页内容必须完整准确地传输
- 顺序性:HTML、CSS、JS 文件需要按顺序接收
- 错误恢复:TCP 会自动重传丢失的数据包
虽然 UDP 不可靠,但 QUIC 在 UDP 之上实现了:
- 自己的可靠性机制(比 TCP 更高效)
- 更快的连接建立(0-RTT)
- 更好的多路复用(避免队头阻塞)
- HTTP 是应用层协议,定义了客户端和服务器如何交换数据
- TCP/UDP 是传输层协议,负责在网络中传输数据
- 传统 HTTP 使用 TCP 保证可靠性
- 现代 HTTP/3 使用基于 UDP 的 QUIC,在保持可靠性的同时提高性能