24、状态机在协议栈中的应用:TCP状态机、HTTP状态机

协议栈,说白了就是网络通信的「交通规则」。你想想看,数据从一个设备传到另一个设备,中间要经过多少环节?谁先发、谁后发、发错了怎么办、丢了怎么补——这些全靠状态机来管。

我个人习惯把协议栈里的状态机分成两类:连接管理型请求响应型。TCP 状态机是前者的典型代表,HTTP 状态机则是后者的标杆。今天我们就拿这两个协议开刀,看看状态机是怎么在真实网络里「干活」的。

TCP 状态机:连接的一生

TCP 状态机,我做了这么多年嵌入式,觉得它是最优雅的状态机之一。它完整描述了一个 TCP 连接从出生到死亡的全过程。

先看核心状态。TCP 规范里定义了 11 个状态,但实际开发中我们最常打交道的就这几个:

状态 含义 触发事件
CLOSED 初始状态,啥也没有
SYN_SENT 主动发起连接 应用层调用 connect()
ESTABLISHED 连接建立成功,可以传数据了 收到 SYN+ACK
FIN_WAIT_1 主动关闭,发了 FIN 应用层调用 close()
CLOSE_WAIT 被动关闭,收到 FIN 收到 FIN 包
TIME_WAIT 等待 2MSL,确保对方收到 ACK 收到 FIN+ACK

我曾经在一个物联网网关项目里,被 TIME_WAIT 状态坑惨了。设备频繁断连重连,端口被占满,新连接死活建不起来。后来一查,就是 TIME_WAIT 积累太多,没及时释放。嗯,这里要注意:TIME_WAIT 不是 bug,是特性,但处理不好就是灾难。

TCP 状态机核心流程

下面这张图,是我自己画的一个简化版 TCP 状态机。它去掉了不常用的状态,保留了三次握手和四次挥手的主干。

CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 CLOSE_WAIT TIME_WAIT connect() 发 SYN 收到 SYN+ACK 主动 close() 发 FIN 收到 FIN 收到 FIN+ACK 2MSL 超时 主动方 被动方

这张图里,我特意把主动关闭和被动关闭的路径用不同颜色标出来了。你仔细看,主动关闭方要走 FIN_WAIT_1 → TIME_WAIT → CLOSED,而被动关闭方是 CLOSE_WAIT 等应用层处理完再发 FIN。这个不对称性,是 TCP 设计里最精妙也最容易出错的地方。

HTTP 状态机:请求响应的节奏

HTTP 状态机比 TCP 简单得多。它本质上就是一个「请求-响应」的循环。但别小看它,我在嵌入式 Web 服务器项目里,见过太多人把 HTTP 状态机写成一团乱麻。

一个典型的 HTTP 状态机长这样:

状态 说明 典型动作
IDLE 等待请求 监听 socket,等待数据
REQ_LINE 解析请求行 读 "GET /index.html HTTP/1.1"
REQ_HEADER 解析请求头 读 "Host: ..." 直到空行
REQ_BODY 读取请求体 根据 Content-Length 读数据
PROCESS 处理请求 路由、业务逻辑
RESP_HEADER 发送响应头 写 "HTTP/1.1 200 OK"
RESP_BODY 发送响应体 写 HTML 或 JSON 数据
KEEP_ALIVE 保持连接,等待下一个请求 回到 IDLE 或关闭

你可能会问:为什么要把解析拆成这么多步?

原因很简单:网络数据是流式的。你一次 read() 可能只读到半行请求头,下一轮 read() 才读到完整的。如果不用状态机分段处理,你就得在回调函数里拼字符串、记偏移量——那代码写出来,自己都不想看。

核心原则:HTTP 状态机的每个状态只做一件事。解析请求行时,绝不碰请求头;发送响应体时,绝不处理新请求。这样每个状态的代码都短小精悍,出 bug 的概率直线下降。

实战:一个极简 HTTP 状态机实现

下面这段代码,是我从一个轻量级嵌入式 Web 服务器里摘出来的。它只保留了核心骨架,但足够说明问题。

// HTTP 状态枚举
typedef enum {
    HTTP_IDLE,
    HTTP_REQ_LINE,
    HTTP_REQ_HEADER,
    HTTP_REQ_BODY,
    HTTP_PROCESS,
    HTTP_RESP_HEADER,
    HTTP_RESP_BODY,
    HTTP_DONE
} http_state_t;

// 状态机上下文
typedef struct {
    http_state_t state;
    char req_line[256];
    char header_buf[1024];
    int content_length;
    int bytes_read;
    // ... 其他字段
} http_conn_t;

// 状态机主循环
void http_state_machine(http_conn_t *conn, int fd) {
    switch (conn->state) {
        case HTTP_IDLE:
            // 等待新请求到来
            if (data_available(fd)) {
                conn->state = HTTP_REQ_LINE;
            }
            break;

        case HTTP_REQ_LINE:
            if (read_line(fd, conn->req_line, sizeof(conn->req_line)) > 0) {
                // 解析请求行:方法、URI、版本
                parse_request_line(conn->req_line, &conn->method, 
                                   conn->uri, &conn->version);
                conn->state = HTTP_REQ_HEADER;
            }
            break;

        case HTTP_REQ_HEADER:
            if (read_headers(fd, conn->header_buf, sizeof(conn->header_buf))) {
                // 解析 Content-Length
                conn->content_length = get_header_value(
                    conn->header_buf, "Content-Length", 0);
                if (conn->content_length > 0) {
                    conn->state = HTTP_REQ_BODY;
                } else {
                    conn->state = HTTP_PROCESS;
                }
            }
            break;

        case HTTP_REQ_BODY:
            conn->bytes_read += read_body(fd, conn->body_buf + conn->bytes_read,
                                           conn->content_length - conn->bytes_read);
            if (conn->bytes_read >= conn->content_length) {
                conn->state = HTTP_PROCESS;
            }
            break;

        case HTTP_PROCESS:
            // 路由到具体处理函数
            handle_request(conn);
            conn->state = HTTP_RESP_HEADER;
            break;

        case HTTP_RESP_HEADER:
            write_response_headers(fd, conn->status_code, conn->content_type,
                                   conn->body_len);
            conn->state = HTTP_RESP_BODY;
            break;

        case HTTP_RESP_BODY:
            if (write_body(fd, conn->body, conn->body_len) >= conn->body_len) {
                conn->state = HTTP_DONE;
            }
            break;

        case HTTP_DONE:
            // 判断是否 keep-alive
            if (conn->keep_alive) {
                conn->state = HTTP_IDLE;
                reset_connection(conn);
            } else {
                close(fd);
            }
            break;
    }
}

避坑指南:我曾经在 HTTP 状态机里犯过一个低级错误——在 REQ_HEADER 状态里直接处理请求体。结果遇到 chunked 编码时,数据解析全乱套了。后来我强制自己遵守一条铁律:一个状态只做一件事,绝不越界。从那以后,HTTP 解析再没出过问题。

TCP 状态机 vs HTTP 状态机:设计哲学的不同

这两种状态机虽然都跑在协议栈里,但设计思路完全不同。我总结了一下:

  • TCP 状态机是「被动响应型」:它由网络事件驱动。收到 SYN 包就进入 SYN_RCVD,收到 FIN 包就进入 CLOSE_WAIT。状态迁移的触发源在外部网络。
  • HTTP 状态机是「主动推进型」:它由应用逻辑驱动。解析完请求行,主动进入请求头解析;处理完业务,主动进入响应发送。状态迁移的节奏掌握在自己手里。

这个区别很重要。你在设计自己的协议状态机时,先想清楚:你的状态机是被别人推着走,还是自己推着自己走?想明白了,代码结构自然就清晰了。

写在最后

TCP 和 HTTP 的状态机,是网络编程里绕不开的两座山。TCP 状态机教会我们「连接是有生命的」,HTTP 状态机教会我们「请求是有节奏的」。把这两个吃透了,你再去看 MQTT、WebSocket、CoAP 这些协议的状态机,会发现——万变不离其宗。

嗯,今天就聊到这儿。代码里的状态机,说到底就是「在正确的时间做正确的事」。你写多了,自然就找到感觉了。


公众号:蓝海资料掘金营,微信deep3321