云计算百科
云计算领域专业知识百科平台

【网络编程】服务器模型(二):并发服务器模型(多线程)和 I/O 复用服务器(select / epoll)

一、多线程并发服务器

在 高并发的 TCP 服务器 中,单线程或 fork() 多进程 方式会导致 资源浪费和性能瓶颈。因此,我们可以使用 多线程 来高效处理多个客户端的连接。

承接上文中的多进程并发服务器,代码优化目标:

1.使用 pthread 实现多线程服务器
2.每个客户端连接后,服务器创建一个独立线程进行处理
3.回显(Echo)客户端发送的消息
4.支持多个客户端同时连接
5.主线程负责监听连接,子线程负责处理客户端请求

完整代码:

#include <stdio.h> // 标准输入输出
#include <stdlib.h> // exit()、malloc()、free()
#include <string.h> // 字符串操作
#include <unistd.h> // read(), write(), close()
#include <arpa/inet.h> // sockaddr_in, inet_addr()
#include <sys/socket.h> // 套接字 API
#include <netinet/in.h> // sockaddr_in 结构体
#include <pthread.h> // 线程 API

#define PORT 8080 // 服务器监听端口
#define BUFFER_SIZE 1024 // 缓冲区大小
#define MAX_CLIENTS 100 // 最大客户端连接数

// **线程处理客户端请求**
void *handle_client(void *arg) {
int client_fd = *((int *)arg);
free(arg); // 释放动态分配的内存
char buffer[BUFFER_SIZE];
int bytes_read;

printf("✅ 客户端线程启动,处理客户端 %d\\n", client_fd);

while (1) {
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
bytes_read = read(client_fd, buffer, BUFFER_SIZE);
if (bytes_read <= 0) {
printf("❌ 客户端 %d 断开连接\\n", client_fd);
break; // 退出循环,关闭连接
}

printf("📩 收到客户端 %d 消息: %s\\n", client_fd, buffer);

// **发送回显消息**
write(client_fd, buffer, bytes_read);
}

// **关闭客户端连接**
close(client_fd);
printf("关闭客户端 %d 连接\\n", client_fd);
return NULL;
}

int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
pthread_t thread_id;

// 1️⃣ 创建服务器套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 1) {
perror("❌ Socket 创建失败");
exit(EXIT_FAILURE);
}

// 2️⃣ 绑定服务器地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("❌ 绑定失败");
close(server_fd);
exit(EXIT_FAILURE);
}

// 3️⃣ 监听客户端连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("❌ 监听失败");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("⚡ 多线程 TCP 服务器已启动,监听端口 %d…\\n", PORT);

while (1) {
printf("\\n等待客户端连接…\\n");

// 4️⃣ 接受客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
perror("❌ 接受客户端连接失败");
continue; // 继续等待下一个客户端
}

printf("✅ 客户端连接成功!IP: %s, 端口: %d\\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

// 5️⃣ 创建线程处理客户端
int *new_sock = malloc(sizeof(int)); // 动态分配内存,避免线程冲突
*new_sock = client_fd;
if (pthread_create(&thread_id, NULL, handle_client, (void *)new_sock) != 0) {
perror("❌ 线程创建失败");
close(client_fd);
free(new_sock);
} else {
pthread_detach(thread_id); // 让线程自动回收
}
}

// 6️⃣ 关闭服务器(通常不会执行到这里)
close(server_fd);
return 0;
}

✅ 代码运行步骤
  • 编译:(假设文件名为 tcp_server_threads.c)
  • gcc tcp_server_threads.c -o tcp_server_threads -pthread

  • 运行服务器
  • ./tcp_server_threads

    输出示例:

    多线程 TCP 服务器已启动,监听端口 8080...
    等待客户端连接...

    ✅ 连接测试

    方式 1:使用 telnet

    telnet 127.0.0.1 8080
    # 输入消息后按 Enter,服务器会返回相同的消息。

    方式 2:使用 nc(Netcat)

    🔹 启动多个客户端

    nc 127.0.0.1 8080

    输入内容,服务器会回显,如:

    Hello Server
    Hello Server # 服务器返回相同内容

    详细步骤流程:
    1. 创建 TCP 套接字 — socket() — 创建服务器 socket
    2. 绑定 IP 和端口 — bind() — 监听 8080 端口
    3. 监听连接 — listen() — 允许最多 MAX_CLIENTS 个客户端排队
    4. 等待客户端连接 — accept() — 接受一个客户端连接
    5. 创建线程 — pthread_create() — 让每个客户端由一个线程处理
    6. 处理客户端请求 — read() — 读取客户端发送的数据
    7. 发送回显数据 — write() — 把数据发回客户端
    8. 关闭连接 — close() — 释放资源

    该代码是一个基本的 TCP 多线程并发服务器,适用于 中等并发负载, 相比 fork(),使用 pthread 可以减少资源消耗,提升并发性能。

    后续代码可优化

    1.使用线程池
    线程池可以复用线程,避免 pthread_create() 过多消耗资源。
    参考 pthread pool 机制,预创建固定数量线程,避免频繁创建销毁。
    2.使用 epoll 结合线程池
    结合 epoll 监听 accept(),减少 CPU 负担。
    3.日志管理
    服务器可以使用 syslog() 或文件写入方式记录 客户端连接信息。
    4.超时处理
    服务器可以设置 setsockopt() 限制客户端连接时间:

    struct timeval timeout = {5, 0}; // 5 秒超时
    setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

    二、I/O 复用服务器(select / poll)

    在 UNIX/Linux 下主要有4种 I/O 模型:

    阻塞I/O: 最常用、最简单、效率最低
    非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
    I/O 多路复用:允许同时对多个I/O进行控制
    信号驱动I/O: 一种异步通信模型

    阻塞I/O 模式是最普遍使用的 I/O 模式,大部分程序使用的都是阻塞模式的 I/O ;缺省情况下,套接字建立后所处于的模式就是阻塞 I/O 模式。很多读写函数在调用过程中会发生阻塞,例如:读操作中的 read、recv、recvfrom,写操作中的 write、send,其他操作:accept、connect。

    读阻塞:以 read 函数为例: 进程调用 read 函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数 read 将发生阻塞。它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过 read 访问这些数据。但如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。

    写阻塞: 在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。这时,写操作不进行任何拷贝工作,将发生阻塞。一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。

    非阻塞模式I/O: 当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

    当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。 应用程序不停的 polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。也正因如此,这种模式在使用中不普遍,太浪费资源了。 非阻塞模式I/O

    fcntl()函数
    一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。
    可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。
    int fcntl(int fd, int cmd, long arg);
    int flag;
    flag = fcntl(sockfd, F_GETFL, 0);
    flag |= O_NONBLOCK;
    fcntl(sockfd, F_SETFL, flag);

    多路复用I/O 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的。可是,若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;比较好的方法是使用I/O多路复用。其基本思想是:

    先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行 I/O时函数才返回。 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

    在 高并发的 TCP 服务器 中,传统的 fork() 多进程 或 pthread 多线程 方式容易导致 资源浪费和性能瓶颈。因此,我们才使用 I/O 复用技术(select / poll / epoll),使 单线程 就能监听 多个客户端连接,从而提高并发性能。

    多路复用select/poll
    /* According to POSIX.1-2001, POSIX.1-2008 */
    #include <sys/select.h>

    /* According to earlier standards */
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

    int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    /**********************************************************************
    @brief: 多路复用,将所需要使用的或者需要关注的文件描述符放在一个集合中,当集合中的文件描述符
    被触发了会去执行相应的任务

    @nfds: 最大文件描述符 + 1

    @readfds: 所有要读的文件文件描述符的集合

    @writefds: 所有要的写文件文件描述符的集合

    @exceptfds:其他要向我们通知的文件描述符

    @timeout: 超时设置.
    NULL:一直阻塞,直到有文件描述符就绪或出错
    时间值为0:仅仅检测文件描述符集的状态,然后立即返回
    时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

    struct timeval {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
    };

    @retval: 成功:返回就绪的文件描述符的个数
    失败:返回-1,并且设置全局错误码

    为了设置文件描述符我们要使用几个宏:
    宏的形式:
    void FD_ZERO(fd_set *fdset) //从fdset中清除所有的文件描述符
    void FD_SET(int fd,fd_set *fdset) //将fd加入到fdset
    void FD_CLR(int fd,fd_set *fdset) //将fd从fdset里面清除
    int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fdset集合中
    **********************************************************************/

    #include <poll.h>

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    /**********************************************************************
    @brief: 多路复用,监管文件描述符

    @fds: 要监管的文件描述符的结构体指针

    struct pollfd {
    int fd; /* file descriptor 希望被触发的文件描述符 用户赋值*/
    short events; /* requested events 希望被触发的事件POLLIN 用户赋值*/
    short revents; /* returned events 希望被触发的事件发生与否POLLIN 系统赋值*/
    };

    The bits that may be set/returned in events and revents are defined in <poll.h>:
    //可在man手册中查询别的events和revents的选值
    POLLIN There is data to read.

    POLLPRI
    There is some exceptional condition on the file descriptor. Possibilities
    include:

    * There is out-of-band data on a TCP socket (see tcp(7)).

    * A pseudoterminal master in packet mode has seen a state change on the
    slave (see ioctl_tty(2)).

    * A cgroup.events file has been modified (see cgroups(7)).

    POLLOUT
    Writing is now possible, though a write larger that the available space in a
    socket or pipe will still block (unless O_NONBLOCK is set).

    /**
    @nfds: 最大文件描述符 + 1

    @timeout: >0:阻塞对应的时间(毫秒级)
    =0:不阻塞
    <0:一直阻塞

    @retval: >0:集合中已就绪的文件描述符个数
    =0:集合中没有已就绪的文件描述符
    -1:poll调用失败,并且设置全局错误码
    **********************************************************************/

    📌 I/O 复用的三种方式

    方法特点适用场景
    select() 需要遍历整个文件描述符集合,最大支持 1024 个连接 适用于 少量连接 的情况
    poll() 使用链表存储,支持更多连接,但仍然需要遍历整个集合 适用于 中等并发
    epoll() 事件驱动,只处理活跃的连接,性能远高于 select/poll 适用于 高并发服务器

    请添加图片描述

    select() 多路复用服务器

    代码实现:

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

    #define PORT 8080
    #define MAX_CLIENTS 100
    #define BUFFER_SIZE 1024

    int main() {
    int server_fd, client_fd, max_fd, activity, i;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    fd_set read_fds, master_fds;

    // 1️⃣ 创建 TCP 套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 1) {
    perror("❌ Socket 创建失败");
    exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定服务器地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    perror("❌ 绑定失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    // 3️⃣ 监听客户端连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
    perror("❌ 监听失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    printf("`select()` 多路复用服务器已启动,监听端口 %d…\\n", PORT);

    // 4️⃣ 初始化 `select` 的文件描述符集合
    FD_ZERO(&master_fds);
    FD_SET(server_fd, &master_fds);
    max_fd = server_fd;

    while (1) {
    read_fds = master_fds; // 每次循环都复制 `master_fds`

    // 5️⃣ 监听多个文件描述符 `select`
    activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
    if (activity < 0) {
    perror("❌ `select` 调用失败");
    continue;
    }

    // 6️⃣ 处理新客户端连接
    if (FD_ISSET(server_fd, &read_fds)) {
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
    perror("❌ 客户端连接失败");
    continue;
    }

    printf("新客户端连接:%s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    FD_SET(client_fd, &master_fds);
    if (client_fd > max_fd) {
    max_fd = client_fd;
    }
    }

    // 7️⃣ 处理已连接的客户端数据
    for (i = server_fd + 1; i <= max_fd; i++) {
    if (FD_ISSET(i, &read_fds)) {
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_read = read(i, buffer, BUFFER_SIZE);
    if (bytes_read <= 0) {
    printf("❌ 客户端断开连接\\n");
    close(i);
    FD_CLR(i, &master_fds);
    } else {
    printf("📩 客户端消息: %s\\n", buffer);
    write(i, buffer, bytes_read); // 回显
    }
    }
    }
    }

    close(server_fd);
    return 0;
    }

    poll() 多路复用服务器

    #include <poll.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>

    #define PORT 8080
    #define MAX_CLIENTS 100
    #define BUFFER_SIZE 1024

    int main() {
    int server_fd, client_fd, i;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    struct pollfd fds[MAX_CLIENTS];
    int nfds = 1;

    // 1️⃣ 创建 TCP 套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 1) {
    perror("❌ Socket 创建失败");
    exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定服务器地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    perror("❌ 绑定失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    // 3️⃣ 监听客户端连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
    perror("❌ 监听失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    printf("`poll()` 多路复用服务器已启动,监听端口 %d…\\n", PORT);

    fds[0].fd = server_fd;
    fds[0].events = POLLIN;

    while (1) {
    // 4️⃣ 监听多个文件描述符 `poll`
    int activity = poll(fds, nfds, 1);
    if (activity < 0) {
    perror("❌ `poll` 调用失败");
    continue;
    }

    // 5️⃣ 处理新客户端连接
    if (fds[0].revents & POLLIN) {
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
    perror("❌ 客户端连接失败");
    continue;
    }
    printf("新客户端连接:%s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    fds[nfds].fd = client_fd;
    fds[nfds].events = POLLIN;
    nfds++;
    }

    // 6️⃣ 处理已连接的客户端数据
    for (i = 1; i < nfds; i++) {
    if (fds[i].revents & POLLIN) {
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_read = read(fds[i].fd, buffer, BUFFER_SIZE);
    if (bytes_read <= 0) {
    printf("❌ 客户端断开连接\\n");
    close(fds[i].fd);
    fds[i] = fds[nfds 1]; // 移除断开的客户端
    nfds;
    } else {
    printf("📩 客户端消息: %s\\n", buffer);
    write(fds[i].fd, buffer, bytes_read); // 回显
    }
    }
    }
    }

    close(server_fd);
    return 0;
    }

    使用 epoll 的高并发服务器(C 语言)

    select() 和 poll() 适用于 1000 以内的连接,但随着连接数增加,性能下降。对于高并发服务器,建议使用 epoll()(Linux) 或 kqueue()(BSD/macOS)。

    epoll 是 Linux 下 高效的 I/O 复用方式,相比 select() 和 poll(),它支持:

    • O(1) 事件触发:只处理活跃的文件描述符,不用遍历整个 fd_set。
    • 支持大规模并发:适用于 上万级别的连接,比 select() / poll() 性能高很多。
    • Edge Trigger (ET) & Level Trigger (LT):支持 边缘触发 和 水平触发,进一步优化性能。
    epoll 关键 API

    函数功能
    epoll_create1(0)创建 epoll 实例,返回 epoll_fd
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event)添加监听的 fd
    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event)修改监听的 fd
    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, &event)删除监听的 fd
    epoll_wait(epoll_fd, events, MAX_EVENTS, timeout)等待事件触发

    epoll 多路复用服务器(代码实现)(重点)

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    #include <fcntl.h>

    #define PORT 8080 // 服务器监听端口
    #define MAX_EVENTS 1000 // epoll 最大监听事件数
    #define BUFFER_SIZE 1024 // 缓冲区大小

    // **🔹 设置 fd 为非阻塞模式**
    void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    }

    int main() {
    int server_fd, client_fd, epoll_fd, event_count, i;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    struct epoll_event event, events[MAX_EVENTS];
    char buffer[BUFFER_SIZE];

    // 1️⃣ 创建 TCP 套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 1) {
    perror("❌ Socket 创建失败");
    exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定服务器地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    perror("❌ 绑定失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    // 3️⃣ 开始监听
    if (listen(server_fd, MAX_EVENTS) < 0) {
    perror("❌ 监听失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }
    printf("⚡ `epoll` 服务器启动,监听端口 %d…\\n", PORT);

    // 4️⃣ 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == 1) {
    perror("❌ epoll_create1 失败");
    close(server_fd);
    exit(EXIT_FAILURE);
    }

    // 5️⃣ 设置 server_fd 为非阻塞模式,并添加到 epoll 监听
    set_nonblocking(server_fd);
    event.events = EPOLLIN; // 监听可读事件(LT 模式)
    event.data.fd = server_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);

    while (1) {
    // 6️⃣ 等待事件触发
    event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 1);
    for (i = 0; i < event_count; i++) {
    if (events[i].data.fd == server_fd) {
    // 7️⃣ 处理新客户端连接
    client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
    perror("❌ 接受客户端连接失败");
    continue;
    }
    printf("✅ 新客户端连接:%s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    set_nonblocking(client_fd); // 设置非阻塞模式
    event.events = EPOLLIN | EPOLLET; // 监听可读事件,ET 模式
    event.data.fd = client_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
    } else {
    // 8️⃣ 处理客户端数据
    int client_fd = events[i].data.fd;
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_read = read(client_fd, buffer, BUFFER_SIZE);
    if (bytes_read <= 0) {
    printf("❌ 客户端断开连接\\n");
    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
    close(client_fd);
    } else {
    printf("📩 客户端消息: %s\\n", buffer);
    write(client_fd, buffer, bytes_read); // 回显
    }
    }
    }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
    }

    ✅ 代码运行步骤

  • 编译(假设文件名为 epoll_server.c)
  • gcc epoll_server.c -o epoll_server

  • 运行服务器
  • ./epoll_server

    输出示例:

    `epoll` 服务器启动,监听端口 8080...

    ✅ 连接测试

    📌 使用 nc(Netcat)

    nc 127.0.0.1 8080

    输入内容,服务器会回显,如:

    Hello Server
    Hello Server # 服务器返回相同内容

    epoll 工作模式

    🔹 水平触发(LT,Level Trigger)

    • 默认模式,事件未处理时会 持续触发。
    • 适用于阻塞 I/O,确保数据不会丢失。

    🔹 边缘触发(ET,Edge Trigger)

    • 仅在状态变化时触发,不会重复触发。
    • 必须使用非阻塞 I/O,否则可能丢失数据。
    ✅ epoll vs select / poll
    特点select()poll()epoll()
    最大连接数 1024(Linux 默认) 无限制(但扫描所有) 无限制(事件驱动)
    性能 O(n),遍历 fd_set O(n),遍历 pollfd O(1),只处理活跃连接
    适用场景 少量连接(<1000) 中等连接数 高并发(>10000)

    代码可优化

    使用线程池
    epoll_wait() 只负责监听,线程池 处理数据,提高吞吐量。
    使用 EPOLLET(边缘触发)
    结合 非阻塞 read(),减少 epoll_wait() 触发次数,提高效率。
    TCP SO_REUSEADDR
    避免服务器重启时 bind() 失败:

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    该代码是一个高效的 TCP 并发服务器,使用 epoll 事件驱动,适用于大规模连接。 相比 select(),epoll 在高并发情况下性能更好,是 Linux 服务器的首选方案! 🎯

    以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

    我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。 感谢!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【网络编程】服务器模型(二):并发服务器模型(多线程)和 I/O 复用服务器(select / epoll)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!