高并发服务器与多路IO转接(select, poll, epoll)

目录

[一. 多进程并发服务器](#一. 多进程并发服务器)

[二. 多线程并发服务器](#二. 多线程并发服务器)

[三. 多路IO转接实现高并发服务器](#三. 多路IO转接实现高并发服务器)

[3.1 使用select实现](#3.1 使用select实现)

[3.1.1 select函数](#3.1.1 select函数)

[3.1.2 select的优缺点](#3.1.2 select的优缺点)

[3.1.3 select实现高并发服务器示例代码](#3.1.3 select实现高并发服务器示例代码)

[3.2 使用poll实现](#3.2 使用poll实现)

[3.3 使用epoll实现](#3.3 使用epoll实现)

[3.3.1 epoll_create函数](#3.3.1 epoll_create函数)

[3.3.2 epoll_ctl函数](#3.3.2 epoll_ctl函数)

[3.3.3 epoll_wait函数](#3.3.3 epoll_wait函数)

[3.4 总结](#3.4 总结)

[3.4.1 理解epoll](#3.4.1 理解epoll)

[3.4.2 epoll的优点](#3.4.2 epoll的优点)

[3.5 使用epoll完成高并发服务器示例代码](#3.5 使用epoll完成高并发服务器示例代码)


一. 多进程并发服务器

使用多进程并发服务器时要考虑以下几点:

  1. 父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
  2. 系统内创建进程个数(与内存大小相关)
  3. 进程创建过多是否降低整体服务性能(进程调度)
  4. 进程安全,但是资源开销比较大

示例代码:

注意这里引入了信号,同时又存在慢速系统调用 accept 故应该注意慢速系统调用被信号中断的情况 ,在判断 accept 的返回值时,注意对 errno == EINTR 这种情况进行判断

cpp 复制代码
#define SER_PORT 50000
//子进程回收,回调函数
void handler(int sig)
{
    pid_t wid;
    while((wid =  waitpid(0, NULL, WNOHANG)) > 0){
        printf("%d wait finish!\n",wid);
    }
    return;
}

//初始化监听套接字函数
int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;

    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;
}

int main()
{
    int lisfd, connfd;
    pid_t pid;
    struct sockaddr_in cliaddr;
    struct sigaction act;
    char cli_ip[64] = { 0 };

    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, NULL);//注册信号捕捉函数

    socklen_t cliaddr_len = sizeof(cliaddr);
    lisfd = init_tcp_ser();
    if(lisfd < 0){
        exit(1);
    }
    //printf("lisfd is %d\n",lisfd);
    while(1){
    //printf("lisfd is %d\n",lisfd);
        connfd = accept(lisfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if(connfd < 0){
            if(errno == EINTR){//慢速系统调用被信号中断的情况
                continue;
            }
            perror("accept()");
            exit(1);
        }
        printf("[%s : %d] get online!\n",inet_ntop(AF_INET,
                &cliaddr.sin_addr, cli_ip, sizeof(cliaddr)), 
               ntohs(cliaddr.sin_port));
        
        pid = fork();
        if(pid < 0){
            perror("fork()");
            exit(1);
        }
        else if(pid == 0){//子进程完成服务器功能
            close(lisfd);//关闭监听套接字
            char rbuff[512] = { 0 };
            int i;
            ssize_t size;
            while(1){
                memset(rbuff, 0, sizeof(rbuff));
                size = recv(connfd, rbuff, sizeof(rbuff), 0);
                if(size < 0){
                    perror("recv()");
                    break;
                }
                else if(size == 0){
                    printf("[%s : %d] offline!\n",inet_ntop(AF_INET,
                             &cliaddr.sin_addr, cli_ip, sizeof(cliaddr)), 
                            ntohs(cliaddr.sin_port));

                    printf("客户端断开连接!\n");
                    break;
                }
                for(i = 0; i < strlen(rbuff); i++){
                    rbuff[i] = toupper(rbuff[i]);
                }
                write(STDOUT_FILENO, rbuff, size);
            }
            close(connfd);
            return 0;
        }
        else{
            close(connfd);//关闭通信套接字
        }
    }
    return 0;
}

结果:

二. 多线程并发服务器

在使用线程模型开发服务器时需考虑以下问题:

  1. 调整进程内最大文件描述符上限
  2. 线程如有共享数据,考虑线程同步
  3. 服务于客户端线程退出时,退出处理。(退出值,分离态)
  4. 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
  5. 线程的开销小,并发量相对进程来说大

示例代码:

cpp 复制代码
#define SER_PORT 50000

typedef struct inf{
    struct sockaddr_in cliaddr;
    int connfd;
}Inf_t;


void *handler(void *arg)
{
    Inf_t inf = *(Inf_t *)arg;
    char rbuff[512] = { 0 };
    char cli_ip[64] = { 0 };
    int i;
    ssize_t size;
    while(1){
        memset(rbuff, 0, sizeof(rbuff));
        size = recv(inf.connfd, rbuff, sizeof(rbuff), 0);
        if(size < 0){
            perror("recv()");
            break;
        }
        else if(size == 0){
            printf("[%s : %d] offline!\n",inet_ntop(AF_INET,
                   &inf.cliaddr.sin_addr, cli_ip, sizeof(inf.cliaddr)), 
                   ntohs(inf.cliaddr.sin_port));

            printf("客户端断开连接!\n");
            break;
        }
        for(i = 0; i < strlen(rbuff); i++){
            rbuff[i] = toupper(rbuff[i]);
        }
        write(STDOUT_FILENO, rbuff, size);
    }
    close(inf.connfd);
    pthread_exit(NULL);
}



int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;

    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }

    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;
}

int main()
{
    int lisfd, connfd, i = 0, ret;
    struct sockaddr_in cliaddr;
    char cli_ip[64] = { 0 };
    pthread_t tid;
    Inf_t inf[256] = { 0 };

    socklen_t cliaddr_len = sizeof(cliaddr);
    lisfd = init_tcp_ser();
    if(lisfd < 0){
        exit(1);
    }
    while(1){
        connfd = accept(lisfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if(connfd < 0){
            //if(errno == EINTR){
                //continue;
            //}
            perror("accept()");
            exit(1);
        }
        printf("[%s : %d] get online!\n",inet_ntop(AF_INET,
                &cliaddr.sin_addr, cli_ip, sizeof(cliaddr)), 
               ntohs(cliaddr.sin_port));
        inf[i].cliaddr = cliaddr;
        inf[i].connfd = connfd;
        
        ret = pthread_create(&tid, NULL, handler, (void *)&inf[i]);
        if(ret != 0){
            fprintf(stderr, "pthread_create error :%s\n",strerror(ret));
            exit(1);
        }
        pthread_detach(tid);
        if(ret != 0){
            fprintf(stderr, "pthread_detach error :%s\n",strerror(ret));
            exit(1);
        }
        i++;
    }
    return 0;
}

结果:

三. 多路IO转接实现高并发服务器

在一个进程里面,同时监测多个IO,称IO多路转接

3.1 使用select实现

3.1.1 select函数

**作用:**通知内核监听文件描述符集合中相应的事件

参数:

nfds: 所监听的所有的文件描述符中最大的一个加+1

原因: 在select内部要进行循环操作,上限就是nfds,但是这样会导致性能下降,效率低

eg:监听3,6,1020,则填1021,去一个一个轮询,效率低,文件描述符比较散乱 ,效率就低下

*readfds: 读事件 读文件描述符监听集合,文件描述符可读就代表客户端在写

*writefds: 写事件 写文件描述符监听集合

*exceptfds: 异常事件 异常文件描述符监听集合

以上三个参数都是传入传出参数,是三个集合set
传入的是要监听的,传出来的是实际有事情发生的

*timeout: 超时时长

NULL:永远等下去

设置timeval,等待固定时间

设置timeval里的时间均为0,检查描述字后立即返回,轮询

返回值:

成功:返回所有监听的文件描述符集合中,有事件满足的总个数

0:无满足事件的文件描述符

失败:-1且errno

3.1.2 select的优缺点

优点:

唯一一个可跨平台:windows,linux,unix,类unix,macOS

缺点:

只能轮询判断是哪个文件描述符发生了对应的事件,这一点只能在代码层面改进,例如:使用一个链表储存客户端的信息,提高了编码难度

监听上限受文件描述符限制 max=1024

select将集合表创建在应用层,需要应用层和内核层反复的数据拷贝

select需要遍历寻找到达的IO事件,效率低

select只能工作在水平触发模式 (低速模式,只要有数据就触发),不能工作在边缘触发模式(高

速模式,数据状态变化时才触发),

3.1.3 select实现高并发服务器示例代码

cpp 复制代码
#define SER_PORT 50000

//初始化监听套接字
int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;

    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;
}

//TCP并发服务器
int main()
{
    int lisfd, connfd, maxfd, ret, i;
    char buff[128] = { 0 };
    char cliip[64] = { 0 };
    fd_set rdfds, tmprdfds;
    struct sockaddr_in cliaddr;
    ssize_t size;
    socklen_t cliaddr_len = sizeof(cliaddr);

    lisfd = init_tcp_ser();//初始化监听套接字
    if(lisfd == -1){
        exit(1);
    }

    FD_ZERO(&rdfds);
    FD_SET(lisfd, &rdfds);//设置文件描述符集合
    maxfd = lisfd;

    while(1){
        tmprdfds = rdfds;
        ret = select(maxfd + 1, &tmprdfds, NULL, NULL, NULL);//阻塞等待
        if(ret < 0){
            perror("select()");
            exit(1);
        }
        if(FD_ISSET(lisfd, &tmprdfds)){
            connfd = accept(lisfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
            if(connfd < 0){
                perror("accept()");
                continue;
            }
            printf("[%s : %d] get online!\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr, cliip, sizeof(cliaddr)),
            ntohs(cliaddr.sin_port));

            FD_SET(connfd, &rdfds);
            if(maxfd < connfd){
                maxfd = connfd;
            }
            if(ret == 1){
                continue;/如果只有一个事件发生并且是lisfd则continue不执行下面的代码,提高效率
            }
            
        }
       for(i = lisfd + 1; i <= maxfd; i++){
           if(FD_ISSET(i, &tmprdfds)){
               memset(buff, 0, sizeof(buff));
               size = recv(i, buff, sizeof(buff), 0);
               if(size < 0){
                   perror("recv()");
                   close(i);
                   FD_CLR(i, &rdfds);
                   continue;
               }
               else if(size == 0){
                   close(i);//关闭下线的文件描述符
                   FD_CLR(i, &rdfds);//清除下线的文件描述符
                   continue;
               }
               else{
                   size = write(1, buff, size);
                   if(size < 0){
                        perror("write()");
                        close(i);
                        FD_CLR(i, &rdfds);
                        continue;
                   }
               }     
           }
       }
    }
    close(lisfd);
    return 0;
}

3.2 使用poll实现

poll使用链表保存文件描述符集合,理论上没有文件描述符的限制,相对于select只做了这一点改变,并没有改变其他

3.3 使用epoll实现

3.3.1 epoll_create函数

**作用:**创建一个文件描述符集合

这个文件描述符本质指向的是一个树(红黑树,二叉树),类似句柄,

参数:

**size:**监听文件描述符的最大上限

返回值:

成功: 返回非负的文件描述符,指向新创建的红黑树的根节点epfd

**失败:**返回-1且errno

3.3.2 epoll_ctl函数

**作用:**控制操作创建的文件描述符集合,即控制红黑树

参数:

**epfd:**epoll_create的返回值,文件描述符集合

**op:**对该监听红黑树所做的操作

EPOLL_CTL_ADD: 添加节点到监听的红黑树

EPOLL_CTL_MOD: 修改监听红黑树上的监听事件

**EPOLL_CTL_DEL:**将一个fd从监听红黑树上取消监听​​​​​​​,event设置成NULL

**fd:**需要操作的文件描述符

**event:**事件类型,该结构体的定义如下

cpp 复制代码
typedef union epoll_data {
               void        *ptr;
               int          fd;  /对应监听事件的文件描述符
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;  /监听的事件类型
                    EPOLLIN  /读事件
					EPOLLOUT  /写事件
					EPOLLERR  /出错
               epoll_data_t data;    /联合体
           };

返回值:

成功: 0

**失败:**返回-1且errno

3.3.3 epoll_wait函数

**作用:**通知内核开始监测文件描述符集合并等待返回监测到的事件结果

参数:

**epfd:**epoll_create的返回值,文件描述符集合

events: 用来存内核得到事件的集合,【数组】,传出参数 ,传出满足监听条件的哪些fd

构体。保存到达事件的数组

**maxevents:**数组元素的总个数,监听事件的最大个数

timeout:超时时间

-1,阻塞

0,立即返回,非阻塞

>0,指定等待毫秒

返回值:

成功: 返回有多少文件描述符就绪,满足监听的总个数,可以用作循环上限。时间到时返回0

**失败:**返回-1且errno

3.4 总结

3.4.1 理解epoll

epoll_create函数返回的文件描述符epfd本质上是一个红黑树,二叉树,可以理解为epfd就是一个句柄,拿着这个句柄就可以操作整个二叉树。

当对这个红黑树进行增加节点操作时,首先要定义一个 struct epoll_event类型的结构体,并且将要监听的文件描述符和监听的事件类型初始化完成之后,将此结构体传入到epoll_wait函数中,完成对红黑树的操作。可以借助下面的图来理解:

每增加一个节点,就要连带挂上其文件描述符监听的事件类型

而对于 epoll_wait传出的数组,也可以使用下图进行理解:

3.4.2 epoll的优点

1、epoll是Linux下多路复用IO接口 select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行

2、epoll除了提供select/poll那种IO事件的电平触发(Level Triggered) 外,还提供了边沿触发(Edge Triggered) ,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率。

3、epoll使用树形结构保存文件描述符集合,红黑树,二叉树,遍历时间复杂度是log

4、直接将集合表创建在了内核层 ,避免了应用层与内核层反复的数据拷贝

5、直接返回到达的事件,应用层不需要去遍历判断,返回即发送事件

6、epoll可以工作在水平触发模式,也可以工作在边缘触发模式

3.5 使用epoll完成高并发服务器示例代码

cpp 复制代码
#define SER_PORT 50000
#define SER_IP "192.168.0.182"
#define OPEN_MAX 1024

//初始化监听套接字
int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;

    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    //seraddr.sin_addr.s_addr = htonl(INADDR_ANY);//监听所以可用网卡
    inet_pton(AF_INET, SER_IP, &seraddr.sin_addr);
    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;//返回监听套接字
}
//添加一个节点
int epoll_add_fd(int epfd, int fd, uint32_t event)
{
    int ret;
    struct epoll_event ep;
    ep.events = event;
    ep.data.fd = fd;
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep);
    if(ret < 0){
        perror("epoll_ctl_add()");
        return -1;
    }
    return 0;
}
//删除一个节点
int epoll_del_fd(int epfd, int fd)
{
    int ret;
    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
    if(ret < 0){
        perror("epoll_ctl_del()");
        return -1;
    }
    return 0;
}


//TCP并发服务器
int main()
{
    int lisfd, connfd, epfd, ret, i, nready, j;
    char buff[128] = { 0 };
    char cliip[64] = { 0 };
    struct sockaddr_in cliaddr;//客户端地址
    ssize_t size;
    socklen_t cliaddr_len = sizeof(cliaddr);

    struct epoll_event evs[OPEN_MAX];


    lisfd = init_tcp_ser();//初始化监听套接字
    if(lisfd == -1){
        exit(1);
    }
    
    epfd = epoll_create(OPEN_MAX);//创建文件描述符集合
    if(epfd < 0){
        perror("epoll_create()");
        exit(1);
    }

    ret = epoll_add_fd(epfd, lisfd, EPOLLIN);//将监听套接字加入集合
    if(ret == -1){
        exit(1);
    }
    while(1){
        nready = epoll_wait(epfd, evs, OPEN_MAX, -1);//阻塞等待
        if(nready < 0){
            perror("epoll_wait()");
            exit(1);
        }
        for(i = 0; i < nready; i++){
            if(evs[i].data.fd == lisfd){//判断是否有新客户端连入
                connfd = accept(lisfd, (struct sockaddr *)&cliaddr,
                                &cliaddr_len);
                if(connfd < 0){
                    perror("accept()");
                    continue;
                }
                printf("[%s : %d] get online!\n", 
                       inet_ntop(AF_INET, &cliaddr.sin_addr, cliip,
                       sizeof(cliaddr)),
                       ntohs(cliaddr.sin_port));
                ret = epoll_add_fd(epfd, connfd, EPOLLIN);
                if(ret == -1){
                    continue;
                }
            }
            else{//读事件的发生
                int sockfd = evs[i].data.fd;
                memset(buff, 0, sizeof(buff));  
                size = recv(sockfd, buff, sizeof(buff), 0);
                if(size < 0){//读出现错误
                    perror("recv()");
                    epoll_del_fd(epfd, sockfd);
                    close(sockfd);
                    continue;
                }
                else if(size == 0){//对方关闭连接
                    epoll_del_fd(epfd, sockfd);
                    close(sockfd);
                    printf("[%d] close!\n",sockfd);
                    continue;
                }
                else{
                    for(j = 0; j < size; j++){
                        buff[j] = toupper(buff[j]);
                    }
                    ret = send(sockfd, buff, size, 0);//
                    if(ret < 0){
                        perror("send()");
                        epoll_del_fd(epfd, sockfd);
                        close(sockfd);
                        continue;
                    }
                    write(1, buff, strlen(buff));
                }
            }
        }
    }
    close(lisfd);//关闭监听套接字
    return 0;
}

结果: