HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 代理技术全栈手册

    • 代理技术全栈手册 - HiHuo
    • 原理篇

      • 第01章 代理是什么:正向 / 反向 / 透明 / 隧道的统一模型
      • 第02章 代理与网络层级:L3 / L4 / L5 / L7 在哪里截断流量
      • 第03章 一个请求穿过代理的一生:连接生命周期全景
    • 协议篇

      • 第04章 HTTP 代理协议:绝对 URI、CONNECT 隧道、转发头与连接池
      • 第05章 HTTPS 与 TLS 代理:终止 / 透传 / MITM / SNI / mTLS
      • 第06章 SOCKS 协议:SOCKS4/4a/5 与 UDP ASSOCIATE 报文级解析
      • 第07章 HTTP/2、gRPC 与 HTTP/3(QUIC) 代理的挑战
      • 第08章 代理自动配置:PAC / WPAD / 系统代理 / NO_PROXY
    • 层级与转发篇

      • 第09章 L4 代理:TCP/UDP 转发与连接级负载均衡
      • 第10章 L7 代理:协议感知与基于内容的路由
      • 第11章 透明代理:iptables REDIRECT/DNAT、TPROXY 与 eBPF 劫持
      • 第12章 数据搬运的艺术:splice / sendfile / 零拷贝 / io_uring
    • 组件横评篇

      • 第13章 Nginx / OpenResty:反向代理、upstream 与 Lua 可编程
      • 第14章 HAProxy:L4/L7、ACL、健康检查与 stick table
      • 第15章 Envoy:xDS 动态配置与 filter chain,为何是云原生数据面
      • 第16章 Traefik / Caddy:自动服务发现与自动 HTTPS
      • 第17章 Squid 与正向/缓存代理:企业出网、缓存与审计
      • 第18章 mitmproxy:抓包、改包、脚本化调试
      • 第19章 内网穿透与隧道:frp / gost / SSH 隧道 / ngrok
      • 第20章 科学上网生态的技术原理(技术中立)
    • 多语言手写篇

      • 第21章 Go:100 行手写 HTTP/CONNECT + SOCKS5 代理
      • 第22章 Rust:基于 tokio 的高性能 TCP 代理
      • 第23章 Python:asyncio 实现,适合调试与脚本
      • 第24章 C:epoll 裸写与零拷贝,及语言选型对比
    • 容器与K8s篇

      • 第25章 Docker 里的代理:HTTP_PROXY、build/pull 与 daemon 配置
      • 第26章 Sidecar 与流量劫持:Istio init-container 的 iptables 原理
      • 第27章 Ingress 与南北流量:Ingress-nginx 与 Gateway API
      • 第28章 Egress 与出网治理:出口网关、registry mirror、审计
      • 第29章 Service Mesh 数据面:Envoy Sidecar 全链路
    • 进阶篇

      • 第30章 可编程代理:Lua / Wasm / eBPF / xDS,代理的"软件定义"
      • 第31章 性能调优:并发模型、连接池、超时与重试、压测
      • 第32章 排错决策树:502 / 504 / 握手失败 / 环路 / 泄漏
      • 第33章 代理安全:开放代理、SSRF、凭证泄漏与攻击面
    • 底层机制篇

      • 第34章 代理的背压与流控:一个代理最难的部分
      • 第35章 socket 与 TCP 状态机:半关闭、超时、连接生命周期
      • 第36章 HTTP/2 帧、流控与 HPACK:h2 代理的内部机制
      • 第37章 负载均衡算法推导与韧性状态机
      • 第38章 Capstone:把玩具代理改造成生产级骨架
    • 综合实战篇

      • 第39章 企业多跳转发链:拓扑、协议矩阵与贯穿性难题
      • 第40章 端到端实战:把 6 类流量全代理通
      • 第41章 更刁钻的流量:gRPC、长轮询、WebRTC、大文件、双向流
      • 第42章 可落地完整参考实现:一套能跑的多协议转发栈
    • 附录

      • 附录 A:代理协议报文速查(HTTP / SOCKS / PAC / PROXY protocol)
      • 附录 B:组件选型决策树
      • 附录 C:抓包与命令速查

第24章 C:epoll 裸写与零拷贝,及语言选型对比

学习目标

  • 用 C + epoll 写出事件驱动代理的核心循环,理解 Nginx/HAProxy 的底层模型
  • 用 splice 做内核零拷贝转发(第12章 的代码落地)
  • 理解裸写 C 的代价:手动状态机、缓冲、fd 管理、EAGAIN 处理
  • 对全篇四种语言做选型总结:数据面 vs 控制面该选谁

前置知识

  • 第21章 Go、第22章 Rust、第23章 Python
  • 第12章 零拷贝/splice、网络手册 · 多核网络优化

原理

为什么(以及为什么不)裸写 C

C 是最接近内核的语言,能榨干每一点性能、精确控制内存与系统调用。Nginx、HAProxy 是 C,Envoy 是 C++——高性能代理的基石。但代价是:前三章里 Go/Rust/Python 帮你做的事(协程调度、缓冲管理、错误传播),C 全要手动。所以现实中很少有人从零裸写 C 代理,而是理解它以读懂 Nginx 这类代码。

epoll:事件驱动的核心

C 写高并发代理靠 epoll(Linux 的 IO 多路复用,网络手册 · 多核网络优化)。模型是:所有 socket 设为非阻塞,注册到一个 epoll 实例,单线程在 epoll_wait 上等待,哪个就绪处理哪个——一个线程就能管十万连接(C10K/C10M)。

  epoll_create1() → 建 epoll 实例
  epoll_ctl(ADD)  → 注册监听 socket 和每个连接的 fd
  loop:
    n = epoll_wait()              ← 阻塞等待,返回就绪的 fd 列表
    for 每个就绪 fd:
       是监听 fd? → accept 新连接,连后端,注册两端
       是连接 fd? → splice 转发数据
  • ET(边缘触发)vs LT(水平触发):ET 性能更高但必须一次读到 EAGAIN(否则丢事件),是裸写最易错的点
  • 每个连接是一个状态机:因为非阻塞,connect/read/write 都可能"还没好",要记录状态等下次事件

splice 零拷贝(第12章的落地)

第12章 讲过 socket→socket 零拷贝要借一个内核管道。C 里直接用 splice:

  socket_in ──splice──▶ [pipe] ──splice──▶ socket_out
            数据全程在内核,不进用户态

代码:epoll + splice TCP 代理核心

下面是事件驱动代理的核心骨架(为聚焦主线,简化了部分错误处理与连接清理;生产级需完整状态机):

// proxy.c —— gcc -O2 proxy.c -o proxy && ./proxy
// 监听 :8088,把每条连接 splice 转发到 BACKEND
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BACKEND_IP   "93.184.216.34"
#define BACKEND_PORT 80
#define MAX_EVENTS   1024

// 一条代理连接:客户端 fd ↔ 后端 fd,各配一个管道做 splice
typedef struct { int peer; int pipe_r, pipe_w; } conn_t;
static conn_t conns[65536];                  // 按 fd 索引(演示用静态表)

static void set_nonblock(int fd) {
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
}

// 把 from 的数据经管道 splice 到它的 peer,全程零拷贝
static void forward(int from) {
    conn_t *c = &conns[from];
    for (;;) {
        // ① socket → pipe
        ssize_t n = splice(from, NULL, c->pipe_w, NULL, 65536,
                           SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
        if (n <= 0) break;                   // EAGAIN 或对端关闭
        // ② pipe → peer socket
        while (n > 0) {
            ssize_t m = splice(c->pipe_r, NULL, c->peer, NULL, n,
                               SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
            if (m <= 0) break;
            n -= m;
        }
    }
}

static int dial_backend(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in a = {0};
    a.sin_family = AF_INET;
    a.sin_port = htons(BACKEND_PORT);
    inet_pton(AF_INET, BACKEND_IP, &a.sin_addr);
    connect(fd, (struct sockaddr*)&a, sizeof(a));   // 简化:阻塞 connect
    set_nonblock(fd);
    return fd;
}

int main(void) {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    struct sockaddr_in srv = {0};
    srv.sin_family = AF_INET; srv.sin_addr.s_addr = INADDR_ANY; srv.sin_port = htons(8088);
    bind(lfd, (struct sockaddr*)&srv, sizeof(srv));
    listen(lfd, 512);
    set_nonblock(lfd);

    int ep = epoll_create1(0);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = lfd };
    epoll_ctl(ep, EPOLL_CTL_ADD, lfd, &ev);
    printf("C epoll 代理监听 :8088 → %s:%d\n", BACKEND_IP, BACKEND_PORT);

    struct epoll_event events[MAX_EVENTS];
    for (;;) {
        int n = epoll_wait(ep, events, MAX_EVENTS, -1);
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;
            if (fd == lfd) {
                // 新连接:accept + 连后端 + 互相登记 + 各建管道
                int cfd = accept(lfd, NULL, NULL);
                if (cfd < 0) continue;
                set_nonblock(cfd);
                int bfd = dial_backend();
                int p1[2], p2[2]; pipe(p1); pipe(p2);
                conns[cfd] = (conn_t){ .peer = bfd, .pipe_r = p1[0], .pipe_w = p1[1] };
                conns[bfd] = (conn_t){ .peer = cfd, .pipe_r = p2[0], .pipe_w = p2[1] };
                struct epoll_event e1 = { .events = EPOLLIN, .data.fd = cfd };
                struct epoll_event e2 = { .events = EPOLLIN, .data.fd = bfd };
                epoll_ctl(ep, EPOLL_CTL_ADD, cfd, &e1);
                epoll_ctl(ep, EPOLL_CTL_ADD, bfd, &e2);
            } else {
                // 已有连接就绪:零拷贝转发到对端
                forward(fd);
            }
        }
    }
}
gcc -O2 proxy.c -o proxy && ./proxy &
curl -s -H "Host: example.com" http://127.0.0.1:8088/ -o /dev/null -w "%{http_code}\n"   # 200
# strace 验证零拷贝(第12章):
strace -f -e trace=splice -p $(pgrep proxy) 2>&1 | head

对比前三章:Go/Rust/Python 用一行 io.Copy/copy_bidirectional/gather(pipe) 就完成的事,C 里要手动 epoll 注册、管道、状态登记、EAGAIN 循环。这就是"控制力"的代价。 而这段代码恰恰让你看懂了 Nginx 事件循环的本质。


全篇语言选型总对比

四种语言写完,给出选型决策(呼应 第02章 的"能力 vs 性能"权衡):

维度GoRustPythonC/C++
吞吐/延迟高最高低最高
内存安全GC 保证编译期保证运行时❌ 全手动
开发速度快慢最快最慢
并发模型goroutine(有栈)async(无栈)asyncio手写 epoll
零拷贝io.Copy 自动 splice需库难直接 splice
心智负担低高低高(手动一切)
代表作Traefik、frp、Clash、v2raypingora(Cloudflare)mitmproxyNginx、HAProxy、Envoy(C++)

数据面 vs 控制面:黄金分工

云原生代理普遍遵循一条分工(第15章 Envoy 数据面 + 控制面即此):

  数据面(Data Plane):要快、要稳、要省          → C/C++、Rust、Go
    每个请求都经过它,性能 = 钱,尾延迟 = 体验
    例:Envoy(C++)、pingora(Rust)、Nginx(C)

  控制面(Control Plane):要开发快、逻辑复杂、不在数据热路径  → Go、Python
    配置管理、服务发现、下发策略,QPS 低、迭代快
    例:Istiod(Go)、各类控制器(Go)、运维脚本(Python)

一句话选型:

  • 高吞吐核心数据面 → Rust / C++ / C(pingora / Envoy / Nginx)
  • 又要性能又要开发快、单体代理 → Go(Traefik / frp / Clash)
  • 调试工具 / 控制面 / 原型 / 中低流量 → Python(mitmproxy)

排错

现象根因解决
ET 模式偶尔"卡住"漏数据没循环读到 EAGAINET 必须把就绪 fd 读尽
fd 耗尽 Too many open files连接关闭没回收 fd/管道完整 close cfd/bfd/pipe
splice 返回 EINVAL一端不是管道socket↔socket 必须经管道中转
惊群(thundering herd)多进程争 acceptSO_REUSEPORT 或 EPOLLEXCLUSIVE
内存越界/崩溃手动管理出错这正是 Rust 用编译期消灭的问题

本章小结

  • C + epoll 是高性能代理的底层模型(Nginx/HAProxy 即此):非阻塞 socket + epoll 事件循环 + 每连接状态机。
  • splice 在 C 里直接可用,实现 socket↔socket 内核零拷贝(第12章)。
  • 裸写 C 的代价是手动管理一切——这让你读懂了高级语言一行 io.Copy 背后的全部工作。
  • 选型黄金律:数据面要快(C++/Rust/Go),控制面要开发快(Go/Python)。

多语言手写篇至此完结(21 Go / 22 Rust / 23 Python / 24 C)。下一篇 第六篇·容器与 Kubernetes 是你最关心的落地重头戏:从 Docker 的 HTTP_PROXY,到 Istio 的流量劫持、Ingress 南北向、Egress 出网治理、Service Mesh 数据面。

Prev
第23章 Python:asyncio 实现,适合调试与脚本