TCP / HTTP 是怎么「手撸」出来的?

从操作系统内核到各语言的 Socket API,用伪代码逐层拆解网络通信的底层实现

1 核心问题:语言层面的网络库到底在做什么?

这些库不是凭空实现的——它们都在调用同一个东西:操作系统提供的 Socket API

简单回答

都是调用操作系统能力。 无论你用 Python 的 socket 模块、Java 的 java.net.Socket、Go 的 net.Dial、还是 Node.js 的 net.createConnection—— 它们底层最终都调用了 C 语言的 socket() / bind() / listen() / accept() / connect() / send() / recv() 系统调用。

这些系统调用直接跟操作系统内核中的 TCP/IP 协议栈交互,而内核又通过网卡驱动程序操作硬件发送和接收数据包。

应用层 你的代码:HTTP Server / Browser / curl … 发送 "GET /index.html HTTP/1.1"
语言层 语言标准库:Python socket / Java net / Go net / Node net … 封装系统调用
系统调用 C 函数:socket() / bind() / connect() / send() / recv() … 陷入内核态
内核层 TCP/IP 协议栈:三次握手 / 流量控制 / 拥塞控制 / 分包重组 … 管理所有连接
硬件层 网卡驱动 → 网卡 → 网线/无线 → 路由器 → 互联网 → 对端
关键认知:你写的代码只负责「说什么」(应用层数据),至于「怎么把数据可靠地送过去」(分包、重传、排序、流量控制),全部由操作系统的 TCP 协议栈自动完成。你只需要调用 send(),内核就帮你搞定一切。
2 什么是 Socket?操作系统给用户程序开的「窗口」

Socket 不是网络协议,它是操作系统提供的一个抽象接口,让你像操作文件一样操作网络连接

Socket 的本质

在 Linux/Unix 中,Socket 被实现为一个文件描述符(file descriptor)。这意味着你可以用 read()write() 操作它,就像操作普通文件一样。

当你调用 socket() 时,内核在内存中创建一个数据结构,用来维护这个连接的所有状态(本地端口、远端地址、发送/接收缓冲区、TCP 状态机等),然后返回一个整数(文件描述符)给你。

C — 创建一个 TCP Socket
// socket(协议族, 类型, 协议)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//   AF_INET     → 使用 IPv4
//   SOCK_STREAM → 面向流的、可靠的连接 = TCP
//   0           → 自动选择协议 (TCP)

if (sockfd < 0) {
    perror("socket creation failed");
    return -1;
}
// sockfd 就是一个普通的整数,但它代表内核中的一个 TCP 连接端点
3 手写一个 TCP 服务器(伪代码)

一个最简 TCP Echo 服务器的完整流程,每一步都是系统调用

TCP Server 完整流程

伪代码 — TCP Echo 服务器
// ═════════════ 第 1 步:创建 Socket ═════════════
let server_fd = socket(AF_INET, SOCK_STREAM, 0)
// 向内核申请一个 TCP socket 端点
// 内核分配内存,初始化 TCP 状态机为 CLOSED

// ═════════════ 第 2 步:绑定地址 ═════════════
let addr = sockaddr_in(
    ip   = "0.0.0.0",    // 监听所有网卡
    port = 8080           // 监听端口
)
bind(server_fd, addr)
// 把这端口的「所有权」注册到 server_fd 上

// ═════════════ 第 3 步:开始监听 ═════════════
listen(server_fd, backlog = 128)
// 把 TCP 状态机从 CLOSED → LISTEN
// backlog = 内核为这个 socket 维护的「等待 accept」队列长度
// 至此,内核已经可以接收 SYN 包了!

// ═════════════ 第 4 步:接受连接 ═════════════
loop:
    client_fd, client_addr = accept(server_fd)
    // accept 是阻塞的——内核完成三次握手后才返回
    // 返回一个新的 socket fd,专门用于和这个客户端通信
    // 原 server_fd 继续监听新连接

    // ═══════ 第 5 步:收发数据 ═══════
    while true:
        data = recv(client_fd, buffer_size = 4096)
        // 从内核的接收缓冲区读取数据
        // 内核已经帮你做了:拆包、去重、排序、ACK 确认

        if data is empty:
            break  // 对方关闭了连接

        send(client_fd, data)
        // 写入内核的发送缓冲区
        // 内核自动分包、加上 TCP 头、通过网卡发出

    // ═══════ 第 6 步:关闭连接 ═══════
    close(client_fd)
    // 内核发送 FIN 包,执行四次挥手

时间线:Server 和 Client 的交互过程

Server socket() → bind() → listen() → 进入 LISTEN 状态,等待连接
Client socket() → connect() → 发起三次握手 → 三次握手 → 三次握手
内核 SYN → SYN-ACK → ACK(三次握手完成,连接建立)
Server accept() 返回 → 拿到新 fd,可以开始通信
双方 send() / recv() 反复调用 → 数据双向流动
任一方 close() → FIN → ACK → FIN → ACK(四次挥手)
关键点:三次握手不是你代码做的!你调用 connect() 之后,内核自动帮你完成三次握手,握手成功后 connect() 才返回。同理,accept() 也是在握手完成后才返回。
4 手写一个 TCP 客户端(伪代码)

客户端比服务端更简单,只需要 socket + connect + send/recv

伪代码 — TCP 客户端
// ═════════════ 第 1 步:创建 Socket ═════════════
let sockfd = socket(AF_INET, SOCK_STREAM, 0)

// ═════════════ 第 2 步:连接到服务器 ═════════════
let server_addr = sockaddr_in(
    ip   = "127.0.0.1",  // 目标 IP
    port = 8080           // 目标端口
)
connect(sockfd, server_addr)
// 内核自动完成:
//   1. 操作系统自动分配一个本地临时端口(ephemeral port)
//   2. 发送 SYN 包到 127.0.0.1:8080
//   3. 等待 SYN-ACK
//   4. 回复 ACK,三次握手完成
//   5. connect() 返回

// ═════════════ 第 3 步:发送数据 ═════════════
let message = "Hello, Server!"
send(sockfd, message)
// 数据进入内核发送缓冲区 → 内核分包 → TCP 头 → IP 头 → 以太网帧 → 网卡发出

// ═════════════ 第 4 步:接收响应 ═════════════
let response = recv(sockfd, buffer_size = 4096)
print(response)

// ═════════════ 第 5 步:关闭 ═════════════
close(sockfd)
注意:客户端不需要 bind()listen()。当你调用 connect() 时,操作系统自动帮你分配一个本地临时端口(比如 52341),你不需要关心。
5 你只调了 send(),内核做了多少事?

一次 send() 调用在内核中的完整路径

用户态 send(fd, "Hello", 5) → 陷入内核(syscall)
内核① VFS 层:根据 fd 找到 socket 数据结构
内核② TCP 层:数据拷贝到发送缓冲区,检查发送窗口、拥塞窗口
内核③ TCP 层:如果可发送,封装 TCP 头(源端口、目的端口、序列号、ACK、窗口大小、校验和…)
内核④ IP 层:封装 IP 头(源 IP、目的 IP、TTL、协议类型…),路由查找下一跳
内核⑤ 数据链路层:封装 MAC 头,ARP 查找下一跳 MAC 地址
硬件 网卡驱动 → DMA 传输 → 网卡 → 网线发出电信号/光信号
给你一个直观感受:
你调用 send(fd, "Hello", 5) 这一行代码——
内核至少执行了:缓冲区检查 → 分段 → TCP头封装 → 拥塞控制判断 → IP头封装 → 路由查找 → ARP查询 → MAC头封装 → 网卡队列 → DMA传输 → 中断处理 → 等待ACK → 可能重传
而你只需要等这一行代码返回(默认是阻塞模式)。
6 HTTP 不过是「在 TCP 连接上按约定格式收发文本」

HTTP 是应用层协议,它完全不关心底层是 TCP 还是别的什么——只要能可靠传输字节流就行

用伪代码实现一个最简 HTTP 服务器

伪代码 — 最简 HTTP/1.1 服务器(基于 TCP)
// 前面:socket() → bind() → listen() → accept()  ← 和 TCP 服务器完全一样

loop:
    client_fd = accept(server_fd)

    // ═══ 从 TCP 连接中读取 HTTP 请求 ═══
    raw_request = ""
    while true:
        chunk = recv(client_fd, 4096)
        raw_request += chunk
        if "\r\n\r\n" in raw_request:    // HTTP 请求头结束标志
            break

    // ═══ 解析 HTTP 请求 ═══
    // raw_request 大概长这样:
    //   GET /index.html HTTP/1.1\r\n
    //   Host: localhost:8080\r\n
    //   Connection: keep-alive\r\n
    //   \r\n

    lines = raw_request.split("\r\n")
    first_line = lines[0].split(" ")
    method  = first_line[0]   // "GET"
    path    = first_line[1]   // "/index.html"
    version = first_line[2]   // "HTTP/1.1"

    // ═══ 解析请求头 ═══
    headers = {}
    for line in lines[1:]:
        if line == "": break
        key, value = line.split(": ", 1)
        headers[key] = value

    // ═══ 生成 HTTP 响应 ═══
    let body = "<h1>Hello, World!</h1>"

    let response = ""
    response += "HTTP/1.1 200 OK\r\n"                    // 状态行
    response += "Content-Type: text/html\r\n"              // 响应头
    response += "Content-Length: " + len(body) + "\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"                                     // 空行分隔
    response += body                                            // 响应体

    // ═══ 通过 TCP 发送响应 ═══
    send(client_fd, response)
    close(client_fd)
看到了吗?HTTP 服务器和 TCP 服务器的唯一区别就是:
① 多了一个「解析 HTTP 请求格式」的步骤
② 多了一个「按 HTTP 格式拼响应字符串」的步骤
③ 底层还是 recv() 收数据、send() 发数据

HTTP 是应用层协议,它定义的是「数据的格式和语义」;
TCP 是传输层协议,它负责「如何可靠地把这些字节送过去」。
7 各语言是如何封装 Socket 的?

换汤不换药——底层都是 POSIX Socket API,只是语法糖不同

C(最底层)
Python
Java
Go
Rust
Node.js
C — 直接调用 POSIX 系统调用
/* C 的 socket 编程就是直接写系统调用 */
/* <sys/socket.h> 提供的函数直接对应内核系统调用 */

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(8080);           // 端口号要转网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;

    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(sockfd, 128);

    while (1) {
        struct sockaddr_in client;
        socklen_t client_len = sizeof(client);
        int client_fd = accept(sockfd, (struct sockaddr*)&client, &client_len);

        char buf[4096];
        ssize_t n = recv(client_fd, buf, sizeof(buf), 0);
        send(client_fd, buf, n, 0);

        close(client_fd);
    }
    return 0;
}
Python — socket 模块是对 C 系统调用的薄封装
import socket

# Python 的 socket 模块底层调用的是 C 的 socket()/bind()/listen()...
# CPython 源码中直接 #include <sys/socket.h> 然后做了一层 Python 对象包装

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(128)

while True:
    client, addr = server.accept()
    data = client.recv(4096)
    client.sendall(data)
    client.close()
Java — java.net.Socket 封装了 Socket 系统调用,通过 JNI 调用 C
import java.net.*;
import java.io.*;

// Java 的 java.net.Socket 最终通过 JNI (Java Native Interface)
// 调用本地 C 库的 socket()/bind()/listen()/accept() 等系统调用
// 路径:Java → JNI → libc → syscall → 内核

ServerSocket server = new ServerSocket(8080);

while (true) {
    Socket client = server.accept();

    InputStream  in  = client.getInputStream();
    OutputStream out = client.getOutputStream();

    byte[] buf = new byte[4096];
    int n = in.read(buf);
    out.write(buf, 0, n);

    client.close();
}
Go — net 包通过 syscall 包直接发起系统调用,不经过 libc
package main

import "net"

// Go 的 net 包比较特殊:
// Go 有自己的运行时,不依赖 libc
// 它通过 //go:linkname 或汇编直接发起 syscall 指令
// Linux 上直接调用 sys_socket / sys_bind / sys_listen ...

func main() {
    // 一行代码就包含:socket() + bind() + listen()
    listener, _ := net.Listen("tcp", ":8080")

    for {
        conn, _ := listener.Accept()   // accept() 系统调用

        go func(c net.Conn) {            // goroutine 并发处理
            buf := make([]byte, 4096)
            n, _ := c.Read(buf)          // recv() 系统调用
            c.Write(buf[:n])              // send() 系统调用
            c.Close()                     // close() 系统调用
        }(conn)
    }
}
Rust — std::net 封装了 libc 的系统调用
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};

// Rust 的 std::net 底层通过 libc crate 调用 C 的系统调用
// 路径:Rust → libc crate → C ABI → syscall → 内核

fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080")?;

    for stream in listener.incoming() {
        let mut stream: TcpStream = stream?;

        let mut buf = [0u8; 4096];
        let n = stream.read(&mut buf)?;
        stream.write_all(&buf[..n])?;
        // drop(stream) 自动调用 close()
    }
    Ok(())
}
Node.js — net 模块底层通过 libuv 异步调用系统调用
const net = require('net');

// Node.js 的网络层路径:
// JavaScript → libuv (C) → 系统调用 → 内核
// libuv 在事件循环中用 epoll/kqueue 实现非阻塞 IO

const server = net.createServer((socket) => {
    // 'socket' 是一个 Duplex stream,封装了 TCP 连接
    socket.on('data', (data) => {
        socket.write(data);  // echo back
    });
    socket.on('end', () => {
        // 连接关闭
    });
});

server.listen(8080, () => {
    console.log('Server listening on port 8080');
});

各语言 Socket 实现路径对比

语言 调用路径 是否经过 libc 特点
C 用户代码 → libc → syscall → 内核 ✅ 是 最直接,1:1 映射系统调用
Python (CPython) Python socket 模块 → C 扩展 → libc → syscall → 内核 ✅ 是 每个 socket 操作有 Python 对象包装开销
Java Java net → JNI → libc → syscall → 内核 ✅ 是 JNI 调用有跨语言开销;NIO 用 epoll 实现非阻塞
Go Go net → 直接 syscall 指令 → 内核 ❌ 否(通常) 绕过 libc,由 Go 运行时直接发起系统调用;netpoller 用 epoll
Rust Rust std::net → libc crate → libc → syscall → 内核 ✅ 是(std) 零成本抽象;也有 tokio 异步运行时用 epoll
Node.js JS net → libuv (C) → syscall → 内核 ✅ 是 libuv 用 epoll 实现事件驱动异步 IO
Go 比较特别:Go 有自己的运行时调度器,不需要 libc。在 Linux 上,Go 通过汇编直接执行 syscall 指令(如 sys_socketsys_connect),完全绕过 libc。这样做的好处是避免了 C 调用栈切换开销,配合 goroutine 可以轻松支撑百万级并发连接。
8 进阶:非阻塞 IO 和事件循环

上面的例子都是阻塞模式——那 Node.js 和 Nginx 是怎么处理成千上万并发连接的?

阻塞 vs 非阻塞

默认情况下,accept()recv() 都是阻塞的——没有连接或数据时,调用线程会挂起。这意味着一个线程只能处理一个连接。

高并发服务器(Nginx、Node.js、Go、Redis)使用 非阻塞 IO + IO 多路复用

伪代码 — epoll 事件循环(Linux 高性能服务器的核心)
// 这是 Nginx / Node.js / Redis / Go netpoller 的底层原理

let epoll_fd = epoll_create1(0)     // 创建一个 epoll 实例

let server_fd = socket(AF_INET, SOCK_STREAM, 0)
set_nonblocking(server_fd)            // 设为非阻塞模式
bind(server_fd, addr)
listen(server_fd, 128)

epoll_ctl(epoll_fd, ADD, server_fd, EPOLLIN)
// 告诉 epoll:「帮我盯着 server_fd,有可读事件时通知我」

let events = []   // 事件数组

while true:
    // ═══ 核心:一个线程等待多个 fd 的事件 ═══
    ready_count = epoll_wait(epoll_fd, events, max_events = 1024, timeout)

    for i in 0..ready_count:
        fd = events[i].fd

        if fd == server_fd:
            // 有新连接到来
            client_fd = accept(server_fd)
            set_nonblocking(client_fd)
            epoll_ctl(epoll_fd, ADD, client_fd, EPOLLIN)
            // 把新连接的 fd 也加入监控

        else:
            // 客户端发来了数据
            data = recv(fd, 4096)
            if data is empty:
                close(fd)
                epoll_ctl(epoll_fd, DEL, fd)
            else:
                send(fd, data)
一句话总结:阻塞 IO 是「一个一个问」,非阻塞 IO + epoll 是「同时盯着几千个 fd,谁有动静就处理谁」——这就是 Nginx 能支撑数万并发连接的秘密。
9 总结:一图胜千言
从你的代码到网络数据包——完整调用链 你的代码(应用层) send(fd, "GET / HTTP/1.1\r\n") 语言标准库(封装层) Python socket / Go net / Java Socket / ... 系统调用(陷入内核) sys_sendto(fd, buf, len, flags, addr) 内核 TCP/IP 协议栈 分段 → 重传 → 流量控制 → 拥塞控制 TCP头 → IP头 → 路由 → ARP → MAC头 网卡驱动 → 网卡 → 网络 DMA → 电信号/光信号 → 互联网 HTTP 协议 语法糖 用户态/内核态 边界 TCP 可靠性 物理传输 核 心 结 论 你写的每一行网络代码最终都变成了 socket() → bind() → listen() → accept() → send()/recv() → close() 这 7 个系统调用就是所有网络编程的共同根基,无论你用什么语言