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

news2025/2/23 5:30:40

一、多线程并发服务器

高并发的 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.coloradmin.cn/o/2303763.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

自学Java-AI结合GUI开发一个石头迷阵的游戏

自学Java-AI结合GUI开发一个石头迷阵的游戏 准备环节1、创建石头迷阵的界面2、打乱顺序3、控制上下左右移动4、判断是否通关5、统计移动步骤&#xff0c;重启游戏6、拓展问题 准备环节 技术&#xff1a; 1、GUI界面编程 2、二维数组 3、程序流程控制 4、面向对象编程 ∙ \bulle…

Liunx(CentOS-6-x86_64)系统安装MySql(5.6.50)

一&#xff1a;安装Liunx&#xff08;CentOS-6-x86_64&#xff09; 安装Liunx&#xff08;CentOS-6-x86_64&#xff09; 二&#xff1a;下载MySql&#xff08;5.6.50&#xff09; MySql下载官网 二&#xff1a;安装MySql 2.1 将mysql上传到Liunx 文件地址 /usr/local/ 2…

Java Web开发实战与项目——开发一个在线论坛系统

在线论坛系统是一个常见的Web应用&#xff0c;通常具有用户注册、帖子发布、评论互动、消息推送等基本功能。开发这样一个系统&#xff0c;既涉及到前后端的技术栈选择&#xff0c;也需要考虑性能、扩展性等实际问题。本文将从设计论坛模块、实现消息推送与实时更新功能、以及优…

ubuntu24.04无法安装向日葵,提示依赖libgconf-2-4怎么办?

在向日葵官方下载的deb包&#xff0c;目前是SunloginClient_15.2.0.63062_amd64.deb&#xff0c;执行安装代码&#xff0c;如下&#xff1a; sudo < /span > dpkg< /span > -i< /span > SunloginClient_15< /span >.2< /span >.0< /span >…

Kubernetes 使用 Kube-Prometheus 构建指标监控 +飞书告警

1 介绍 Prometheus Operator 为 Kubernetes 提供了对 Prometheus 机器相关监控组件的本地部署和管理方案&#xff0c;该项目的目的是为了简化和自动化基于 Prometheus 的监控栈配置&#xff0c;主要包括以下几个功能&#xff1a; Kubernetes 自定义资源&#xff1a;使用 Kube…

WPF的页面设计和实用功能实现

目录 一、TextBlock和TextBox 1. 在TextBlock中实时显示当前时间 二、ListView 1.ListView显示数据 三、ComboBox 1. ComboBox和CheckBox组合实现下拉框多选 四、Button 1. 设计Button按钮的边框为圆角&#xff0c;并对指针悬停时的颜色进行设置 一、TextBlock和TextBox…

window安装MySQL5.7

1、下载MySQL5.7.24 浏览器打开&#xff1a; https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.24-winx64.zip 2、解压缩 下载下来的是一个压缩包&#xff0c;解压到你想放到的目录下面&#xff0c;我放的是“C:\MySQL” 3、配置MySQL环境变量 计算机右键 - 属性 …

数据结构:哈希表(二)

目录 一、哈希表 1、概念 二、哈希冲突 1、概念 2、冲突避免 &#xff08;1&#xff09;哈希函数设计 &#xff08;2&#xff09;负载因子调节 3、冲突解决 &#xff08;1&#xff09;闭散列 1、线性探测 2、二次探测 &#xff08;2&#xff09;开散列 4、哈希桶实…

blender笔记2

一、物体贴地 物体->变换->对齐物体 ->对齐弹窗(对齐模式&#xff1a;反方&#xff0c;相对于&#xff1a;场景原点&#xff0c;对齐&#xff1a;z)。 之后可以设置原点->原点--3d游标 二、面上有阴影 在编辑模式下操作过后&#xff0c;物体面有阴影。 数据-&g…

1.21作业

1 unserialize3 当序列化字符串中属性个数大于实际属性个数时&#xff0c;不会执行反序列化 外部如果是unserialize&#xff08;&#xff09;会调用wakeup&#xff08;&#xff09;方法&#xff0c;输出“bad request”——构造url绕过wakeup 类型&#xff1a;public class&…

【Quest开发】全身跟踪(一)

软件&#xff1a;Unity 2022.3.51f1c1、vscode、Meta XR All in One SDK V72 硬件&#xff1a;Meta Quest3 最终效果&#xff1a;能像meta的操作室沉浸场景一样根据头盔移动来推断用户姿势&#xff0c;实现走路、蹲下、手势匹配等功能 需要借助UnityMovement这个包 GitHub …

最新版本Exoplayer扩展FFmpeg音频软解码保姆级教程

ExoPlayer 是一个开源的 Android 媒体播放库&#xff0c;由 Google 开发和维护&#xff0c;用于替代 Android 系统自带的 MediaPlayer。它提供了更强大的功能、更好的性能和更高的灵活性&#xff0c;适用于各种复杂的媒体播放场景。所以被广泛用于各种播放器场景。 最近项目中…

JS:页面事件

文章目录 一、页面加载事件二、页面滚动事件三、页面尺寸事件总结 一、页面加载事件 有时候我们会把script的内容放在body前&#xff0c;这时候代码的执行在元素的加载之前&#xff0c;会导致页面元素未加载而报错 解决办法是调用Window的load加载事件&#xff0c;将所有操作放…

vue,vue3 keepalive没有效果,无法缓存页面include无效,keep-alive

keepalive没有效果&#xff0c;无法缓存页面&#xff1f; 问题大概是组件的name值不对应&#xff0c;vue2修改组件文件的name值&#xff0c;vue3保持组件文件名称和路由页面配置的name一致就可以了&#xff0c;如果vue3不想保持一致&#xff0c;必须手动在文件后面添加export..…

DeepSeek智能测试知识库助手PRO版:多格式支持+性能优化

前言 测试工程师在管理测试资产时,需要面对多种文档格式、大量文件分类及知识库的构建任务。为了解决这些问题,我们升级了 DeepSeek智能测试知识库助手,不仅支持更多文档格式,还加入了 多线程并发处理 和 可扩展格式支持,大幅提升处理性能和灵活性。 主要功能亮点: 多格…

纯手工搭建整套CI/CD流水线指南

目录 一、前言 二、环境准备 1、服务器开荒&#xff08;192.168.1.200&#xff09; 2、离线资源清单&#xff08;提前用U盘拷好&#xff09; 三、硬核安装&#xff1a;比拧螺丝还细的步骤 Step1&#xff1a;搭建GitLab&#xff08;注意&#xff01;这是只内存饕餮&#xf…

智能硬件新时代,EasyRTC开启物联音视频新纪元

在万物互联的时代浪潮中&#xff0c;智能硬件正以前所未有的速度融入我们的生活&#xff0c;从智能家居的便捷控制&#xff0c;到智能穿戴设备的健康监测&#xff0c;再到工业物联网的高效管理&#xff0c;智能硬件的应用场景不断拓展。而在这个智能硬件蓬勃发展的背后&#xf…

Rust编程语言入门教程(八)所有权 Stack vs Heap

Rust 系列 &#x1f380;Rust编程语言入门教程&#xff08;一&#xff09;安装Rust&#x1f6aa; &#x1f380;Rust编程语言入门教程&#xff08;二&#xff09;hello_world&#x1f6aa; &#x1f380;Rust编程语言入门教程&#xff08;三&#xff09; Hello Cargo&#x1f…

交易所开发:数字市场的核心动力

数字资产交易所作为连接用户与市场的核心枢纽&#xff0c;已成为推动数字经济发展的关键引擎。其开发不仅需要技术创新&#xff0c;还需兼顾用户体验、合规安全与生态构建&#xff0c;以下是交易所开发的核心要素与实践路径分析&#xff1a; 一、交易所的核心定位与技术架构…

Jmeter进阶篇(34)如何解决jmeter.save.saveservice.timestamp_format=ms报错?

问题描述 今天使用Jmeter完成压测执行,然后使用命令将jtl文件转换成html报告时,遇到了报错! 大致就是说jmeter里定义了一个jmeter.save.saveservice.timestamp_format=ms的时间格式,但是jtl文件中的时间格式不是标准的这个ms格式,导致无法正常解析。对于这个问题,有如下…