欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux IO多路转接——select,poll,epoll

程序员文章站 2022-06-13 14:03:09
...

一、IO操作方式

多路I/O转接服务器

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

阻塞等待

  • 好处:不占用CPU宝贵时间
    Linux IO多路转接——select,poll,epoll
  • 缺点:同一时间只能处理一个操作,效率低
    Linux IO多路转接——select,poll,epoll

非阻塞, 忙轮询

  • 优点: 提高了程序的执行效率
  • 缺点: 需要占用更多的cpu和系统资源

一个任务
Linux IO多路转接——select,poll,epoll
多个任务
Linux IO多路转接——select,poll,epoll

解决方案:

使用IO多路转接技术 select/poll/epoll

第一种: select/poll

Linux IO多路转接——select,poll,epoll

select 代收员比较懒, 她只会告诉你有几个快递到了,但是哪个快递,你需要挨个遍历一遍。

select服务器代码

/*************************************************************************
    > File Name: select_server.c
    > Author: 杨永利
    > Mail: aaa@qq.com 
    > Created Time: 2020年10月26日 星期一 10时36分26秒
 ************************************************************************/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>

int main(int argc, char* argv[])
{
    if(argc<2)
    {
        printf("eg: ./a.out port\n");
         exit(1);
    }
    struct sockaddr_in serv_addr;

    socklen_t serv_len =sizeof(serv_addr);
    int port = atoi(argv[1]);
    
    // 创建套接字
    int lfd=socket(AF_INET,SOCK_STREAM,0);
    // 初始化服务器 sockaddr_in
    memset(&serv_addr,0,serv_len);
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port=htons(port);
    // 绑定IP和端口
    bind(lfd,(struct sockaddr*)&serv_addr,serv_len);

    // 设置同时监听的最大个数
    listen(lfd,36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len=sizeof(client_addr);

    // 最大的文件描述符
    int maxfd= lfd;
    // 文件描述符读集合
    fd_set reads, temp;
    // init 初始化
    FD_ZERO(&reads);
    FD_SET(lfd, &reads);
    
    while(1)
    {
        // 委托内核做IO检测
        temp =reads;
        int ret=select(maxfd+1,&temp,NULL,NULL,NULL);
        if(ret == -1)
        {
            perror("select error\n");
            exit(1);
        }
         // 客户端发起了新的连接
        if(FD_ISSET(lfd, &temp))
        {
            // 接受连接请求 - accept不阻塞
            int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
            if(cfd == -1)
            {
                perror("accept error");
                exit(1);
            }
            char ip[64];
            printf("new client IP: %s, Port: %d\n", 
                   inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                   ntohs(client_addr.sin_port));
            // 将cfd加入到待检测的读集合中 - 下一次就可以检测到了
            FD_SET(cfd, &reads);
            // 更新最大的文件描述符
            maxfd = maxfd < cfd ? cfd : maxfd;
        }
        // 已经连接的客户端有数据到达
        for(int i=lfd+1; i<=maxfd; ++i)
        {
            if(FD_ISSET(i, &temp))
            {
                char buf[1024] = {0};
                int len = recv(i, buf, sizeof(buf), 0);
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("客户端已经断开了连接\n");
                    close(i);
                    // 从读集合中删除
                    FD_CLR(i, &reads);
                }
                else
                {
                    printf("recv buf: %s\n", buf);
                    send(i, buf, strlen(buf)+1, 0);
                }
            }    
        }

    }
    
    return 0;
}

poll服务器代码

/*************************************************************************
    > File Name: poll.c
    > Author: 杨永利
    > Mail: aaa@qq.com 
    > Created Time: 2020年10月28日 星期三 11时51分23秒
 ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h>

#define SERV_PORT 8989

int main(int argc, const char* argv[])
{
    int lfd, cfd;
    struct sockaddr_in serv_addr, clien_addr;
    int serv_len, clien_len;

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(SERV_PORT);            // 设置端口 
    serv_len = sizeof(serv_addr);
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    // poll结构体
    struct pollfd allfd[1024];
    int max_index = 0;
    // init
    for(int i=0; i<1024; ++i)
    {
        allfd[i].fd = -1;
    }
    allfd[0].fd = lfd;
	allfd[0].events = POLLIN;

    while(1)
    {
        int i = 0;
        int ret = poll(allfd, max_index+1, -1); 
        if(ret == -1)
        {
            perror("poll error");
            exit(1);
        }

        // 判断是否有连接请求
        if(allfd[0].revents & POLLIN)
        {
            clien_len = sizeof(clien_addr);
            // 接受连接请求
            int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
            printf("============\n");

            // cfd添加到poll数组
            for(i=0; i<1024; ++i)
            {
                if(allfd[i].fd == -1)
                {
                    allfd[i].fd = cfd;
                    break;
                }
            }
            // 更新最后一个元素的下标
            max_index = max_index < i ? i : max_index;
        }

        // 遍历数组
        for(i=1; i<=max_index; ++i)
        {
            int fd = allfd[i].fd;
            if(fd == -1)
            {
                continue;
            }
            if(allfd[i].revents & POLLIN)
            {
                // 接受数据
                char buf[1024] = {0};
                int len = recv(fd, buf, sizeof(buf), 0);
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    allfd[i].fd = -1;
                    close(fd);
                    printf("客户端已经主动断开连接。。。\n");
                }
                else
                {
                    printf("recv buf = %s\n", buf);
                    for(int k=0; k<len; ++k)
                    {
                        buf[k] = toupper(buf[k]);
                    }
                    printf("buf toupper: %s\n", buf);
                    send(fd, buf, strlen(buf)+1, 0);
                }

            }

        }
    }

    close(lfd);
    return 0;
}



客户端代码

/*************************************************************************
    > File Name: client.c
    > Author: 杨永利
    > Mail: aaa@qq.com 
    > Created Time: 2020年10月27日 星期二 23时20分19秒
 ************************************************************************/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }

    int port = atoi(argv[1]);
    // 创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    // 连接服务器
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(port);
    // oserv.sin_addr.s_addr = htonl();
    inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
    connect(fd, (struct sockaddr*)&serv, sizeof(serv) );

    // 通信
    while(1)
    {
        // 发送数据
        char buf[1024];
        printf("请输入要发送的字符串: \n");
        fgets(buf, sizeof(buf), stdin);
        write(fd, buf, strlen(buf));

        // 等待接收数据
        int len = read(fd, buf, sizeof(buf));
        if(len == -1)
        {
            perror("read error");
            exit(1);
        }
        else if(len == 0)
        {
            printf("服务器端关闭了连接\n");
            break;
        }
        else
        {
            printf("recv buf: %s\n", buf);
        }
    }

    close(fd);

    return 0;
}


第二种: epoll

Linux IO多路转接——select,poll,epoll

epoll代收快递员很勤快, 她不仅会告诉你有几个快递到了, 还会告诉你是哪个快递公司的快递

epoll服务器代码

/*************************************************************************
    > File Name: epoll.c
    > Author: 杨永利
    > Mail: aaa@qq.com 
    > Created Time: 2020年10月28日 星期三 15时38分00秒
 ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>

int main(int argc, const char* argv[])
{
    if(argc<2)
    {
        printf("eg: ./a.out  port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);
    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;
    ev.events=EPOLLIN;
    ev.data.fd=lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd  文件IO检测
        int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(all[0]), -1 );

        // 遍历all数组中的前ret个元素
        for (int i = 0; i < ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd==lfd)
            {
                // 有就接守新连接
                int cfd =accept(lfd,(struct sockaddr*)&client_addr,&cli_len );
                if (cfd == -1)
                {
                   perror("accept error\n");
                   exit(1);
                          /* code */
                }
                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                temp.events =EPOLLIN;
                temp.data.fd = cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&temp);

                // 打印客户端信息
                char ip[64]={0};
                printf("New  client IP :%s ,Port: %d\n",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    ntohs(client_addr.sin_port));
            }
            else // 没有新连接
            {
                // c处理已经连接进来的客户端发来的信息
                if (!all[i].events & EPOLLIN)
                {
                    continue;
                }
                // 读数据
                char buf[1024]={0};
                int len = recv(fd,buf,sizeof(buf),0);
                if (len==-1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("client disconnected ....\n");
                    close(fd);
                    // fd从epoll树上删除
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                }
                else
                {
                    printf(" recv buf: %s\n", buf);
                    write(fd, buf, len);
                }
            }
        }
    }
    

    close(lfd);
    return 0;

}

二. 什么是I/O多路转接技术:

  • 先构造一张有关文件描述符的列表, 将要监听的文件描述符添加到该表中

  • 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回。

    • 该函数为阻塞函数
    • 函数对文件描述符的检测操作是由内核完成的
  • 在返回时,它告诉进程有多少(哪些)描述符要进行I/O操作。