Linux:TCP并发服务器实现

目录

1.并发处理的必要性

2.Linux系统的四种IO模型

2.1四种基础IO模型

2.2示例

3.IO多路复用核心机制

[3.1. select:最早的多路复用接口](#3.1. select:最早的多路复用接口)

[3.2. poll:select的改进版](#3.2. poll:select的改进版)

[3.3. epoll:Linux的高性能解决方案](#3.3. epoll:Linux的高性能解决方案)

4.相关函数接口

[4.1.1.select API详解](#4.1.1.select API详解)

4.1.2示例

[4.2.poll API详解(- select的改进版)](#4.2.poll API详解(- select的改进版))

4.2.2示例

[4.3.1. epoll API详解](#4.3.1. epoll API详解)

4.3.2示例

5、三种机制对比总结

6.总结


在构建高性能网络服务器时,一个核心挑战是如何高效地处理大量并发连接 。传统的"一线程/进程一连接"模型在连接数激增时会迅速耗尽系统资源。此时,IO多路复用技术便成为解决这一难题的核心技术之一。它允许单个线程/进程同时监控多个IO描述符(如socket),极大地提升了资源利用率和程序性能。

1.并发处理的必要性

在深入技术细节前,先明确几个基本概念:

  • 并发:指服务器能够同时处理多个客户端请求的能力。就像一家餐厅需要同时服务多桌客人一样,服务器也需要同时应对多个连接。

  • 协议差异

    • UDP:无连接协议,每个数据报独立处理,天然无连接状态,无需专门的并发机制

    • TCP:面向连接协议,每个连接都是长期状态,必须实现并发处理才能服务多个客户端

  • 传统并发方案的局限

    早期常用多进程/多线程模型(每个连接创建一个线程/进程),虽实现简单但存在明显缺点:

    • 创建/销毁开销大(比如创建一个线程要8M栈区,最多可创建三四百个线程)

    • 上下文切换成本高

    • 内存消耗随连接数线性增长

    • 受操作系统线程数限制

  • 多线程实现TCP并发服务器测试示例如下:

cpp 复制代码
#服务端
#include "head.h"

int CreateListenSocket(const char *pip, int port)
{
    int ret = 0;
    int sockfd = 0;
    struct sockaddr_in seraddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(port);
    seraddr.sin_addr.s_addr = inet_addr(pip);
    ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
        perror("fail to listen");
        return -1;
    }

    return sockfd;
}

void *threadfun(void *arg)
{
    char tmpbuff[4096] = {0};
    ssize_t nret = 0;
    int confd = (int)arg;

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to recv");
            return NULL;
        }
        else if (0 == nret)
        {
            printf("关闭连接\n");
            break;
        }

        printf("RECV:%s\n", tmpbuff);

        sprintf(tmpbuff, "%s --- echo", tmpbuff);
        nret = send(confd, tmpbuff, strlen(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to send");
            return NULL;
        }
    }
        
    close(confd);
    
    return NULL;
}

int main(void)
{
    int sockfd = 0;
    int confd = 0;
    pthread_t tid;
    pthread_attr_t attr;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    sockfd = CreateListenSocket("192.168.0.118", 50000);

    while (1)
    {
        confd = accept(sockfd, NULL, NULL);
        if (-1 == confd)
        {
            perror("fail to accept");
            return -1;
        }

        pthread_create(&tid, &attr, threadfun, (void *)confd);
    }

    close(sockfd);
    pthread_attr_destroy(&attr);

    return 0;
}
cpp 复制代码
#客户端代码
#include "head.h"

int main(void)
{
    int sockfd = 0;
    int ret = 0;
    struct sockaddr_in seraddr;
    char tmpbuff[4096] = {0};
    int cnt = 0;
    ssize_t nret = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.118");
    ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to connect");
        return -1;
    }

    while (1)
    {
        sprintf(tmpbuff, "hello world ---------- %d", cnt);
        cnt++;
        nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to send");
            return -1;
        }

        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to recv");
            return -1;
        }
        else if (0 == nret)
        {
            printf("关闭连接\n");
            break;
        }
        printf("RECV:%s\n", tmpbuff);
    }
    
    close(sockfd);

    return 0;
}

2.Linux系统的四种IO模型

2.1四种基础IO模型

在讨论多路复用前,先了解Linux提供的四种基础IO模型:

  1. 阻塞IO(block):进程发起IO请求后一直等待,直到数据就绪

  2. 非阻塞IO(nonblock):应用层向内核层读数据,不管有没有数据都返回相应的结果,不会阻塞等待,所以得通过轮询检查状态

  3. 异步信号驱动IO(async):内核在数据就绪时发送信号通知进程(类似中断)

  4. 多路复用IO:内核完成整个IO操作后通知进程(由一个函数接口监听多个连接,类似于ADC,服务器都用它)

前三种都只能处理一对一,只有多路复用IO可以处理一对多,它属于前两种模型的增强,它让单个进程能同时等待多个文件描述符的就绪事件。

2.2示例

阻塞IO

cpp 复制代码
//客户端
#include "head.h"

int main(void)
{
    int fd = 0;
    char tmpbuff[4096] = {0};

    mkfifo("/tmp/myfifo", 0664);
    
    fd = open("/tmp/myfifo", O_WRONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        gets(tmpbuff);
        write(fd, tmpbuff, strlen(tmpbuff));
    }
    
    close(fd);
    
    return 0;
cpp 复制代码
//服务端
#include "head.h"

int main(void)
{
    int fd = 0;
    char tmpbuff[4096] = {0};

    mkfifo("/tmp/myfifo", 0664);

    fd = open("/tmp/myfifo", O_RDONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        read(fd, tmpbuff, sizeof(tmpbuff)); //阻塞等待
        printf("FIFO:%s\n", tmpbuff);

        memset(tmpbuff, 0, sizeof(tmpbuff));
        gets(tmpbuff); //阻塞等待
        printf("STDIN:%s\n", tmpbuff);
    }

    close(fd);

    return 0;
}

非阻塞IO

cpp 复制代码
//服务端
#include "head.h"

int main(void)
{
    int fd = 0;
    int flags = 0;
    char tmpbuff[4096] = {0};
    ssize_t nret = 0;
    char *pret = NULL;

    mkfifo("/tmp/myfifo", 0664);

    fd = open("/tmp/myfifo", O_RDONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }

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

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

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = read(fd, tmpbuff, sizeof(tmpbuff));
        if (nret > 0)
        {
            printf("FIFO:%s\n", tmpbuff);
        }

        memset(tmpbuff, 0, sizeof(tmpbuff));
        pret = gets(tmpbuff);
        if (pret != NULL)
        {
            printf("STDIN:%s\n", tmpbuff);
        }
    }

    close(fd);

    return 0;
}

异步信号驱动IO

cpp 复制代码
//服务端
#include "head.h"

int fd = 0;

void handler(int signo)
{
    char tmpbuff[4096] = {0};

    memset(tmpbuff, 0, sizeof(tmpbuff));
    read(fd, tmpbuff, sizeof(tmpbuff));
    printf("FIFO:%s\n", tmpbuff);

    return;
}

int main(void)
{
    int flags = 0;
    char tmpbuff[4096] = {0};
    ssize_t nret = 0;
    char *pret = NULL;

    signal(SIGIO, handler);

    mkfifo("/tmp/myfifo", 0664);

    fd = open("/tmp/myfifo", O_RDONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }

    flags = fcntl(fd, F_GETFL);
    flags |= O_ASYNC;
    //设置fd为异步IO(当有IO事件发生时,内核会发送SIGIO异步信号通知应用层)
    fcntl(fd, F_SETFL, flags);
    //设置fd对应的异步IO信号由当前进程接收
    fcntl(fd, F_SETOWN, getpid());

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        gets(tmpbuff);
        printf("STDIN:%s\n", tmpbuff);
    }

    close(fd);

    return 0;
}

3.IO多路复用核心机制

IO多路复用的核心思想是:通过一个函数接口同时监听多个文件描述符的IO事件,当任一描述符就绪时,函数则不再阻塞,程序即可处理对应事件。------类似中断处理

Linux提供了三种主要实现,各有特点:

select

poll

apoll

3.1. select:最早的多路复用接口

工作原理:创建文件描述符集合,把所有需要监听的文件描述符放入,用一个函数来监听,只要有一个事件发生,就不阻塞,把产生事件的文件描述符留在集合中,没有产生的则踢出去(补充:文件描述符的规律--新文件描述符总选择最小的可用的非负整数。系统中预定义的标准文件描述符:0为stdin,1为stdout,2为stderr)

复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, 
           fd_set *exceptfds, struct timeval *timeout);

select通过三个位图集合(读、写、异常)管理文件描述符,监控其状态变化。

四大局限性

  1. 数量限制 :监听的描述符数有上限(通常1024),由FD_SETSIZE宏定义

  2. 性能开销 :select监听的文件描述符集合在应用层,当事件发生需要产生一次内核层向应用层数据的拷贝,会产生资源开销

  3. 触发模式单一 :仅支持水平触发模式(Level Triggered)(低速模式),即只要可读/写就不断通知

  4. 效率问题 :每次调用后需遍历整个集合找出就绪的描述符,时间复杂度O(n)

3.2. poll:select的改进版

工作原理

复制代码
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll使用pollfd结构体数组替代select的位图,消除了描述符数量限制。

相对于select的改进

  • ✅ 描述符数量无硬性上限

  • ❌ 仍需每次调用时传递整个描述符数组

  • ❌ 仍需遍历所有描述符查找就绪事件

  • ❌ 同样只支持水平触发模式

3.3. epoll:Linux的高性能解决方案

设计优势

  1. 无数量限制:仅受系统最大文件描述符数限制

  2. 高效内核数据结构:在内核维护事件表,避免用户-内核空间的数据拷贝

  3. 双触发模式:支持水平触发(默认)和边沿触发(通过EPOLLET标志设置)

  4. 精准事件返回 :只返回就绪的描述符,无需遍历整个集合

4.相关函数接口

4.1.1.select API详解

fcntl - 文件描述符控制

fcntl函数用于对已打开的文件描述符进行各种控制操作,在多路复用中常用于设置非阻塞模式。

复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

功能

向文件描述符发送cmd控制命令,可获取或设置文件描述符的属性。

参数说明

  • fd:要操作的文件描述符

  • cmd:控制命令,常用值包括:

    • F_GETFL:获取文件描述符的状态标志

    • F_SETFL:设置文件描述符的状态标志

  • arg:可变参数,根据cmd的不同而不同

常用操作示例

复制代码
// 获取文件描述符当前标志
int flags = fcntl(fd, F_GETFL, 0);

// 设置为非阻塞模式
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 取消非阻塞模式
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);

返回值

  • 成功:根据cmd不同返回不同值

    • F_GETFL:返回文件状态标志

    • F_SETFL:返回0

  • 失败:返回-1,并设置errno

select - 传统多路复用接口

select是最早的IO多路复用实现,可同时监控多个文件描述符的读、写和异常事件。

复制代码
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

功能

监听指定文件描述符集合中是否有事件发生(可读、可写或异常)。

参数说明

  • nfds :最大文件描述符值加1,即max(fd1, fd2, ...) + 1

  • readfds:读文件描述符集合(传入要监听的可读fd,返回可读的fd)

  • writefds:写文件描述符集合(传入要监听的可写fd,返回可写的fd)

  • exceptfds:异常文件描述符集合(传入要监听的异常fd,返回异常的fd)

  • timeout:超时时间

    • NULL:一直阻塞,直到有事件发生

    • 0:立即返回,不阻塞

    • 具体值:等待指定时间

  • 返回值

成功返回产生事件的文件描述符个数

失败返回-1

如果超时仍然没有事件发生返回0

辅助函数

复制代码
void FD_ZERO(fd_set *set);           // 清空文件描述符集合
void FD_SET(int fd, fd_set *set);    // 将fd加入集合
void FD_CLR(int fd, fd_set *set);    // 从集合中删除fd
int  FD_ISSET(int fd, fd_set *set);  // 判断fd是否在集合中

基本使用流程

复制代码
fd_set readfds;
struct timeval timeout;
int max_fd = 0;

// 1. 清空集合
FD_ZERO(&readfds);

// 2. 添加要监控的文件描述符
FD_SET(sock_fd1, &readfds);
FD_SET(sock_fd2, &readfds);
max_fd = (sock_fd1 > sock_fd2) ? sock_fd1 : sock_fd2;

// 3. 设置超时(可选)
timeout.tv_sec = 5;  // 5秒
timeout.tv_usec = 0;

// 4. 调用select
int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout);

// 5. 检查哪些描述符就绪
if (FD_ISSET(sock_fd1, &readfds)) {
    // sock_fd1可读
}

返回值

  • 成功:返回就绪的文件描述符总数

  • 超时:返回0

  • 错误:返回-1,并设置errno

注意事项

  1. select返回后,未就绪的描述符会被从集合中清除

  2. 每次调用前需要重新设置描述符集合

  3. 描述符集合大小受FD_SETSIZE限制(通常1024)

4.1.2示例

cpp 复制代码
//客户端
#include "head.h"

int main(void)
{
    int sockfd = 0;
    int ret = 0;
    struct sockaddr_in seraddr;
    char tmpbuff[4096] = {0};
    int cnt = 0;
    ssize_t nret = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.118");
    ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to connect");
        return -1;
    }

    while (1)
    {
        sprintf(tmpbuff, "hello world ---------- %d", cnt);
        cnt++;
        nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to send");
            return -1;
        }

        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to recv");
            return -1;
        }
        else if (0 == nret)
        {
            printf("关闭连接\n");
            break;
        }
        printf("RECV:%s\n", tmpbuff);
    }
    
    close(sockfd);

    return 0;
}
cpp 复制代码
//服务端
#include "head.h"

int CreateListenSocket(const char *pip, int port)
{
    int ret = 0;
    int sockfd = 0;
    struct sockaddr_in seraddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(port);
    seraddr.sin_addr.s_addr = inet_addr(pip);
    ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
        perror("fail to listen");
        return -1;
    }

    return sockfd;
}

int main(void)
{
    int sockfd = 0;
    int confd = 0;
    ssize_t nret = 0;
    char tmpbuff[4096] = {0};
    fd_set rdfds;
    fd_set tmpfds;
    int maxfd = 0;
    int ret = 0;
    int i = 0;

    sockfd = CreateListenSocket("192.168.0.118", 50000);
    
    FD_ZERO(&rdfds);
    FD_SET(sockfd, &rdfds);
    maxfd = sockfd;

    while (1)
    {
        tmpfds = rdfds;
        ret = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
        if (-1 == ret)
        {
            perror("fail to select");
            return -1;
        }

        if (FD_ISSET(sockfd, &tmpfds))
        {
            confd = accept(sockfd, NULL, NULL);
            if (-1 == confd)
            {
                perror("fail to accept");
                FD_CLR(sockfd, &rdfds);
                close(sockfd);
                continue;
            }

            FD_SET(confd, &rdfds);
            maxfd = maxfd > confd ? maxfd : confd;
        }

        for (i = 4; i <= maxfd; i++)
        {
           if (FD_ISSET(i, &tmpfds))
           {
                memset(tmpbuff, 0, sizeof(tmpbuff));
                nret = recv(i, tmpbuff, sizeof(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to recv");
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
                else if (0 == nret)
                {
                    printf("关闭连接\n");
                    FD_CLR(i, &rdfds);
                    close(i);
                    break;
                }

                printf("RECV:%s\n", tmpbuff);

                sprintf(tmpbuff, "%s --- echo", tmpbuff);
                nret = send(i, tmpbuff, strlen(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to send");
                    FD_CLR(i, &rdfds);
                    close(i);
                    continue;
                }
           } 
        }
    }

    close(sockfd);

    return 0;
}

4.2.poll API详解(- select的改进版)

poll解决了select的文件描述符数量限制问题,使用结构体数组替代位图。

复制代码
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能

监听事件集合中的事件是否发生。

参数说明

  • fds:pollfd结构体数组的首地址

  • nfds:数组元素个数

  • timeout:超时时间(毫秒)

    • -1:一直阻塞,直到事件发生

    • 0:立即返回

    • >0:等待指定毫秒数

  • 返回值:

失败返回-1

成功返回产生事件的个数

时间达到没有事件发生返回0

pollfd结构体

复制代码
struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 监听的事件 */
    short revents;    /* 实际发生的事件(由内核填充) */
};

常用事件标志

  • POLLIN:有数据可读

  • POLLPRI:有紧急数据可读

  • POLLOUT:可写

  • POLLERR:错误

  • POLLHUP:挂起

  • POLLNVAL:描述符未打开

使用示例

复制代码
struct pollfd fds[2];

// 设置要监听的描述符
fds[0].fd = sock_fd1;
fds[0].events = POLLIN;

fds[1].fd = sock_fd2;
fds[1].events = POLLIN | POLLOUT;

// 调用poll
int ret = poll(fds, 2, 5000);  // 等待5秒

// 检查结果
if (fds[0].revents & POLLIN) {
    // sock_fd1可读
}

返回值

  • 成功:返回就绪的描述符个数

  • 超时:返回0

  • 错误:返回-1,并设置errno

4.2.2示例

cpp 复制代码
//服务端
#include "head.h"

int CreateListenSocket(const char *pip, int port)
{
    int ret = 0;
    int sockfd = 0;
    struct sockaddr_in seraddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(port);
    seraddr.sin_addr.s_addr = inet_addr(pip);
    ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
        perror("fail to listen");
        return -1;
    }

    return sockfd;
}

void InitPollFds(struct pollfd *pfds, int maxlen)
{
    int i = 0;

    for (i = 0; i < maxlen; i++)
    {
        pfds[i].fd = -1;
    }

    return;
}

int AddEvent(struct pollfd *pfds, int maxlen, int fd, short event)
{
    int i = 0;

    for (i = 0; i < maxlen; i++)
    {
        if (-1 == pfds[i].fd)
        {
            pfds[i].fd = fd;
            pfds[i].events = event;
            break;
        }
    }

    return 0;
}

int DelEvent(struct pollfd *pfds, int maxlen, int fd)
{
    int i = 0;

    for (i = 0; i < maxlen; i++)
    {
        if (fd == pfds[i].fd)
        {
            pfds[i].fd = -1;
            break;
        }
    }

    return 0;
}

int main(void)
{
    int sockfd = 0;
    int confd = 0;
    int nready = 0;
    int i = 0;
    ssize_t nret = 0;
    char tmpbuff[4096] = {0};
    struct pollfd fds[1024];

    sockfd = CreateListenSocket("192.168.0.165", 50000);

    InitPollFds(fds, 1024);
    AddEvent(fds, 1024, sockfd, POLLIN);

    while (1)
    {
        nready = poll(fds, 1024, -1);
        if (-1 == nready)
        {
            perror("fail to poll");
            return -1;
        }

        if (fds[0].revents & POLLIN)
        {
            confd = accept(sockfd, NULL, NULL);
            if (-1 == confd)
            {
                perror("fail to accept");
                DelEvent(fds, 1024, sockfd);
                close(sockfd);
                continue;
            }

            AddEvent(fds, 1024, confd, POLLIN);
        }

        for (i = 1; i < 1024; i++)
        {
            if (-1 == fds[i].fd)
            {
                continue;
            }

            if (fds[i].revents & POLLIN)
            {
                memset(tmpbuff, 0, sizeof(tmpbuff));
                nret = recv(fds[i].fd, tmpbuff, sizeof(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to recv");
                    DelEvent(fds, 1024, fds[i].fd);
                    close(fds[i].fd);
                    continue;
                }
                else if (0 == nret)
                {
                    printf("关闭连接\n");
                    DelEvent(fds, 1024, fds[i].fd);
                    close(fds[i].fd);
                    continue;
                }

                printf("RECV:%s\n", tmpbuff);

                sprintf(tmpbuff, "%s --- echo", tmpbuff);
                nret = send(fds[i].fd, tmpbuff, strlen(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to send");
                    DelEvent(fds, 1024, fds[i].fd);
                    close(fds[i].fd);
                    continue;
                }
            }
        }        
    }

    close(sockfd);

    return 0;
}
cpp 复制代码
//客户端
#include "head.h"

int main(void)
{
    int sockfd = 0;
    int ret = 0;
    struct sockaddr_in seraddr;
    char tmpbuff[4096] = {0};
    int cnt = 0;
    ssize_t nret = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.165");
    ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to connect");
        return -1;
    }

    while (1)
    {
        sprintf(tmpbuff, "hello world ---------- %d", cnt);
        cnt++;
        nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to send");
            return -1;
        }

        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to recv");
            return -1;
        }
        else if (0 == nret)
        {
            printf("关闭连接\n");
            break;
        }
        printf("RECV:%s\n", tmpbuff);
    }
    
    close(sockfd);

    return 0;
}

4.3.1. epoll API详解

epoll通过三个核心系统调用提供完整功能:

epoll_create - 创建事件表(epoll 集合)(内核维护的二叉树结构)

复制代码
int epoll_create(int size);

功能:创建一张内核监听的事件表(本质是红黑树+就绪链表)

参数

  • size:建议的事件表初始大小(Linux 2.6.8后忽略,但需大于0)

    返回值

  • 成功:epoll文件描述符(epfd)

  • 失败:-1

epoll_ctl - 管理监控事件(添加、修改、删除事件)

复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:向事件表添加、修改或删除监控事件

参数

  • epfd:epoll_create返回的描述符

  • op:操作类型

    • EPOLL_CTL_ADD:添加事件

    • EPOLL_CTL_MOD:修改事件

    • EPOLL_CTL_DEL:删除事件

  • fd:要操作的目标文件描述符

  • event:事件配置结构体指针

    返回值

  • 成功:0

  • 失败:-1

epoll_wait - 等待事件就绪

复制代码
int epoll_wait(int epfd, struct epoll_event *events, 
               int maxevents, int timeout);

功能:阻塞等待监听事件表中的事件发生

参数

  • epfd:epoll描述符

  • events:输出参数,存放就绪事件数组

  • maxevents:events数组的最大容量

  • timeout:超时时间(毫秒),-1表示无限等待

    返回值

  • 成功:就绪事件数量(产生事件的文件描述符个数)

  • 超时:0(时间达到没有事件产生)

  • 错误:-1

4.3.2示例

cpp 复制代码
//服务端
#include "head.h"

int CreateListenSocket(const char *pip, int port)
{
    int ret = 0;
    int sockfd = 0;
    struct sockaddr_in seraddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(port);
    seraddr.sin_addr.s_addr = inet_addr(pip);
    ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret = listen(sockfd, 10);
    if (-1 == ret)
    {
        perror("fail to listen");
        return -1;
    }

    return sockfd;
}

int main(void)
{
    int sockfd = 0;
    int confd = 0;
    int epfd = 0;
    int ret = 0;
    int nready = 0;
    int i = 0;
    ssize_t nret = 0;
    char tmpbuff[4096] = {0};
    struct epoll_event env;
    struct epoll_event retenv[1024];

    epfd = epoll_create(1024);
    if (-1 == epfd)
    {
        perror("fail to epoll_create");
        return -1;
    }

    sockfd = CreateListenSocket("192.168.0.177", 50000);

    env.events = EPOLLIN;
    env.data.fd = sockfd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &env);
    if (-1 == ret)
    {
        perror("fail to epoll_ctl");
        return -1;
    }

    while (1)
    {
        nready = epoll_wait(epfd, retenv, 1024, -1);
        if (-1 == nready)
        {
            perror("fail to epoll_wait");
            return -1;
        }

        for (i = 0; i < nready; i++)
        {
            if (retenv[i].data.fd == sockfd)
            {
                confd = accept(sockfd, NULL, NULL);
                if (-1 == confd)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
                    close(sockfd);
                    continue;
                }

                env.events = EPOLLIN;
                env.data.fd = confd;
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, confd, &env);
                if (-1 == ret)
                {
                    perror("fail to epoll_ctl");
                    return -1;
                }
            }
            else
            {
                memset(tmpbuff, 0, sizeof(tmpbuff));
                nret = recv(retenv[i].data.fd, tmpbuff, sizeof(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to recv");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
                    close(retenv[i].data.fd);
                    continue;
                }
                else if (0 == nret)
                {
                    printf("关闭连接\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
                    close(retenv[i].data.fd);
                    continue;
                }

                printf("RECV:%s\n", tmpbuff);

                sprintf(tmpbuff, "%s --- echo", tmpbuff);
                nret = send(retenv[i].data.fd, tmpbuff, strlen(tmpbuff), 0);
                if (-1 == nret)
                {
                    perror("fail to send");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, retenv[i].data.fd, NULL);
                    close(retenv[i].data.fd);
                    continue;
                }
            }
        }
    }

    close(sockfd);

    return 0;
}
cpp 复制代码
//客户端
#include "head.h"

int main(void)
{
    int sockfd = 0;
    int ret = 0;
    struct sockaddr_in seraddr;
    char tmpbuff[4096] = {0};
    int cnt = 0;
    ssize_t nret = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("192.168.0.177");
    ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if (-1 == ret)
    {
        perror("fail to connect");
        return -1;
    }

    while (1)
    {
        sprintf(tmpbuff, "hello world ---------- %d", cnt);
        cnt++;
        nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to send");
            return -1;
        }

        memset(tmpbuff, 0, sizeof(tmpbuff));
        nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
        if (-1 == nret)
        {
            perror("fail to recv");
            return -1;
        }
        else if (0 == nret)
        {
            printf("关闭连接\n");
            break;
        }
        printf("RECV:%s\n", tmpbuff);
    }
    
    close(sockfd);

    return 0;
}

5、三种机制对比总结

特性 select poll epoll
最大连接数 有限制(1024) 无限制 无限制
数据结构 位图数组 结构体数组 内核红黑树
内存拷贝 每次调用都拷贝 每次调用都拷贝 仅初始化时注册
触发模式 仅水平触发 仅水平触发 水平/边沿触发
时间复杂度 O(n) O(n) O(1)(就绪事件数)
可移植性 跨平台 多数Unix Linux特有

6.总结

并发服务器实现图示:

选择建议:

  • 低并发、跨平台需求:select/poll

  • 高并发、Linux专属:epoll

  • 极致性能、最新内核:考虑io_uring(Linux 5.1+)

IO多路复用技术是现代高性能网络服务器的基石,理解其原理和实现差异,是每个Linux后台开发者的必修课。随着连接数的增长,从select/poll迁移到epoll往往是性能提升的关键一步。

相关推荐
柏木乃一1 小时前
Linux线程(2)线程的优点和缺点/线程异常/posix线程库原理
linux·运维·服务器·c++·线程·posix
MIXLLRED1 小时前
Ubuntu 22.04 + ROS2 Humble 环境下设计图形化交互界面
linux·ubuntu·交互·图形界面
2301_808414381 小时前
Linux的权限
linux·运维·服务器
GEO_Huang1 小时前
电商选品自动化,AgentOffice 比 Openclaw 的数据抓取更准吗?
运维·自动化
wbs_scy1 小时前
从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析
linux·运维·服务器
你的论文学长2 小时前
【架构拆解】从 RAG 检索到全局 Linting:如何用工程化思维跑通几万字的自动化写作流?
运维·人工智能·安全·自然语言处理·架构·自动化·ai写作
IT枫斗者2 小时前
CentOS 7 一键部署 K8s 1.23 + Rancher 2.7 完整指南
java·linux·spring boot·后端·kubernetes·centos·rancher
江湖有缘2 小时前
基于华为openEuler搭建Coolmonitor监控服务
linux·华为云·openeuler
the sun342 小时前
Linux上位机开发中的串口termios库函数使用
linux·运维·服务器