高并发服务器--多路IO转接(多路IO复用)

本文详细介绍了在高并发服务器中,select、poll和epoll三种多路IO转接技术的工作原理、API使用、代码示例及优缺点,探讨了它们在不同场景下的适用性和效率提升策略。
摘要由CSDN通过智能技术生成

目录

1、select实现

1.1 基本原理:

1.2 API:

1.3 代码:

1.4 优缺点

2、poll实现

2.1 工作流程

2.1 API

2.2 代码

3、epoll实现

3.1 API

3.1.1 epoll_create

3.1.2 epoll_ctl

 3.1.3 epoll_wait

3.2 代码


高并发服务器的三种方式:

  1. 阻塞等待--消耗资源(如多线程多进程实现)
  2. 非阻塞忙轮询--消耗cpu
  3. 多路IO转接(内核监听多个文件描述符的属性(读写缓冲区)变化,如果某个文件描述符的读缓冲区变化了,这个时候就是可以读了,将这个事件告知应用层)

        多路IO转接三种方式:select(windows, 跨平台)、poll(少用)、epoll(linux)。

        多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想时,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。

1、select实现

        select能监听文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。

        解决1024以下客户端时使用select是很合适的,但是如果客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力。

1.1 基本原理:
  1. select核心实现原理是位图,select总共有三种位图,分别为读,写,异常位图。用户程序预先将socket文件描述符注册至读,写,异常位图,然后通过select系统调用轮询位图中的socket的读,写,异常事件。
  2. 内核通过轮询方式获取读,写,异常位图中注册的socket文件事件,如果检测到有socket文件处于就绪状态,则会将socket对应的事件设置到输出位图,等所有位图中的socket都被轮询完,会统一将输出位图通过copy_to_user函数复制到输入位图,并且覆盖掉输入位图注册信息(也就是用户初始化的位图被内核修改)
  3. select轮询完所有位图,如果未检测到任何socket文件处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
  4. socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询所有位图。
  5. select返回时会将剩余的超时时间通过copy_to_user覆盖原来的超时时间。
1.2 API:
#include<sys/select.h>
#include<sys/time.h>
#include<sys/type.h>
#include<unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, 
            fd_set *exceptfds, struct timeval *timeout);

功能:监听多个文件描述符的属性变化(读、写、异常)
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

参数:
    nfds : 最大文件描述符+1
    readfds : 需要监听的读的文件描述符存放的集合
    writefds : 需要监听的写的文件描述符存放的集合 NULL
    exceptfds : 需要监听的异常的文件描述符存放的集合 NULL
    timeout : 多长时间监听一次 固定的事件,限时等待 NULL永久监听
    struct timeval{
        long tv_sec;   // seconds
        long tv_usec;  // microseconds
    };
返回值:返回的是变化的文件描述符的个数
注意:变化的文件描述符会存在监听的集合中,未变化的文件描述符会被删除
1.3 代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/time.h>
#include"wrap.h"
#include<sys/select.h>
#define PORT 8800

int main()
{
    // 创建监听套接字、绑定
    int lfd = tcp4bind(PORT, NULL);
    
    // 监听
    Listen(lfd, 128);
    int maxfd = lfd;
    fd_set oldset, rset;
    FD_ZERO(&oldset);
    FD_ZERO(&rset);
    FD_SET(lfd, &oldset);
    // 循环调用select,并处理发生变化的文件描述符
    while(1)
    {
        rset = oldset;
        int n = select(maxfd+1, &rset, NULL, NULL, NULL); // n为发生变化文件描述符的数量
        if(n < 0)
        {
            perror("select error:");
            break;
        }
        else if(n == 0) // 超时
        {
            continue;
        }
        else // 有文件描述符发生变化
        {
            // lfd变化
            if(FD_ISSET(lfd, &rset))
            {
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                char ip[16] = "";
                int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &len);
                printf("new client ip = %s; port = %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip ,16),
                                ntohs(cliaddr.sin_port));
                // 将cfd添加至oldset集合中,以下次监听
                FD_SET(cfd, &oldset);
                // 更新maxfd
                if(cfd > maxfd)
                    maxfd = cfd;
                // 只有lfd变化,continue
                if(--n == 0)
                    continue;
            }
        }
        // cfd变化
        for(int i = lfd + 1; i <= maxfd; i++)
        {
            // 如果i文件描述符在rset中
            if(FD_ISSET(i, &rset))
            {
                char buf[1500]= "";
                int ret = Read(i, buf, sizeof(buf));
                if(ret < 0)
                {
                    perror("read error:");
                    close(i);
                    FD_CLR(i, &oldset);
                    continue;
                }
                else if(ret == 0)
                {
                    printf("client close.\n");
                    close(i);
                    FD_CLR(i, &oldset);
                }
                else
                {
                    printf("%s\n", buf);
                    write(i, buf, ret);
                }
            }
        }
    }
    return 0;
}
1.4 优缺点

        优点:跨平台

        缺点:    文件描述符1024的限制

                       只是返回变化的文件描述符的个数,具体哪个变化需要遍历

                       每次都需要将需要监听的文件描述符集合由应用层拷贝到 内核

                       效率低:

                                假设现在4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了?

                                假设现在4-1023个文件描述符需要监听,但是只有5,1002发来消息。

2、poll实现

2.1 工作流程
  1. 用户空间程序调用poll函数,并传入了一个pollfd结构数组,以及数组的大小和超时时间等参数。
  2. 内核遍历该数组,检查每个文件描述符所对应的I/O事件是否发生
  3. 如果有文件描述符的I/O事件发生,就在相应的pollfd结构中设置相应的标志位。
  4. poll函数返回给用户空间,并通知哪些文件描述符已经就绪。
  5. 用户空间程序根据返回的结果进行相应的处理
2.1 API
#include<poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:监听多个文件描述符的属性变化
参数:
fds: 监听的数组的首元素地址
nfds: 数组的有效元素的最大下标+1
timeout: 超时事件  -1为永久监听  >=0为限时等待

数组元素:
struct pollfd
{
    int fd;        // 需要监听的文件描述符
    short events;  // 需要监听文件描述符什么事件 POLLIN读事件、POLLOUT写事件
    short revents; // 返回监听到的事件          同上
}
2.2 代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<poll.h>
#include<errno.h>
#include<ctype.h>
#include"wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000
#define OPEN_MAX 1024

int main()
{
    int i, j, maxi, lfd, cfd, sockfd;
    int nready;
    ssize_t n;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];
    socklen_t clilen;
    struct pollfd client[OPEN_MAX]; // 定义poll数组
    struct sockaddr_in cliaddr, servaddr;
    
    lfd = Socket(AF_INET, SOCK_STREAM, 0);
    
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 设置端口复用
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    Bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    Listen(lfd, 128);
    
    client[0].fd = lfd;             // 要监听的第一个文件描述符,存入client[0]
    client[0].events = POLLIN;      // lfd监听普通读事件

    for(i = 1; i < OPEN_MAX; i++)
        client[i].fd = -1; // 用-1初始化client[]里剩下元素  0也是文件描述符,不能用来初始化
    maxi = 0;

    while(1)
    {
        nready = poll(client, maxi+1, -1); // 阻塞监听是否有客户端链接请求
        if(client[0].revents & POLLIN)
        {
            clilen = sizeof(cliaddr);
            cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &clilen); // 接收客户端请求
            printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
                    ntohs(cliaddr.sin_port));
            for(i = 1; i<OPEN_MAX; i++)
                if(client[i].fd < 0)
                {
                    client[i].fd = cfd; // 找到client[]中空闲的位置,存放accept返回的cfd
                    break;
                }

            if(i == OPEN_MAX) // 达到最大客户端数
            {
                perror("too many clients");
                continue;
            }
            client[i].events = POLLIN; // 设置刚刚返回的cfd,监控读事件

            if(i > maxi)
                maxi = i;
            if(--nready <= 0)
                continue;
        }

        for(i = 1; i<=maxi; i++)
        {
            if((sockfd = client[i].fd) < 0)
                continue; // 找到第一个大于0的
            if(client[i].revents & POLLIN)
            {
                if((n = Read(sockfd, buf, MAXLINE)) < 0)
                {
                    // connection reset by client
                    if(errno == ECONNRESET) // 收到RST标志
                    {
                        printf("client[%d] aborted connection\n", i);
                        close(sockfd);
                    }
                }
                else if(n == 0)
                {
                    printf("client[%d] closed connection\n", i);
                    close(sockfd);
                    client[i].fd = -1;
                }
                else
                {
                    for(j = 0; j < n; j++)
                        buf[j] = toupper(buf[j]);
                    Write(sockfd, buf, n);
                }
                if(--nready <= 0)
                    break;
            }
        }
    }
    return 0;
}

3、epoll实现

3.1 API
3.1.1 epoll_create
int epoll_creat(int size);
功能:创建一个epoll对象

参数:size取大于0即可

返回值:成功返回epoll对象epfd。
    小于0表示创建失败

3.1.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:epoll_ctl函数用于增加、删除、修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。
参数:
epfd:epoll文件描述符
op:操作码
    EPOLL_CTL_ADD: 插入事件
    EPOLL_CTL_DEL: 删除事件
    EPOLL_CTL_MOD: 修改事件
fd:epoll事件绑定的套接字文件描述符
event:epoll事件结构体。
返回值:
    成功:返回0。
    失败:返回-1,并设置errno。

struct epoll_event
{
    uint32_t events; // epoll事件
    epoll_data_t data;
};
typedef union epoll_data
{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;

        epoll_ctl函数增加epoll事件时,系统默认注册EPOLLERR和EPOLLHUP事件

 3.1.3 epoll_wait

epoll就绪事件处理示例:

        1、注册epoll事件

struct epoll_event ev;
ev.data.fd = sock_fd;
ev.event = EPOLLIN; // 注册EPOLLIN事件
epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &ev);

        2、就绪epoll事件

res = EPOLLIN | EPOLLRDNORM;

        3、epoll_wait获取事件

events = (EPOLLIN | EPOLLERR | EPOLLHUP) & (EPOLLIN | EPOLLRDNORM) = EPOLLIN;

注意:只有注册的事件才能通过epoll_wait获取

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:epoll_wait用于监听套接字事件。
参数:
epfd: epoll文件描述符。
events: epoll事件数组。
maxevents:epoll事件数组长度
timeout:超时时间
    小于0:一直等待。
    等于0:立即返回。
    大于0:等待超时时间返回,单位毫秒
返回值:
    小于0:出错。
    等于0:超时。
    大于0:返回就绪事件个数。
3.2 代码
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include"wrap.h"
int main()
{
    // 创建套接字并绑定
    int lfd = tcp4bind(8000, NULL);
    // 监听
    Listen(lfd, 128);
    // 创建树
    int epfd = epoll_create(1);
    // 将lfd上树
    struct epoll_event ev, evs[1024];
    ev.data.fd = lfd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    // while监听套接字事件
    while(1)
    {
        int nready = epoll_wait(epfd, evs, 1024, -1); // epoll_wait监听套接字事件, nready为就绪事件的数量,evs为epoll事件数组(用于接收)
        if(nready < 0)
        {
            perror("");
            break;
        }
        else if(nready == 0) // 超时
            continue;
        else // 有文件描述符变化
        {
            for(int i = 0; i < nready; i++)
            {
                // 判断lfd变化,并且是读事件变化
                if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN)
                {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len); // 提取新的连接
                    printf("new client ip = %s port = %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                                    ntohs(cliaddr.sin_port));
                    // 将cfd上树
                    ev.data.fd = cfd;
                    ev.events = EPOLLIN;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                }
                else if(evs[i].events & EPOLLIN) // cfd变化,而且是读事件变化
                {
                    char buf[1024] = "";
                    int n = read(evs[i].data.fd, buf, sizeof(buf));
                    if(n < 0) // 出错,cfd下树
                    {
                        perror("");
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
                    }
                    else if(n == 0) // 客户端关闭
                    {
                        printf("client close\n");
                        close(evs[i].data.fd);//将cfd关闭
                        epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); // 下树
                    }
                    else
                    {
                        printf("%s\n", buf);
                        write(evs[i].data.fd, buf, n);
                    }
                }
            }
        }
    }
    return 0;
}
3.3 epoll两种工作方式

1、监听读缓冲区的变化

        水平(LT)触发:只要读缓冲区有数据就会触发epoll_wait。

        边沿(ET)触发:数据来一次,epoll_wait只触发一次。

2、监听写缓冲区的变化

        水平触发:只要可以写,就会触发。

        边沿触发:数据从有到无,就会触发。

        LT模式只不过比ET模式多执行了一个步骤,就是当epoll_wait获取完就绪队列epoll事件后,LT模式会再次将epoll事件节点再次添加到就绪队列。

        默认设置都是水平触发,水平触发如果数据一次性都不干净,就需要多次系统调用,浪费资源,一般都会设置为边沿触发,改为边沿触发只需要将监听事件或上一个宏:

ev.events = EPOLLIN | EPLLET; // 监听读事件并设置为ET模式

        设置完之后,则需要在读数据的时候一次性将数据读完,不然会出现读不完的情况。此时只需要将read那里加一个while循环。如果read读完了缓冲区的数据之后会阻塞,所以需要将其设置为非阻塞:

int flags = fcntl(cfd, F_GETFL); // 获取cfd的标志位
flags |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flags); // 将cfd设置为非阻塞
  • 33
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值