高并发服务器模型select poll epoll

前言

  这里介绍三种高并发服务器模型,分别是select 、poll和 epoll。重点为epoll,为接下来reactor做铺垫。

select

多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理.

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

返回值:

成功返回发生变化的文件描述符的个数
失败返回-1, 并设置errno值.

函数介绍:委托内核监控该文件描述符对应的读,写或者错误事件的发生

数据类型fd_set::文件描述符集合——本质是位图

  • nfds: 最大的文件描述符+1 readfds: 读集合, 是一个传入传出参数
传入: 指的是告诉内核哪些文件描述符需要监控 
传出: 指的是内核告诉应用程序哪些文件描述符发生了变化 
  • writefds: 写文件描述符集合(传入传出参数)
  • execptfds: 异常文件描述符集合(传入传出参数)
  • timeout:
NULL--表示永久阻塞, 直到有事件发生
0 --表示不阻塞, 立刻返回, 不管是否有监控的事件发生
>0--到指定事件或者有事件发生了就返回

select-API

void FD_CLR(int fd, fd_set *set);

  将fd从set集合中清除.

int FD_ISSET(int fd, fd_set *set);

  判断fd是否在集合中
  返回值: 如果fd在set集合中, 返回1, 否则返回0.

void FD_SET(int fd, fd_set *set);

  将fd设置到set集合中.

void FD_ZERO(fd_set *set);

  初始化set集合.

  调用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生;

select的优缺点

select优点:

  • 1 一个进程可以支持多个客户端
  • 2 select支持跨平台

select缺点:

  • 1 代码编写困难
  • 2 会涉及到用户区到内核区的来回拷贝
  • 3 当客户端多个连接, 但少数活跃的情况, select效率较低
      例如:作为极端的一种情况, 3-1023文件描述符全部打开, 但是只有1023有发送数据, select就显得效率低下
  • 4最大支持1024个客户端连接
      select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的,而是由FD_SETSIZE=1024限制的.

  FD_SETSIZE=1024 fd_set使用了该宏, 当然可以修改内核, 然后再重新编译内核, 一般不建议这么做.

以下是select的实现代码:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <pthread.h>


int main()
{
    char buf[1024]={0};

    //socket()
    int socketfd= socket(AF_INET,SOCK_STREAM,0);
    if(socketfd<0){
        return -1;
    }

    //bind()
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port= htons(8002);
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    int bindret=bind(socketfd,(struct sockaddr *)&addr,sizeof(addr));
    if(bindret<0){
        return -2;
    }

    //listen()
    int listenret=listen(socketfd,128);
    if(listenret<0){
        return -3;
    }

    //set select
    fd_set rse,cprse;
    FD_ZERO(&rse);
    FD_ZERO(&cprse);
    FD_SET(socketfd,&rse);

    int max_fd=socketfd;
    while(1){
        cprse=rse;
        int nretse=select(max_fd+1,&cprse,NULL,NULL,NULL);
        if(nretse<0){
            return -9;
        }

        if(FD_ISSET(socketfd,&cprse)){
            int confd=accept(socketfd,NULL,NULL);
            if(confd<0){
                return -9;
            }
            printf("have new con\n");
            FD_SET(confd,&rse);
            if(confd>max_fd)max_fd=confd;
            if(--nretse==0)continue;
        }

        for(int i=socketfd+1;i<=max_fd;i++){
            if(FD_ISSET(i,&cprse)){
                memset(buf,0,sizeof(buf));
                int n=read(i,buf,sizeof(buf));
                if(n<=0){
                    printf("have con break\n");
                    close(i);
                    FD_CLR(i,&rse);
                }
                else if(n>0){
                    printf("buf=[%s]\n",buf);
                    write(i,"OK\n",4);
                }
                if(--nretse==0)continue;
            }
        }
    }

    close(socketfd);

    return 0;
}

  将想要被监听的文件描述符先添加到集合,然后调用select进行轮询,也就是内部一个个查找,如果有文件描述符可读,可写,异常等,就会返回一个集合,这个集合上有的文件描述符也就是发生了事件的文件描述符,传入传出参数的意思就是这样,传出来的时候和传进去的时候不一样,会在原有的传入的东西上面做修改,这样就需要提前备份好一份内容,不然传入再传出的时候就会发生改变。当有连接到来的时候,就是socketfd这个文件描述符有可读事件发生,当某个客户端有数据发来的时候对应的文件描述符就有可读事件发生。

poll

poll跟select类似, 监控多路IO, 但poll不能跨平台.

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds: 传入传出参数, 实际上是一个结构体数组
struct pollfd 
{
   int   fd;        /* file descriptor */   监控的文件描述符
   short events;     /* requested events */  要监控的事件---不会被修改
   short revents;    /* returned events */   返回发生变化的事件 ---由内核返回
};
fds.events: 
   POLLIN---->读事件
   POLLOUT---->写事件
   。。。。。。。
  • nfds: 数组实际有效内容的个数
  • timeout: 超时时间, 单位是毫秒.
  -1: 永久阻塞, 直到监控的事件发生
   0: 不管是否有事件发生, 立刻返回
  >0: 直到监控的事件发生或者超时

返回值:

  成功:返回就绪事件的个数
  失败: 返回-1
  若timeout=0, poll函数不阻塞,且没有事件发生, 此时返回-1, 并且errno=EAGAIN, 这种情况不应视为错误.

这个函数的第一个参数的结构体类型

struct pollfd 
{
   int   fd;        /* file descriptor */   监控的文件描述符
   short events;     /* requested events */  要监控的事件---不会被修改
   short revents;    /* returned events */   返回发生变化的事件 ---由内核返回
};

说明:
  1 当poll函数返回的时候, 结构体当中的fd和events没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离.
  2 struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.
  3 相对于select, poll没有本质上的改变; 但是poll可以突破1024的限制.

  在/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限.
  如果需要可以修改配置文件: /etc/security/limits.conf
  加入如下配置信息, 然后重启终端即可生效.

* soft nofile 1024
* hard nofile 100000

  soft和hard分别表示ulimit命令可以修改的最小限制和最大限制
以下是poll的代码实现:

int main()
{
    char buf[1024]={0};

    //socket()
    int socketfd= socket(AF_INET,SOCK_STREAM,0);
    if(socketfd<0){
        return -1;
    }

    //bind()
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port= htons(8002);
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    int bindret=bind(socketfd,(struct sockaddr *)&addr,sizeof(addr));
    if(bindret<0){
        return -2;
    }

    //listen()
    int listenret=listen(socketfd,128);
    if(listenret<0){
        return -3;
    }

    //set poll
    struct pollfd plfd[1024];
    memset(&plfd,0,sizeof(plfd));
    plfd[0].fd=socketfd;
    plfd[0].events=POLLIN;

    for(int i=1;i<1024;i++){
        plfd[i].fd=-1;
    }

    int max_po=0;
    while(1){

        int nready=poll(plfd,max_po+1,-1);
        if(nready<0){
            return -9;
        }

        if(plfd[0].revents & POLLIN){
            int confd=accept(socketfd,NULL,NULL);
            if(confd<0){
                return -9;
            }
            printf("have new con\n");
            for(int i=1;i<=max_po+1;i++){
                if(plfd[i].fd==-1){
                    plfd[i].fd=confd;
                    plfd[i].events=POLLIN;
                    if(i==max_po+1)max_po++;
                    break;
                }
            }
        }

        for(int i=1;i<=max_po;i++){
            if(plfd[i].revents & POLLIN){
                memset(buf,0,sizeof(buf));
                int n=read(plfd[i].fd,buf,sizeof(buf));
                if(n<=0){
                    printf("have con break\n");
                    close(plfd[i].fd);
                    plfd[i].fd=-1;
                }
                else if(n>0){
                    printf("buf=[%s]\n",buf);
                    write(plfd[i].fd,"OK\n",4);
                }
            }
        }
    }

    close(socketfd);

    return 0;
}

  这里我没有清空结构体中revents的内容,因为每次返回回来之后有事件发生它就会有值,没有事件发生内核就会把这里相应的值清空,打个比方:我这里没有对它进行处理如果还有残留的话就算没有新连接到来,socketfd还会显示有事件发生,这样accept()函数就会阻塞在那,但是没有阻塞,实验证明这个代码可以正常运行。

epoll

  epoll是将检测文件描述符的变化委托给内核去处理, 然后内核将发生变化的文件描述符对应的事件返回给应用程序。这个的底层是红黑树加链表,后续文章会具体介绍epoll原理,简单理解就是把fd作为节点,把要监控的节点都挂在树上,返回有事件发生的节点。

epoll-API

int epoll_create(int size);

函数说明: 创建一个树根
参数说明:

  • size: 最大节点数, 此参数在linux 2.6.8已被忽略, 但必须传递一个大于0的数.无论大于0的数填几都只是创建一个树根,不限制最大节点数。
    返回值:
成功: 返回一个大于0的文件描述符, 代表整个树的树根.
失败: 返回-1, 并设置errno值.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数说明: 将要监听的节点在epoll树上添加, 删除和修改
返回值:

成功返回 0 
失败返回-1
  • epfd: epoll树根
  • op:
EPOLL_CTL_ADD: 添加事件节点到树上
EPOLL_CTL_DEL: 从树上删除事件节点
EPOLL_CTL_MOD: 修改树上对应的事件节点
  • fd: 事件节点对应的文件描述符
  • event: 要操作的事件节点
struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };
typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;
  • event.events常用的有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 错误事件
EPOLLET: 边缘触发模式
  • event.fd: 要监控的事件对应的文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数说明:等待内核返回事件发生
参数说明:

  • epfd: epoll树根
  • events: 传出参数, 其实是一个事件结构体数组
  • maxevents: 数组大小
  • timeout:
-1: 表示永久阻塞
 0: 立即返回
>0: 表示超时等待事件

返回值:

成功: 返回发生事件的个数
失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值, 

  epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值,当epoll_wait返回的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改。

epoll的两种工作模式

epoll的两种模式ET和LT模式
  水平触发LT: 高电平代表1
    只要缓冲区中有数据, 就一直通知
  边缘触发ET: 电平有变化就代表1
    缓冲区中有数据只会通知一次, 之后再有数据才会通知.(若是读数据的时候没有读完, 则剩余的数据不会再通知, 直到有新的数据到来)

  epoll默认是水平触发LT,在需要高性能的场景下,可以改成边缘ET非阻塞方式来提高效率。
  ET模式由于只通知一次, 所以在读的时候要循环读, 直到读完, 但是当读完之后read就会阻塞, 所以应该将该文件描述符设置为非阻塞模式(fcntl函数)。如果不循环读,读完的话还好,没读完的话数据就会留在读缓冲区,影响数据的接收和判断。read函数在非阻塞模式下读的时候, 若返回-1, 且errno为EAGAIN, 则表示当前资源不可用, 也就是说缓冲区无数据(缓冲区的数据已经读完了); 或者当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲区中已没有数据可读了,也就可以认为此时读事件已处理完成。

以下是epoll的代码实现:

int main()
{
    char buf[1024]={0};

    //socket()
    int socketfd= socket(AF_INET,SOCK_STREAM,0);
    if(socketfd<0){
        return -1;
    }

    //bind()
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port= htons(8002);
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    int bindret=bind(socketfd,(struct sockaddr *)&addr,sizeof(addr));
    if(bindret<0){
        return -2;
    }

    //listen()
    int listenret=listen(socketfd,128);
    if(listenret<0){
        return -3;
    }

    //set epoll
    int epofd= epoll_create(1);
    if(epofd<0){
        return -1;
    }

    struct epoll_event node[1024];
    struct epoll_event temp;
    memset(&node,0,sizeof(node));
    temp.events=EPOLLIN;
    temp.data.fd=socketfd;
    int retc= epoll_ctl(epofd,EPOLL_CTL_ADD,socketfd,&temp);
    if(retc<0) {
        return -1;
    }

    while(1){

        int nready= epoll_wait(epofd,node,1024,-1);
        if(nready<0){
            return -9;
        }

        for(int i=0;i<nready;i++){
            if(node[i].data.fd==socketfd){
                int confd=accept(socketfd,NULL,NULL);
                if(confd<0){
                    return -9;
                }
                printf("have new con\n");
                temp.events=POLLIN;
                temp.data.fd=confd;
                epoll_ctl(epofd,EPOLL_CTL_ADD,confd,&temp);
            }else {
                memset(buf,0,sizeof(buf));
                int n=read(node[i].data.fd,buf,sizeof(buf));
                if(n<=0) {
                    printf("have con break\n");
                    close(node[i].data.fd);
                    epoll_ctl(epofd,EPOLL_CTL_DEL,node[i].data.fd,NULL);
                }else {
                    printf("buf=[%s]\n",buf);
                    write(node[i].data.fd,"OK\n",4);
                }
            }
        }
    }

    close(socketfd);
    return 0;
}

  epoll的优点:
    1.性能高,百万并发不在话下,而select就不行
  epoll的缺点:
    1.不能跨平台,linux下的

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值