当前位置: 首页 > article >正文

【网络编程】服务器模型(二):并发服务器模型(多线程)和 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;
}
✅ 代码运行步骤
  1. 编译:(假设文件名为 tcp_server_threads.c
gcc tcp_server_threads.c -o tcp_server_threads -pthread
  1. 运行服务器
./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 模式。很多读写函数在调用过程中会发生阻塞,例如:读操作中的 readrecvrecvfrom,写操作中的 writesend,其他操作:acceptconnect

读阻塞:以 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;
}

✅ 代码运行步骤

  1. 编译(假设文件名为 epoll_server.c
gcc epoll_server.c -o epoll_server
  1. 运行服务器
./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_setO(n),遍历 pollfdO(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 服务器的首选方案! 🎯

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

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


http://www.kler.cn/a/557654.html

相关文章:

  • 【多语言生态篇四】【DeepSeek×Rust:安全内存管理实践】
  • verilog笔记
  • 【Leetcode 每日一题 - 扩展】1512. 好数对的数目
  • C语言实现的常见算法示例
  • 【算法】直接插入排序、折半插入排序、希尔排序
  • Dockerfile中volume功能作用
  • ok113i平台——更改根目录分区大小
  • 【深度学习】Pytorch的深入理解和研究
  • 跟着李沐老师学习深度学习(十二)
  • Cython学习笔记1:利用Cython加速Python运行速度
  • 算法日记25:01背包(DFS->记忆化搜索->倒叙DP->顺序DP->空间优化)
  • HDFS入门与应用开发
  • 蓝桥杯——按键
  • 从零搭建微服务项目Pro(第1-1章——Quartz实现定时任务模块)
  • 实现 INFINI Console 与 GitHub 的单点登录集成:一站式身份验证解决方案
  • 国产编辑器EverEdit - 洞察秋毫!内置文件比较功能!
  • 正确清理C盘空间
  • 【AI】常见的AI工具地址和学习资料链接
  • INDEMIND:AI视觉赋能服务机器人,“零”碰撞避障技术实现全天候安全
  • picgo-plugin-huawei插件发布