土法炼钢兴趣小组的算法知识备份

【网络工程】Socket 编程模型演进:从阻塞到多路复用

文章导航

分类入口
network
标签入口
#socket#epoll#io-multiplexing#c10k#network-programming

目录

网络编程模型的选择是高性能服务器的第一个架构决策。从最简单的阻塞 I/O 到 epoll,每一步演进都是为了解决前一种模型的性能瓶颈。理解这些演进不是为了”面试八股”,而是为了在实际工程中做出正确的选择。

一、阻塞 I/O 模型

最直觉的网络编程方式:调用 read() 时,如果数据没有到达,进程就阻塞等待。

1.1 基本实现

// blocking_server.c — 最简单的阻塞 TCP 服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUF_SIZE 1024

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 128);
    
    printf("Blocking server listening on port %d\n", PORT);
    
    // 问题:一次只能处理一个连接
    while (1) {
        int client_fd = accept(server_fd, NULL, NULL);  // 阻塞等待连接
        
        char buf[BUF_SIZE];
        ssize_t n;
        
        while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {  // 阻塞等待数据
            write(client_fd, buf, n);  // echo back
        }
        
        close(client_fd);
    }
    
    return 0;
}

1.2 多进程模型

为了处理多个连接,最早的做法是每个连接 fork 一个子进程:

// fork_server.c — 多进程并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>

#define PORT 8080
#define BUF_SIZE 1024

void handle_client(int client_fd) {
    char buf[BUF_SIZE];
    ssize_t n;
    
    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        write(client_fd, buf, n);
    }
    
    close(client_fd);
    exit(0);
}

void sigchld_handler(int sig) {
    (void)sig;
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
    signal(SIGCHLD, sigchld_handler);
    
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 128);
    
    printf("Fork server listening on port %d\n", PORT);
    
    while (1) {
        int client_fd = accept(server_fd, NULL, NULL);
        
        pid_t pid = fork();
        if (pid == 0) {
            // 子进程
            close(server_fd);
            handle_client(client_fd);
        } else {
            // 父进程
            close(client_fd);
        }
    }
    
    return 0;
}

多进程模型的开销分析:

每个进程的资源消耗:
  内存:    ~4MB(默认栈大小 8MB,COW 共享后实际 ~4MB)
  PID:     占用一个 PID(/proc/sys/kernel/pid_max 默认 32768)
  调度:    上下文切换 ~3-5μs
  创建:    fork() ~100μs

1000 个并发连接 = 1000 个进程
  内存: 4GB
  上下文切换: 1000 × 3μs = 3ms(每轮调度)
  
10000 个并发连接 = 10000 个进程
  内存: 40GB  ← 不可接受
  上下文切换成为主要开销

1.3 多线程模型

线程比进程轻量,但仍有可扩展性问题:

// thread_server.c — 多线程并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUF_SIZE 1024

void *handle_client(void *arg) {
    int client_fd = *(int *)arg;
    free(arg);
    
    pthread_detach(pthread_self());
    
    char buf[BUF_SIZE];
    ssize_t n;
    
    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        write(client_fd, buf, n);
    }
    
    close(client_fd);
    return NULL;
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 128);
    
    printf("Thread server listening on port %d\n", PORT);
    
    while (1) {
        int *client_fd = malloc(sizeof(int));
        *client_fd = accept(server_fd, NULL, NULL);
        
        pthread_t tid;
        pthread_create(&tid, NULL, handle_client, client_fd);
    }
    
    return 0;
}

线程 vs 进程的资源对比:

每个线程的资源消耗:
  栈空间:   ~64KB-1MB(可配置,默认 8MB 但实际使用少)
  线程控制块: ~几KB
  创建:     ~10-50μs(比 fork 快 10 倍)
  上下文切换: ~1-3μs

10000 线程(栈设为 64KB):
  栈内存:  640MB(可接受)
  调度开销: 仍然显著
  
问题:
  - 线程数 > CPU 核数时,大量时间花在上下文切换
  - 锁竞争随线程数增加而恶化
  - 栈内存是预分配的,即使不用也占虚拟地址空间

二、非阻塞 I/O

将 socket 设为非阻塞后,read() 在没有数据时立即返回 EAGAIN 而不是阻塞:

// 设置非阻塞
#include <fcntl.h>

int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 非阻塞 read
ssize_t n = read(fd, buf, BUF_SIZE);
if (n < 0) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // 没有数据可读,稍后重试
    } else {
        // 真正的错误
    }
}

非阻塞 I/O 的问题——忙轮询(Busy Polling):

// 忙轮询模式(CPU 利用率 100%,极其浪费)
while (1) {
    for (int i = 0; i < num_clients; i++) {
        ssize_t n = read(clients[i], buf, BUF_SIZE);
        if (n > 0) {
            // 处理数据
        }
        // n < 0 && errno == EAGAIN → 跳过,继续轮询下一个
    }
    // 没有数据时也在不停循环
}

纯非阻塞 I/O 几乎不单独使用——它需要配合 I/O 多路复用才有意义。

三、select

select() 是第一个 I/O 多路复用系统调用(POSIX 标准,1983 年 4.2BSD):

3.1 实现原理

// select_server.c — 基于 select 的并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUF_SIZE 1024
#define MAX_CLIENTS 1024

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 128);
    
    int clients[MAX_CLIENTS];
    int num_clients = 0;
    
    printf("Select server listening on port %d\n", PORT);
    
    while (1) {
        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(server_fd, &read_fds);
        int max_fd = server_fd;
        
        // 把所有客户端 fd 加入监听集合
        for (int i = 0; i < num_clients; i++) {
            FD_SET(clients[i], &read_fds);
            if (clients[i] > max_fd) max_fd = clients[i];
        }
        
        // 阻塞等待任意 fd 就绪
        int ready = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (ready < 0) break;
        
        // 检查是否有新连接
        if (FD_ISSET(server_fd, &read_fds)) {
            int client_fd = accept(server_fd, NULL, NULL);
            if (num_clients < MAX_CLIENTS) {
                clients[num_clients++] = client_fd;
            }
        }
        
        // 检查已有连接是否有数据
        for (int i = 0; i < num_clients; i++) {
            if (FD_ISSET(clients[i], &read_fds)) {
                char buf[BUF_SIZE];
                ssize_t n = read(clients[i], buf, BUF_SIZE);
                
                if (n <= 0) {
                    // 连接关闭
                    close(clients[i]);
                    clients[i] = clients[--num_clients];
                    i--;
                } else {
                    write(clients[i], buf, n);
                }
            }
        }
    }
    
    close(server_fd);
    return 0;
}

3.2 select 的性能问题

select 的三个硬伤:

1. FD 上限
   - fd_set 是固定大小的位图,默认 FD_SETSIZE = 1024
   - 在 Linux 上可以重新编译修改,但受限于位图扫描效率
   - 超过 1024 个连接 → 需要改用 poll 或 epoll

2. 线性扫描
   - 每次 select 返回后,必须遍历所有 fd 检查 FD_ISSET
   - 时间复杂度: O(max_fd)
   - 1000 个连接中只有 1 个就绪 → 仍需检查 1000 次

3. 每次调用都要重新构建 fd_set
   - select 会修改传入的 fd_set(标记就绪的 fd)
   - 每次调用前必须重新 FD_ZERO + FD_SET 所有 fd
   - fd_set 的拷贝开销: O(max_fd / 8) bytes

系统调用开销:
  select(1024, ...)   → ~5μs
  select(4096, ...)   → ~20μs
  select(65536, ...)  → ~300μs(如果 FD_SETSIZE 增大)

四、poll

poll() 解决了 select 的 FD 上限问题,但线性扫描问题仍在:

// poll_server.c — 基于 poll 的并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUF_SIZE 1024
#define INIT_CAPACITY 64

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 128);
    
    // 动态数组管理 pollfd
    int capacity = INIT_CAPACITY;
    struct pollfd *fds = malloc(sizeof(struct pollfd) * capacity);
    int nfds = 0;
    
    // 第一个 pollfd 是 server socket
    fds[0].fd = server_fd;
    fds[0].events = POLLIN;
    nfds = 1;
    
    printf("Poll server listening on port %d\n", PORT);
    
    while (1) {
        int ready = poll(fds, nfds, -1);  // -1 = 无限等待
        if (ready < 0) break;
        
        // 检查 server socket
        if (fds[0].revents & POLLIN) {
            int client_fd = accept(server_fd, NULL, NULL);
            
            // 动态扩容
            if (nfds >= capacity) {
                capacity *= 2;
                fds = realloc(fds, sizeof(struct pollfd) * capacity);
            }
            
            fds[nfds].fd = client_fd;
            fds[nfds].events = POLLIN;
            nfds++;
        }
        
        // 检查客户端 socket
        for (int i = 1; i < nfds; i++) {
            if (fds[i].revents & (POLLIN | POLLERR | POLLHUP)) {
                char buf[BUF_SIZE];
                ssize_t n = read(fds[i].fd, buf, BUF_SIZE);
                
                if (n <= 0) {
                    close(fds[i].fd);
                    fds[i] = fds[--nfds];  // 用最后一个替换
                    i--;
                } else {
                    write(fds[i].fd, buf, n);
                }
            }
        }
    }
    
    free(fds);
    close(server_fd);
    return 0;
}

poll vs select 对比:

                    select              poll
──────────────────────────────────────────────────────
FD 上限          FD_SETSIZE(1024)     无限制(数组动态增长)
数据结构         位图(fd_set)          结构体数组(pollfd)
每次调用的拷贝   重建 fd_set           fds 数组拷贝到内核
就绪检查         O(max_fd) 线性扫描   O(nfds) 线性扫描
内核实现         遍历 fd_set           遍历 pollfd 数组
性能特征         fd 值越大越慢         fd 数量越多越慢

共同问题:
- 都需要线性扫描所有 fd
- 都需要在用户空间和内核空间之间拷贝 fd 集合
- 10000 个连接中 100 个就绪 → 仍需遍历 10000 个

五、epoll

epoll 是 Linux 2.6 引入的 I/O 多路复用机制,从根本上解决了 select/poll 的性能问题:

5.1 核心 API

// epoll 三个核心系统调用

// 1. 创建 epoll 实例
int epfd = epoll_create1(0);

// 2. 注册/修改/删除监听的 fd
struct epoll_event ev;
ev.events = EPOLLIN;       // 监听可读事件
ev.data.fd = client_fd;    // 关联的用户数据
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);  // 注册
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &ev);  // 修改
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL);  // 删除

// 3. 等待事件(只返回就绪的 fd)
struct epoll_event events[MAX_EVENTS];
int nready = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);
// nready = 就绪的 fd 数量
// events[0..nready-1] = 就绪的事件

5.2 完整实现

// epoll_server.c — 基于 epoll 的高性能并发服务器
#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>

#define PORT 8080
#define MAX_EVENTS 1024
#define BUF_SIZE 4096

void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY,
        .sin_port = htons(PORT),
    };
    
    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 4096);
    set_nonblocking(server_fd);
    
    // 创建 epoll 实例
    int epfd = epoll_create1(0);
    
    // 注册 server socket
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
    
    struct epoll_event events[MAX_EVENTS];
    
    printf("Epoll server listening on port %d\n", PORT);
    
    while (1) {
        // 只返回就绪的 fd,无需扫描所有连接
        int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
        
        for (int i = 0; i < nready; i++) {
            int fd = events[i].data.fd;
            
            if (fd == server_fd) {
                // 接受新连接
                while (1) {
                    int client_fd = accept(server_fd, NULL, NULL);
                    if (client_fd < 0) {
                        if (errno == EAGAIN) break;  // 所有连接已接受
                        break;
                    }
                    
                    set_nonblocking(client_fd);
                    
                    ev.events = EPOLLIN | EPOLLET;  // Edge Triggered
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
                }
            } else {
                // 处理客户端数据
                if (events[i].events & (EPOLLERR | EPOLLHUP)) {
                    close(fd);
                    continue;
                }
                
                char buf[BUF_SIZE];
                while (1) {
                    ssize_t n = read(fd, buf, BUF_SIZE);
                    if (n < 0) {
                        if (errno == EAGAIN) break;  // 数据读完
                        close(fd);
                        break;
                    }
                    if (n == 0) {
                        close(fd);
                        break;
                    }
                    write(fd, buf, n);
                }
            }
        }
    }
    
    close(epfd);
    close(server_fd);
    return 0;
}

5.3 epoll 内部数据结构

epoll 的内核实现:

┌──────────────────────────────────────────────┐
│              epoll 实例(epfd)                │
│                                              │
│  ┌─────────────────────┐  ┌────────────────┐ │
│  │    红黑树(RB-Tree)   │  │ 就绪链表        │ │
│  │                     │  │ (Ready List)    │ │
│  │  存储所有监听的 fd    │  │                │ │
│  │  增删改: O(log n)    │  │ 存储就绪的 fd   │ │
│  │                     │  │ 插入: O(1)      │ │
│  │    ┌───┐            │  │                │ │
│  │    │fd5│            │  │ fd3 → fd7 →    │ │
│  │   ╱    ╲            │  │ fd12 → NULL    │ │
│  │ ┌───┐ ┌───┐        │  │                │ │
│  │ │fd3│ │fd7│        │  │                │ │
│  │ ╱  ╲   ╲          │  │                │ │
│  │┌──┐┌──┐ ┌──┐      │  │                │ │
│  ││f1││f4│ │f12│      │  │                │ │
│  │└──┘└──┘ └──┘      │  │                │ │
│  └─────────────────────┘  └────────────────┘ │
│                                              │
│  工作流程:                                    │
│  1. epoll_ctl(ADD) → 在红黑树中插入 fd         │
│  2. fd 就绪时 → 内核回调将 fd 加入就绪链表      │
│  3. epoll_wait() → 直接从就绪链表取出就绪 fd    │
│     不需要遍历所有 fd!                         │
└──────────────────────────────────────────────┘

5.4 性能对比

模型性能对比(echo server,单线程):

                select      poll        epoll
──────────────────────────────────────────────────────
100 连接       12μs/call   10μs/call   8μs/call
1000 连接      120μs/call  100μs/call  10μs/call
10000 连接     1.2ms/call  1.0ms/call  12μs/call
100000 连接    N/A         10ms/call   15μs/call

关键差异:
  select/poll: O(n) 随连接数线性增长
  epoll:       O(1) 几乎不随连接数变化(只和就绪数有关)

吞吐量实测(echo server,1000 并发,每秒请求数):

  阻塞 + fork:    ~500 req/s    (fork 开销太大)
  阻塞 + 线程:    ~5,000 req/s   (上下文切换开销)
  select:         ~20,000 req/s  (FD_SETSIZE 限制)
  poll:           ~18,000 req/s  (线性扫描)
  epoll:          ~100,000 req/s (只处理就绪的 fd)

六、从 C10K 到 C1M

6.1 C10K 问题

1999 年 Dan Kegel 提出的”C10K 问题”——如何让单台服务器处理 10000 个并发连接:

C10K 的核心挑战(1999 年):

硬件限制:
  CPU:     单核 500MHz
  内存:    256MB-1GB
  网络:    100Mbps

软件限制:
  每连接一线程: 10000 线程 × 8MB 栈 = 80GB ← 不可能
  select:       FD_SETSIZE = 1024 ← 不够
  内核:         进程/线程调度开销显著

解决方案:
  1. epoll / kqueue / IOCP 替代 select/poll
  2. 非阻塞 I/O + 事件驱动
  3. 单线程/少线程模型
  
  → Nginx(2004)、Node.js(2009)都是 C10K 的产物

6.2 C1M(百万连接)的挑战

C1M 的额外挑战:

1. 文件描述符限制
   默认: ulimit -n = 1024
   系统: /proc/sys/fs/file-max
   需要: 1,000,000+

2. 内存消耗
   每个 TCP 连接的内核内存: ~3.5KB
   1M 连接: 3.5GB 仅内核态
   加上应用层缓冲: 10-20GB

3. 端口消耗(作为客户端时)
   端口范围: 1024-65535 = ~64000 个
   1M 连接需要多个源 IP

4. 中断处理
   网卡中断集中在一个 CPU 核上
   需要 RSS(Receive Side Scaling)分散中断

达到 C1M 的内核调优:

#!/bin/bash
# c1m-tuning.sh — 百万连接内核参数调优

# 文件描述符限制
echo "fs.file-max = 2000000" >> /etc/sysctl.conf
echo "fs.nr_open = 2000000" >> /etc/sysctl.conf

# 进程级别限制
cat >> /etc/security/limits.conf << EOF
* soft nofile 1000000
* hard nofile 1000000
EOF

# TCP 内存优化(减少每连接内存)
echo "net.ipv4.tcp_mem = 786432 1048576 1572864" >> /etc/sysctl.conf
echo "net.ipv4.tcp_rmem = 4096 4096 16384" >> /etc/sysctl.conf
echo "net.ipv4.tcp_wmem = 4096 4096 16384" >> /etc/sysctl.conf

# 连接表扩容
echo "net.core.somaxconn = 65535" >> /etc/sysctl.conf
echo "net.ipv4.tcp_max_syn_backlog = 65535" >> /etc/sysctl.conf
echo "net.core.netdev_max_backlog = 65535" >> /etc/sysctl.conf

# 端口范围
echo "net.ipv4.ip_local_port_range = 1024 65535" >> /etc/sysctl.conf

# TIME_WAIT 优化
echo "net.ipv4.tcp_tw_reuse = 1" >> /etc/sysctl.conf
echo "net.ipv4.tcp_fin_timeout = 15" >> /etc/sysctl.conf

# conntrack 表(如果使用)
echo "net.netfilter.nf_conntrack_max = 2000000" >> /etc/sysctl.conf

sysctl -p

七、模型选型决策

选型决策矩阵:

场景                          │ 推荐模型
─────────────────────────────┼──────────────────────────
简单工具/脚本(<10 连接)      │ 阻塞 I/O(最简单)
中等并发(<1000 连接)         │ poll 或 线程池
高并发(>1000 连接)           │ epoll + 非阻塞
百万连接                      │ epoll + 单线程事件循环
跨平台(macOS/BSD)           │ kqueue(或 libevent/libuv 封装)
Windows                      │ IOCP

语言/框架与底层模型的对应关系:
  Nginx        → epoll(Linux)/ kqueue(BSD)
  Node.js      → libuv(epoll/kqueue/IOCP 封装)
  Go           → netpoller(epoll/kqueue 封装)
  Java NIO     → epoll(Linux)/ kqueue(macOS)
  Rust tokio   → mio(epoll/kqueue/IOCP 封装)
  Python asyncio → selectors(epoll/kqueue/select 封装)

八、总结

网络编程模型的演进路线:

  1. 阻塞 I/O + 多进程/线程:最直觉,但受限于进程/线程的资源开销,C1K 级别
  2. select:第一个多路复用方案,但有 FD 上限和线性扫描问题
  3. poll:去掉了 FD 上限,但线性扫描问题仍在
  4. epoll:通过红黑树 + 就绪链表实现 O(1) 事件通知,解决了 C10K

核心原则:不要为不活跃的连接付出代价。epoll 只在 fd 就绪时通知你,而 select/poll 要求你主动检查每一个 fd。


上一篇: CDN 故障调试:缓存命中率、回源异常与头分析 下一篇: epoll 深度剖析:ET/LT 模式、源码分析与性能特征

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-08-06 · network

【网络工程】可编程数据平面与 P4:软件定义转发

传统网络设备的转发逻辑固化在硬件中。P4 语言让交换机的转发管线可编程——你可以定义自己的包头解析、匹配规则和转发动作。本文从 P4 语言核心概念出发,讲解 Parser/Match-Action/Deparser 的编程模型、可编程交换机芯片(Tofino)的架构、P4 在数据中心和运营商网络中的应用案例,以及 P4 与 eBPF 的定位差异。

2025-07-26 · network

【网络工程】网络 I/O 模式:Reactor、Proactor 与协程

Reactor 和 Proactor 是网络服务器的两种核心 I/O 处理模式。本文从单线程 Reactor、多线程主从 Reactor、Proactor 与 io_uring 的天然契合,到 Go goroutine、Rust async 和 Java Virtual Thread 的协程网络 I/O 对比,系统分析各模式的适用场景与工程权衡。


By .