找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4700

积分

1

好友

649

主题
发表于 2 小时前 | 查看: 1| 回复: 0

说到高并发架构,很多开发者首先想到的是C++、Go这些语言,却忽略了纯C语言贴近系统底层、可极致压榨硬件性能的先天优势。今天我们就用纯C语言,从原理到实战,一步步实现一个能够承载百万连接的服务器,其核心正是 epoll+Reactor 这套黄金组合。

一、传统阻塞 IO 为何撑不起百万连接

在网络编程的早期,传统阻塞 I/O 模型凭借简单直观的设计思路,成为构建网络应用的首选。其典型模式是“一连接一线程”,即每有一个客户端连接到服务器,服务器就创建一个新线程来专门处理该连接的所有 I/O 操作。这就好比一家餐厅,每来一桌客人,就安排一位专属服务员全程服务。

但在高并发场景下,这种模型的弊端暴露无遗。当并发连接数达到万级甚至百万级时,大量线程的创建和管理成为沉重负担。线程的上下文切换开销巨大,每次切换都需要保存和恢复线程的运行环境,这会占用大量 CPU 时间。同时,每个线程都需要占用一定的内存空间,海量线程会迅速耗尽服务器的内存资源,导致系统性能急剧下降,甚至崩溃。

因此,传统阻塞 I/O 模型在面对高并发时,就像一辆小马拉大车的破旧马车,无论如何也无法承载百万并发的庞大负载,急需更高效的解决方案来打破这一性能瓶颈。

二、epoll+Reactor 为何能行?

传统阻塞IO搭配多线程的架构,天生无法适配高并发场景,想要突破百万连接瓶颈,势必需要一套更高效、更轻量化的IO解决方案。

epoll 是 Linux 内核提供的一种高效 I/O 多路复用机制,是 select 和 poll 的增强版。它通过三个系统调用 epoll_createepoll_ctlepoll_wait 实现对大量文件描述符的高效管理。epoll 内部使用红黑树来存储被监控的文件描述符,当有事件发生时,内核将就绪的文件描述符放入一个就绪链表中,epoll_wait 只需遍历这个链表,而无需像 select 和 poll 那样遍历所有文件描述符,这使得 epoll 的时间复杂度从 O(n) 降至 O(1),大大提高了事件监听的效率。

Reactor 模式则是一种事件驱动的设计模式,它将 I/O 事件的监听和处理分离。Reactor 负责监听文件描述符上的事件,一旦有事件发生,就将其分发给对应的事件处理器进行处理。这就好比一个大型活动的调度中心,有专人负责收集各方信息,然后将任务分配给不同的执行小组。

当 epoll 与 Reactor 模式相结合,便产生了1+1>2的质变。epoll 高效的事件监听能力为 Reactor 提供了坚实的基础,使其能够轻松应对百万级别的并发连接;而 Reactor 模式的事件分发机制,则充分发挥了 epoll 的优势,确保每个事件都能得到及时、准确的处理。在这种组合下,服务器可以用单线程或少量线程管理海量连接,极大地减少了线程上下文切换开销和内存占用,为实现百万并发提供了可靠的技术支撑。

三、原理:epoll 与 Reactor 模式深度拆解

仅照搬代码并非真正掌握,唯有吃透底层原理,方能灵活优化、按需拓展。接下来我们拆解epoll与Reactor模式的核心逻辑。

3.1 epoll:高并发的“性能王者”

3.1.1 epoll 的核心机制:红黑树 + 就绪链表

epoll 之所以能在高并发场景中脱颖而出,得益于其精妙的内部实现机制,而红黑树与就绪链表的组合堪称其中的精髓。

epoll的操作逻辑简洁明晰,核心依托三大系统调用,掌握即可把握其精髓:epoll_create,用于创建epoll实例,相当于搭建专属的事件监控中枢;epoll_ctl,实现待监听文件描述符的增删改操作,完成监控任务的配置;epoll_wait,阻塞等待事件触发,实时接收监控中枢的事件通知。

其远超select与poll的效率,核心源于内部红黑树与就绪链表的精妙设计。红黑树负责存储全量待监听文件描述符,增删查改操作效率极高;当某一连接产生读写事件(如数据可读),内核会自动将该连接移入就绪链表,无需全量检索。epoll_wait执行时,无需遍历所有连接,仅需扫描就绪链表,按需处理活跃事件,杜绝无效算力消耗,这便是epoll能承载海量并发的核心奥义。

3.1.2 LT 与 ET:触发模式的选择

epoll包含水平触发LT与边缘触发ET两种模式。

水平触发是 epoll 的默认触发模式,它的工作机制较为直观。在 LT 模式下,只要文件描述符对应的缓冲区中还有未处理的数据,epoll_wait 就会持续通知应用程序。例如,当一个 socket 接收缓冲区中有数据时,即使应用程序一次没有读取完所有数据,下次调用 epoll_wait 时,依然会收到该 socket 的可读事件通知,直到缓冲区数据被完全处理。这种模式的优点是编程相对简单,不容易遗漏事件。

在 ET 模式下,epoll_wait 仅在文件描述符的状态发生变化时,即从无数据变为有数据(可读事件)或从可写变为不可写(可写事件)等状态转变时,才会通知应用程序一次。这就要求应用程序在收到通知后,必须一次性尽可能多地处理完所有可用数据。ET 模式通常需要配合非阻塞 I/O 使用,虽然编程难度较高,但它能有效减少 epoll_wait 的调用次数,提高系统的整体效率,非常适合高并发、大流量的服务器程序。

3.2 Reactor 模式:事件驱动的“反应堆”架构

3.2.1 Reactor 模式的核心组件

Reactor 模式作为一种事件驱动的设计模式,其核心包含四大组件:

  1. 句柄:也可理解为文件描述符,是操作系统提供的一种抽象,用于标识各种 I/O 资源,如网络 socket、文件等。它是产生 I/O 事件的源头。
  2. 同步事件分离器:在基于 Linux 的实现中通常就是 epoll,它负责监听多个句柄上的 I/O 事件,能够同时监控大量句柄,一旦有事件发生,就迅速捕捉到并通知后续组件。
  3. 事件分发器:也可称为事件循环,是 Reactor 模式的“调度员”。它不断地循环执行,调用同步事件分离器获取就绪事件,然后根据事件类型将其分发给对应的事件处理器。
  4. 事件处理器:则是真正执行具体业务逻辑的“工人”,它是一系列回调函数的集合。每个事件处理器对应一种特定类型的事件,如处理新连接的 AcceptHandler、处理数据读取的 ReadHandler 等。

这些组件之间分工明确、相互协作,并且高度解耦,使得系统在面对高并发场景时,具有极强的扩展性。

3.2.2 Reactor 的工作流程:注册 - 等待 - 分发 - 处理

Reactor 模式的工作流程是一个严谨且高效的事件处理闭环。

  1. 注册:在服务器初始化阶段,首先会将需要监听的句柄(如监听 socket 的文件描述符)及其感兴趣的事件注册到同步事件分离器(epoll)中。
  2. 等待:注册完成后,事件分发器进入事件循环,调用 epoll_wait 开始阻塞等待事件的发生。
  3. 分发:当有事件发生时,epoll_wait 会被唤醒,返回就绪事件的集合。事件分发器拿到就绪事件后,会根据事件类型将其分发给对应的事件处理器。
  4. 处理:事件处理器收到事件后,会执行预先定义好的回调函数,完成具体的业务逻辑处理。处理完成后,系统又回到事件循环,继续等待下一轮事件的到来。

通过这样的流程,Reactor 模式能够有条不紊地处理大量并发 I/O 事件,确保服务器高效稳定地运行。

四、纯 C 实战:从零搭建百万并发服务器

接下来我们进入实战环节,以纯C搭建基于epoll+Reactor的百万并发服务器。

4.1 核心数据结构设计:连接管理的关键

4.1.1 conn 结构体:封装连接的核心属性

实现海量连接的高效管理,第一步是设计规整的连接结构体。conn 结构体精心封装了每个连接的关键属性。

typedef int (*RCALLBACK)(int fd);

struct conn {
    int fd; // 连接对应的文件描述符,如同连接的“身份证”,是识别和操作连接的关键标识
    char rbuffer[BUFFER_LENGTH]; // 读缓冲区,用于存储从客户端接收的数据
    int rlength; // 读缓冲区中已接收数据的长度
    char wbuffer[BUFFER_LENGTH]; // 写缓冲区,存放即将发送给客户端的数据
    int wlength; // 写缓冲区中待发送数据的长度
    RCALLBACK send_callback; // 发送数据时的回调函数指针
    union {
        RCALLBACK recv_callback; // 接收数据时的回调函数指针
        RCALLBACK accept_callback; // 接受新连接时的回调函数指针
    } recv_action;
};

fd 字段作为文件描述符,是操作系统用于标识连接的唯一编号。读缓冲区 rbuffer 和写缓冲区 wbuffer 的设计至关重要,将读写缓冲区分离开来,各司其职,提高了数据处理的效率和稳定性。而回调函数指针则为服务器的事件驱动处理机制提供了强大的灵活性,当特定的事件发生时,服务器通过这些指针调用相应的回调函数。

4.1.2 数组 + Union:高效管理连接与回调

为了实现对百万级连接的高效管理和回调函数的灵活调用,我们采用了数组结合 Union 类型的巧妙设计。

struct conn conn_list[CONN_SIZE] = {0};

这里定义的 conn_list 数组,以连接的文件描述符 fd 作为数组下标,实现了对连接信息的 O(1) 时间复杂度访问。无论并发连接数有多少,都能在极短的时间内定位到特定连接的相关信息。

union {
    RCALLBACK recv_callback;
    RCALLBACK accept_callback;
} recv_action;

在 conn 结构体中,recv_action 字段采用 Union 类型,将 recv_callbackaccept_callback 这两个回调函数指针封装在一起。由于在同一时刻,一个连接要么是在接受新连接,要么是在接收数据,不会同时发生这两种情况,所以使用 Union 类型可以在不影响功能的前提下,巧妙地减少内存占用。

4.2 Reactor 核心模块实现

4.2.1 服务器初始化:多端口监听的小技巧

服务器搭建的第一步为初始化流程,涵盖socket创建、端口绑定、监听启动等核心步骤。

int init_server(unsigned short port){
    // 创建TCP流式socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有网卡
    servaddr.sin_port = htons(port);
    // 端口绑定,失败则打印错误信息
    if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
        printf(“bind failed: %s\n”, strerror(errno));
        return -1;
    }
    listen(sockfd, 10); // 启动监听,设置监听队列长度
    return sockfd;
}

init_server 函数中,首先通过 socket 系统调用创建一个 TCP 套接字。接着,设置服务器地址结构,将地址族设置为 AF_INET,IP 地址设置为 INADDR_ANY。随后,使用 bind 系统调用将创建的套接字绑定到指定的地址和端口上。最后,通过 listen 系统调用将套接字设置为监听状态。

这里还有一个提升并发处理能力的小技巧,就是采用多端口监听策略。在实际应用中,一个端口所能承载的最大并发连接数是有限的。通过监听多个端口,可以将并发连接分散到不同端口上,从而有效减轻单端口的 backlog 压力。同时,在设置 listen 参数时,还可以结合内核调优(如调整 somaxconn 参数)来进一步提升服务器性能。

4.2.2 三大回调函数:accept/recv/send

Reactor模式的核心落地依托 accept_cbrecv_cbsend_cb 这三大回调函数,分别对应新连接接入、数据读取、数据发送三大核心场景。

accept_cb 函数负责处理新连接的接受:

int accept_cb(int fd){
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    // 接收客户端新连接
    int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
    if (clientfd < 0) return -1;
    // 为新连接注册可读事件
    event_register(clientfd, EPOLLIN);
    // 每千条连接打印日志,便于调试与连接数监控
    if (clientfd % 1000 == 0) {
        struct timeval current;
        gettimeofday(¤t, NULL);
        int time_used = TIME_SUB_MS(current, begin);
        memcpy(&begin, ¤t, sizeof(struct timeval));
        printf(“accept finshed:%d, time_used:%d\n”, clientfd, time_used);
    }
    return 0;
}

recv_cb 函数用于处理客户端数据的接收:

int recv_cb(int fd){
    // 读取客户端上行数据
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    // 客户端断开连接,释放对应资源
    if (count == 0) {
        printf(“client finshed: %d\n”, fd);
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    // 数据存入读缓冲区,配置回显数据
    conn_list[fd].rlength = count;
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
    // 切换为可写事件,准备数据下发
    set_event(fd, EPOLLOUT, 0);
    return count;
}

send_cb 函数负责将数据发送给客户端:

int send_cb(int fd) {
    // 向客户端下发数据
    int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
    // 发送完毕,切回可读事件,等待下一轮数据交互
    set_event(fd, EPOLLIN, 0);
    return count;
}

通过不断地在 EPOLLINEPOLLOUT 事件之间切换,实现了数据的高效收发和连接状态的有效管理,构成了一个完整的事件处理状态机。

4.2.3 事件循环:Reactor架构的核心引擎

事件循环是Reactor架构的核心引擎,相当于服务器的“心脏”,持续驱动事件监听与处理。

while (1) {
    struct epoll_event events[1024] = {0};
    // 阻塞等待事件触发
    int nready = epoll_wait(epfd, events, 1024, -1);
    // 遍历活跃事件,精准分发处理
    for (int i = 0; i < nready; i++) {
        int connfd = events[i].data.fd;
        // 可读事件触发,调用对应接收/接入回调
        if (events[i].events & EPOLLIN) {
            conn_list[connfd].recv_action.recv_callback(connfd);
        }
        // 可写事件触发,调用数据发送回调
        if (events[i].events & EPOLLOUT) {
            conn_list[connfd].send_callback(connfd);
        }
    }
}

在这个主事件循环中,首先通过 epoll_wait 系统调用阻塞等待事件的发生。当有事件发生时,epoll_wait 会返回就绪事件的数量 nready。接着,遍历这些就绪事件,根据事件类型(EPOLLINEPOLLOUT)调用相应的回调函数进行处理。这个循环不断地重复执行,确保服务器能够及时响应每个连接的请求,是整个百万并发服务器的灵魂所在。

五、百万并发的关键优化策略

代码编写完成后,切勿直接运行,想要真正实现百万并发承载,系统层面与代码层面的优化必不可少。Linux系统默认配置存在诸多性能限制,未优化前难以突破万级连接。

5.1 系统参数调优

Linux系统默认参数对文件描述符数量、TCP队列长度等设置严苛,无法适配百万并发需求,需通过内核参数调优,放开性能限制:

其一,放开文件描述符限制:每条连接占用一个FD,默认1024的上限远不足以支撑百万并发,需修改/etc/security/limits.conf,配置软限制与硬限制为* soft nofile 1048576* hard nofile 1048576;同步修改/etc/sysctl.conf,设置fs.file-max = 2097152,执行sysctl -p生效。

其二,调大TCP监听队列:默认somaxconn仅为128,高并发场景下极易出现连接丢包,需修改net.core.somaxconn = 65535,扩容监听队列长度。

其三,优化内存交换策略:调低vm.swappiness=10,引导系统优先使用物理内存,规避磁盘交换带来的性能暴跌。

5.2 代码层优化:非阻塞 IO + 内存复用

代码层面的两项核心优化,可进一步提升并发性能:

启用非阻塞IO:通过将 FD 设置为非阻塞模式,当 I/O 操作无法立即完成时,系统会立即返回错误而不会阻塞线程,服务器可以继续处理其他连接的事件。可以使用 fcntl 函数来设置:

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

践行内存复用:频繁地进行内存分配和释放会产生较大开销。可以采用内存复用策略,即预先分配一块较大的内存空间作为内存池,需要时从中获取,使用完毕后再归还,减少系统调用和内存碎片。

5.3 连接管理:超时清理与资源回收

百万并发服务器长期运行,易出现无效连接占用资源、内存泄漏等问题,需建立完善的超时清理机制

可以添加时间轮定时器,为每条连接配置超时阈值,长时间无数据交互的无效连接,自动关闭FD、从epoll中移除、释放对应内存资源。此外,需养成规范的资源回收习惯,客户端断开连接时,务必及时关闭FD、通过epoll_ctl删除对应事件,从根源规避资源泄漏。

六、源码:服务器 + 客户端

6.1 百万并发服务器源码(纯 C)

下面是百万并发服务器 C 语言完整源码:

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/time.h>

// 缓冲区大小
#define BUFFER_LENGTH 1024
// 最大连接数
#define CONNECTION_SIZE 1048576
// 最大监听端口数
#define MAX_PORTS 20
// 计算两个时间差,单位为毫秒
#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

// 定义回调函数指针类型
typedef int (*RCALLBACK)(int fd);

// 连接结构体定义
struct conn {
    int fd; // 连接对应的文件描述符
    char rbuffer[BUFFER_LENGTH]; // 读缓冲区
    int rlength; // 读缓冲区中已接收数据的长度
    char wbuffer[BUFFER_LENGTH]; // 写缓冲区
    int wlength; // 写缓冲区中待发送数据的长度
    RCALLBACK send_callback; // 发送数据时的回调函数指针
    union {
        RCALLBACK recv_callback; // 接收数据时的回调函数指针
        RCALLBACK accept_callback; // 接受新连接时的回调函数指针
    } recv_action;
};

// epoll实例的文件描述符
int epfd = 0;
// 记录开始时间
struct timeval begin;
// 连接列表,以文件描述符为下标,方便快速访问
struct conn conn_list[CONNECTION_SIZE] = {0};

// 设置事件,flag为1时添加事件,为0时修改事件
int set_event(int fd, int event, int flag){
    if (flag) { // non-zero add
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    } else { // zero mod
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
    return 0;
}

// 注册事件到epoll实例,并初始化连接相关信息
int event_register(int fd, int event){
    if (fd < 0) return -1;
    conn_list[fd].fd = fd;
    conn_list[fd].recv_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].rlength = 0;
    memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].wlength = 0;
    set_event(fd, event, 1);
    return 0;
}

// 接受新连接的回调函数
int accept_cb(int fd){
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
    if (clientfd < 0) {
        printf(“accept errno: %d --> %s\n”, errno, strerror(errno));
        return -1;
    }
    // 注册新连接的读事件
    event_register(clientfd, EPOLLIN);
    if ((clientfd % 1000) == 0) {
        struct timeval current;
        gettimeofday(¤t, NULL);
        int time_used = TIME_SUB_MS(current, begin);
        memcpy(&begin, ¤t, sizeof(struct timeval));
        printf(“accept finshed: %d, time_used: %d\n”, clientfd, time_used);
    }
    return 0;
}

// 接收数据的回调函数
int recv_cb(int fd){
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    if (count == 0) { // 客户端断开连接
        printf(“client disconnect: %d\n”, fd);
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    } else if (count < 0) { // 接收数据出错
        printf(“count: %d, errno: %d, %s\n”, count, errno, strerror(errno));
        close(fd);
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
        return 0;
    }
    conn_list[fd].rlength = count;
    // 简单回显,将接收到的数据复制到写缓冲区
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
    // 修改为监听写事件,准备发送数据
    set_event(fd, EPOLLOUT, 0);
    return count;
}

// 发送数据的回调函数
int send_cb(int fd){
    int count = 0;
    if (conn_list[fd].wlength != 0) {
        count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
    }
    // 发送完成后重新监听读事件
    set_event(fd, EPOLLIN, 0);
    return count;
}

// 初始化服务器,创建监听套接字并绑定端口
int init_server(unsigned short port){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意地址
    servaddr.sin_port = htons(port);
    if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
        printf(“bind failed: %s\n”, strerror(errno));
        return -1;
    }
    listen(sockfd, 10); // 监听队列长度为10
    return sockfd;
}

int main(){
    unsigned short port = 2000;
    epfd = epoll_create(1);
    int i = 0;
    // 监听多个端口,提升并发处理能力
    for (i = 0; i < MAX_PORTS; i++) {
        int sockfd = init_server(port + i);
        conn_list[sockfd].fd = sockfd;
        conn_list[sockfd].recv_action.accept_callback = accept_cb;
        set_event(sockfd, EPOLLIN, 1);
    }
    gettimeofday(&begin, NULL);
    while (1) { // 主事件循环
        struct epoll_event events[1024] = {0};
        int nready = epoll_wait(epfd, events, 1024, -1);
        int i = 0;
        for (i = 0; i < nready; i++) {
            int connfd = events[i].data.fd;
            if (events[i].events & EPOLLIN) {
                conn_list[connfd].recv_action.recv_callback(connfd);
            }
            if (events[i].events & EPOLLOUT) {
                conn_list[connfd].send_callback(connfd);
            }
        }
    }
    return 0;
}

6.2 多端口客户端源码(纯 C)

多端口客户端源码,可以实现对服务器多端口的并发连接请求,模拟产生测试流量。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_PORTS 20
#define PORT_BASE 2000
#define BUFFER_SIZE 1024

int main(){
    int sockfd[MAX_PORTS];
    struct sockaddr_in servaddr[MAX_PORTS];
    char buffer[BUFFER_SIZE];

    // 初始化每个端口的套接字和服务器地址
    for (int i = 0; i < MAX_PORTS; i++) {
        sockfd[i] = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd[i] < 0) {
            perror(“socket creation failed”);
            return 1;
        }
        servaddr[i].sin_family = AF_INET;
        servaddr[i].sin_addr.s_addr = inet_addr(“127.0.0.1”);
        servaddr[i].sin_port = htons(PORT_BASE + i);
    }

    // 连接到服务器的各个端口
    for (int i = 0; i < MAX_PORTS; i++) {
        if (connect(sockfd[i], (struct sockaddr *)&servaddr[i], sizeof(servaddr[i])) < 0) {
            perror(“connection failed”);
            close(sockfd[i]);
            return 1;
        }
        printf(“Connected to port %d\n”, PORT_BASE + i);
    }

    // 发送和接收数据示例,这里简单发送固定数据并接收回显
    for (int i = 0; i < MAX_PORTS; i++) {
        const char *send_data = “Hello, Server!”;
        send(sockfd[i], send_data, strlen(send_data), 0);
        int len = recv(sockfd[i], buffer, BUFFER_SIZE - 1, 0);
        if (len > 0) {
            buffer[len] = ‘\0’;
            printf(“Received from port %d: %s\n”, PORT_BASE + i, buffer);
        }
    }

    // 关闭所有连接
    for (int i = 0; i < MAX_PORTS; i++) {
        close(sockfd[i]);
    }

    return 0;
}

编译与运行说明

  1. 编译服务器gcc -o server server.c
  2. 编译客户端gcc -o client client.c
  3. 运行:先运行优化后的服务器 ./server,再运行客户端 ./client。注意客户端代码中服务器地址为 127.0.0.1,可根据需要修改。

七、总结

实现百万级高并发服务器的核心在于三点:Epoll机制消除了全量遍历的开销,Reactor模式支撑了异步非阻塞的事件驱动架构,系统参数调优则突破了操作系统的默认限制。这一架构体系构成了C语言高性能网络编程的基础范式,同样也是Nginx、Redis等业界标杆软件的底层引擎。

通过本文从原理到代码的完整剖析,相信你对如何使用纯C语言构建高性能服务器有了更深入的理解。实践是检验真理的唯一标准,建议你动手编译运行代码,并结合系统调优步骤,亲自体验百万连接的处理过程。如果你想深入学习更多C/C++及系统编程知识,欢迎来云栈社区交流探讨,这里汇集了大量热爱技术的开发者。




上一篇:从对话到系统:掌握Claude Projects、Skills与Artifacts构建高效工作流
下一篇:用MRXL命令行工具将Mermaid流程图转为可编辑Excel数据表
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-28 08:43 , Processed in 0.540591 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表