前面几章都是做 TCP 客户端——发 SYN、连接、发请求、收响应。这章反过来,让内核能作为 TCP 服务端:监听端口,接受连接,响应请求。

客户端 vs 服务端的区别

客户端:主动发 SYN,等待 SYN-ACK。

服务端:被动等待 SYN,收到后发 SYN-ACK,等 ACK 完成三次握手,然后通过 accept() 把新连接交给应用层。

关键区别在于:服务端有两种 socket 角色——

角色 职责
监听 socket(listen fd) 绑定端口,等待连接请求
连接 socket(accept fd) 每个客户端连接对应一个,负责实际数据收发

三步接口

bind(fd, &sa, len)    // 绑定本地端口
listen(fd, backlog)   // 进入监听状态
accept(fd, &sa, &len) // 阻塞,等到有连接,返回新 fd

数据结构

tcp_sock_t 加了几个字段:

int  is_listener;
int  accept_queue[8];   // 存已完成三次握手的连接 index
uint32_t accept_head, accept_tail;

还新增了两个状态:

  • TCP_LISTEN:监听中,等待 SYN
  • TCP_SYN_RECV:收到 SYN 已回 SYN-ACK,等最终 ACK

核心流程

1. 收到 SYN

tcp_handle() 里单独处理 SYN:

if ((tcp->flags & TCP_SYN) && !(tcp->flags & TCP_ACK)) {
    // 找到监听该端口的 socket
    // 分配新连接 slot
    ns->state       = TCP_SYN_RECV;
    ns->local_port  = dport;
    ns->remote_port = sport;
    ns->remote_ip   = src_ip;
    ns->ack         = ntohl(tcp->seq) + 1;  // 下次要 ACK 的序号
    ns->syn_seq     = 0xABCD1234;
    ns->snd_una = ns->snd_nxt = ns->syn_seq;
    tcp_send_seg(ns, ns->syn_seq, TCP_SYN | TCP_ACK, 0, 0);
}

2. 收到最终 ACK(完成三次握手)

if (s->state == TCP_SYN_RECV && (tcp->flags & TCP_ACK)) {
    if (ack_val == s->syn_seq + 1) {
        s->snd_una = s->snd_nxt = ack_val;
        s->state = TCP_ESTABLISHED;
        // 放入监听 socket 的 accept queue
        ls->accept_queue[ls->accept_tail % 8] = i;
        ls->accept_tail++;
    }
}

3. accept() 取连接

int tcp_accept(int s, ...) {
    tcp_sock_t *ls = &tcp_socks[s];
    while (ls->accept_head == ls->accept_tail) {
        sti; net_poll(); cli;   // 阻塞等待
    }
    int ni = ls->accept_queue[ls->accept_head++ % 8];
    return ni;
}

测试

写了一个简单的 HTTP 服务端程序:

int srv = socket(AF_INET, SOCK_STREAM, 0);
bind(srv, &sa, sizeof(sa));   // 端口 8080
listen(srv, 4);

for (int i = 0; i < 3; i++) {
    int cli = accept(srv, 0, 0);
    char buf[2048];
    int n = read(cli, buf, sizeof(buf)-1);
    buf[n] = 0;
    // 打印收到的 HTTP 请求...

    write(cli, "HTTP/1.0 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!", 52);
    close(cli);
}

QEMU 加上端口转发启动(hostfwd=tcp::28080-:8080),然后:

$ curl http://localhost:28080/
Hello, World!

串口日志:

[tcp] listening on port 0x1f90
[tcp_in] flags=0x02           ← 收到 SYN
[tcp] SYN rcvd, sent SYN-ACK
[tcp_in] flags=0x10           ← 收到 ACK,三次握手完成
[tcp] accepted connection
[tcp_in] flags=0x18           ← 收到 HTTP 请求(PSH+ACK)
recv: GET / HTTP/1.1...
response sent

几个设计细节

为什么 SYN 处理要单独提前?

tcp_handle 遍历 socket 时,监听 socket 的匹配条件是 local_port == dport,连接 socket 要求 local_port + remote_port + remote_ip 三个都匹配。如果不提前处理 SYN,SYN 包可能被错误地分发给已有连接(如果有 remote_port 恰好相同的话)。单独处理 SYN(且 flags 中没有 ACK 位)是最干净的方案。

syn_seq + 1 的含义

发 SYN-ACK 时,syn_seq 是我方的初始序号。SYN 本身占一个序号,所以完成握手后双方的序号起点是 syn_seq + 1。客户端的 ACK 里的 ack_seq 字段应该是 syn_seq + 1,用这个值来确认三次握手是否真正完成。

accept queue 为什么用 index 而不是指针?

tcp_socks 数组固定大小,index 更稳定——指针在数组 realloc 后会失效,但我们没有动态分配,用 index 更简单。

小结

服务端的核心是状态机的扩展:

CLOSED → (bind+listen) → LISTEN
LISTEN → (收到 SYN) → SYN_RECV(新连接)
SYN_RECV → (收到 ACK) → ESTABLISHED → accept queue

accept() 从 queue 里取已建立的连接,返回新 fd。之后的 read/write/close 和客户端完全相同。

下一章做 mmap 文件映射。