Skip to content

Latest commit

 

History

History
596 lines (461 loc) · 14.5 KB

File metadata and controls

596 lines (461 loc) · 14.5 KB

HTTP 协议与实现详解

目录


HTTP 协议基础

什么是 HTTP?

HTTP (HyperText Transfer Protocol) 是应用层协议,基于 TCP/IP 协议栈。它定义了客户端和服务器之间的通信格式。

协议栈层次:

┌─────────────────────┐
│   应用层 (HTTP)      │  ← 我们实现的层
├─────────────────────┤
│   传输层 (TCP)       │  ← socket API 封装的层
├─────────────────────┤
│   网络层 (IP)        │
├─────────────────────┤
│   链路层             │
└─────────────────────┘

HTTP 的特点

  1. 无状态协议:每个请求都是独立的,服务器不保存之前的请求信息
  2. 基于请求-响应模型:客户端发起请求,服务器返回响应
  3. 文本协议:HTTP/1.1 使用纯文本格式(易于调试)
  4. 默认端口:HTTP 使用 80 端口,HTTPS 使用 443 端口

TCP 连接机制

三次握手 (Three-way Handshake)

在 HTTP 通信开始前,必须先建立 TCP 连接:

客户端                          服务器
  │                               │
  │─────── SYN ──────────────────>│  1. 客户端发起连接
  │                               │
  │<────── SYN-ACK ──────────────│  2. 服务器确认
  │                               │
  │─────── ACK ──────────────────>│  3. 客户端确认
  │                               │
  │      连接建立,可以发送数据     │

对应代码:

  • 服务器端bind()listen()accept()
  • 客户端connect()

四次挥手 (Four-way Handshake)

连接关闭过程:

客户端                          服务器
  │                               │
  │─────── FIN ──────────────────>│  1. 客户端请求关闭
  │                               │
  │<────── ACK ──────────────────│  2. 服务器确认
  │                               │
  │<────── FIN ──────────────────│  3. 服务器请求关闭
  │                               │
  │─────── ACK ──────────────────>│  4. 客户端确认
  │                               │

对应代码: close()shutdown()


HTTP 请求与响应格式

HTTP 请求格式

GET / HTTP/1.1\r\n              ← 请求行 (方法 路径 协议版本)
Host: localhost\r\n              ← 请求头
Connection: close\r\n            ← 请求头
\r\n                             ← 空行 (标记 headers 结束)
[请求体]                          ← 可选的请求体 (POST/PUT 等)

重要组成部分:

  1. 请求行

    • 方法:GET, POST, PUT, DELETE 等
    • 路径:/, /api/users, /index.html
    • 版本:HTTP/1.1HTTP/1.0
  2. 请求头 (Headers)

    • Host: 必须字段(HTTP/1.1)
    • Connection: keep-aliveclose
    • Content-Length: 请求体长度
    • User-Agent: 客户端标识
  3. 空行\r\n\r\n 标记 headers 结束

  4. 请求体 (Body):可选,POST/PUT 等携带数据

HTTP 响应格式

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)

代码实现讲解

服务器实现要点

1. Socket 创建与绑定

// 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:等待队列的最大长度

2. 循环接收数据(核心改进)

问题:为什么需要循环接收?

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 结束
        break

3. 不同语言的实现对比

C 语言:

// 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
  • 优点:简洁易读
  • 缺点:性能相对较低

客户端实现要点

1. 发送完整请求

# 必须包含所有必要的 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)  # 确保发送完整

2. 接收完整响应

方法一:循环直到连接关闭

# 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();  // 内部实现了循环

常见问题与注意事项

1. 地址已被占用 (Address already in use)

问题原因: 服务器关闭后,端口处于 TIME_WAIT 状态(默认 2 分钟)

解决方法:

int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

2. 部分数据丢失

问题原因: 只调用一次 recv()/read(),数据可能分片到达

解决方法: 使用循环接收,检查结束标记 \r\n\r\n

3. 客户端接收不完整

问题原因: 服务器还在发送,客户端就停止接收

解决方法:

# ✅ 循环接收直到连接关闭
while True:
    chunk = sock.recv(1024)
    if not chunk:
        break
    response += chunk

4. 缓冲区溢出

问题原因: 固定大小的 buffer 无法容纳大请求

解决方法:

// 检查 buffer 是否满
if (total_bytes >= BUFFER_SIZE - 1) {
    // 返回 413 Payload Too Large
    break;
}

5. 字符串处理错误

常见错误:

// ❌ 忘记 null 终止符
char buffer[1024];
read(fd, buffer, 1024);
printf("%s", buffer);  // 可能读取越界

// ✅ 正确做法
buffer[bytes_read] = '\0';
printf("%s", buffer);

6. 连接未正确关闭

问题原因: 客户端不知道数据何时结束

解决方法: 使用以下之一:

  • Connection: close header(简单)
  • Content-Length header(精确)
  • Chunked Transfer Encoding(流式)

7. 大小端问题

注意事项:

// 网络字节序是大端,需要转换
server_addr.sin_port = htons(8080);  // host to network short

8. 非阻塞 I/O 的考虑

当前实现都是阻塞 I/O:

  • 优点:简单直观
  • 缺点:一个慢客户端会阻塞整个服务器

改进方向:

  • 多线程(Go 和 Rust 示例已实现)
  • 多进程
  • 非阻塞 I/O + select/poll/epoll
  • 异步 I/O (async/await)

9. 错误处理

生产代码必须检查所有返回值:

// ❌ 危险
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;
}

10. 安全注意事项

缓冲区安全:

// ❌ 不检查边界
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)

调试技巧

1. 使用 netcat 测试

# 手动发送 HTTP 请求
nc localhost 8080
GET / HTTP/1.1
Host: localhost

# (按两次回车)

2. 使用 curl 调试

# 显示详细信息
curl -v http://localhost:8080/

# 显示原始 HTTP 内容
curl --trace-ascii - http://localhost:8080/

3. 使用 tcpdump 抓包

# 抓取 8080 端口的数据包
sudo tcpdump -i lo -A port 8080

4. 使用 strace 追踪系统调用

# 追踪服务器的系统调用
strace -e trace=network,read,write ./server

HTTP/1.1 vs HTTP/1.0

主要区别:

特性 HTTP/1.0 HTTP/1.1
持久连接 默认关闭 默认开启 (keep-alive)
Host header 可选 必须
管道化 不支持 支持
缓存控制 基础 增强

我们的实现:

  • 使用 HTTP/1.1 协议版本
  • 通过 Connection: close 禁用持久连接(简化实现)

进阶主题

1. 持久连接 (Keep-Alive)

复用 TCP 连接发送多个请求:

客户端                服务器
  │── Request 1 ────>│
  │<─── Response 1 ──│
  │── Request 2 ────>│   (同一个 TCP 连接)
  │<─── Response 2 ──│

2. HTTP/2

  • 二进制协议(非文本)
  • 多路复用(一个连接多个请求)
  • 服务器推送
  • Header 压缩 (HPACK)

3. HTTPS

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、TCP 和 UDP 的关系

1. HTTP 是建立在 TCP 之上的吗?

是的,传统的 HTTP/1.0、HTTP/1.1 和 HTTP/2 都是建立在 TCP 之上的。

2. 可以用 UDP 发送 HTTP 请求吗?

传统上不行,但现在可以

  • HTTP/1.x 和 HTTP/2:必须使用 TCP
  • HTTP/3:使用 UDP!基于 QUIC 协议(QUIC 是在 UDP 之上构建的)

3. 三者之间的关系(层次结构):

应用层:    HTTP (网页、API 等)
           ↓
传输层:    TCP (可靠传输)  或  UDP (快速传输)
           ↓
网络层:    IP (寻址和路由)
           ↓
链路层:    以太网、WiFi 等

TCP vs UDP 的核心区别:

特性 TCP UDP
连接 面向连接(三次握手) 无连接
可靠性 可靠(保证数据到达、顺序正确) 不可靠(可能丢包、乱序)
速度 较慢(有握手、确认等开销) 较快(无额外开销)
用途 HTTP/1.x、HTTP/2、文件传输、邮件 HTTP/3、视频直播、游戏、DNS

为什么 HTTP 传统上使用 TCP?

  1. 可靠性:网页内容必须完整准确地传输
  2. 顺序性:HTML、CSS、JS 文件需要按顺序接收
  3. 错误恢复:TCP 会自动重传丢失的数据包

为什么 HTTP/3 改用 UDP(QUIC)?

虽然 UDP 不可靠,但 QUIC 在 UDP 之上实现了:

  • 自己的可靠性机制(比 TCP 更高效)
  • 更快的连接建立(0-RTT)
  • 更好的多路复用(避免队头阻塞)

总结:

  • HTTP 是应用层协议,定义了客户端和服务器如何交换数据
  • TCP/UDP 是传输层协议,负责在网络中传输数据
  • 传统 HTTP 使用 TCP 保证可靠性
  • 现代 HTTP/3 使用基于 UDP 的 QUIC,在保持可靠性的同时提高性能