select实现多路IO转接

Posted milaiko

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了select实现多路IO转接相关的知识,希望对你有一定的参考价值。

select函数介绍

#include <sys/time.h>
#include <sys/types.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 需要观测的文件描述符集合, 观测其中文件描述符有没有read的事件发生,如果没有,就会在这个文件描述符集合删去(位图设为0)

writefdreadfds类似,观测其中文件描述符有没有write的事件发生,如果没有,就会在这个文件描述符集合删去(位图设为0)

exceptfds 和上面类似,观测其中文件描述符有没有异常的事件发生,如果没有,就会在这个文件描述符集合删去(位图设为0)

timeval 是用来告知内核等待所指定的描述符的任何一个就绪可花多长时间。

  • NULL 说明内核会一直等下去
  • 固定时间 设置好timeval结果的秒和微秒
  • 0 说明内核会一直询问(根本不等待),也就是轮询

其中FD_CLR FD_ISSET, FD_SETFD_ZERO 分别表示

  • 从set里面clear某个文件描述符, 设置某个文件描述符为0
  • 判断set里面有没有某个文件描述符
  • 在set里面设置某个文件描述符为1
  • 将set集合全部清零

返回值

该函数返回值表示跨所有描述符集的已就绪的总位数,如果在任何文件描述符就绪之前定时器到时, 则返回0;如果出错返回-1。

用select实现服务器IO转接

服务器常规步骤

  • 先使用socket创建监听套接字
  • 设置setsockopt实现端口复用
  • 设置服务器结构体 sockaddr_in ,传入参数时强转为sockaddr
  • 使用Bind绑定服务器地址结构
  • 开始监听

设置fdset

我们现在只对读和异常这两个事件进行监视

  • 定义fd_set rset, exception_set, 并使用FD_ZERO函数将这两个fdset清零
  • 将connfd放入对应的文件描述符集合中rsetexcepiton_set
  • 使用select监听用户感兴趣的文件描述符(connfd )的可读事件和异常事件
  • 利用FD_ISSET来对可读事件进行处理
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<ctype.h>
#include<pthread.h>
#include<string.h>
#include<stdio.h>
#include<stdlib.h>

#include"wrap.h"

const int PORT  = 8888;
int main(int argc, char* argv[]){
    int i,j,n,maxi;

    int nready, client[FD_SETSIZE];                     //FD-SERSIZE是1024,防止遍历1024个文件描述符
    int maxfd, listenfd, connfd, sockfd;
    char buf[BUFSIZ], str[INET_ADDRSTRLEN];

    struct sockaddr_in cli_addr, serv_addr;
    socklen_t cli_addr_len;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);         //创建监听套接字
    // 端口复用
    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    //设置服务器地址结构体
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    //将服务器地址和套接字绑定
    Bind(listenfd,(struct sockaddr*)&serv_addr, sizeof(serv_addr));
    //开始监听
    Listen(listenfd, 128);
    printf("Accpeting connetion");

    // maxfd 是最后一个(最大一个)文件描述符, maxi是对应的下标
    maxfd = listenfd;
    maxi = -1;  
    for(i = 0;i<FD_SETSIZE;i++){                        //初始化client数组
        client[i] = -1;
    }

    fd_set rset, allset;                                //rset:读事件文件描述符集合, allset用来暂存

    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    while(1){
        rset = allset;                                  //rset 一开始是用来监视listenfd套接字的, 备份

        nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if(nready < 0){
            perr_exit("select error");
        }
        if(FD_ISSET(listenfd, &rset)){                  //if listenfd had data, start response, listenfd满足监听的读事件
            cli_addr_len = sizeof(cli_addr);
            connfd = Accept(listenfd, (struct sockaddr*)&cli_addr, &cli_addr_len);  //建立连接(不会阻塞, 因为已经有了就绪事件)
            printf("received from %s at port %d\\n", 
            inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str)), 
            ntohs(cli_addr.sin_port));

            for(i=0;i<FD_SETSIZE;i++){
                if(client[i] < 0){                      //找client没有使用过的位置,感觉逻辑有问题,client数组默认值小于0?--- 所以要在前面把它设为-1
                    client[i] = connfd;                 //保存accept返回的文件描述符到client[]里面
                    break;
                }
            }
            if(i == FD_SETSIZE){                        //i 到达文件描述符上限 1024
                fputs("too many clients\\n", stderr);
                exit(1);
            }
            // 将connfd放进测试set里
            FD_SET(connfd, &allset);    

            if(connfd > maxfd){                 //修改maxfd
                maxfd = connfd;
            }

            if(i > maxi){                               //maxi用来存放最后一个文件描述符的下标
                maxi = i; 
            }

            if(--nready == 0){
                continue;       
            }
        }
        // 检测哪个文件描述符 有数据就绪
        for(i=0;i<=maxi;i++){    //这里必须设为小于等于,不然第一个创建的套接字下标为0,这个循环将不被执行,也就没法执行读写操作了。
            if((sockfd = client[i]) < 0){
                continue;
            }

            if(FD_ISSET(sockfd, &rset)){ //检查就绪
                if((n = Read(sockfd, buf, sizeof(buf))) == 0){ //检查到客户端关闭连接。 
                    Close(sockfd);  
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                }else if(n > 0){
                    for(j = 0;j < n;j++){
                        buf[j] = toupper(buf[j]);
                    }
                    Write(sockfd, buf, n);
                    Write(STDOUT_FILENO, buf, n);
                }

                if(--nready == 0){
                    break;
                }
            }
        }

    }
    Close(listenfd);
    return 0;
}

总结

使用select写的服务器和普通的多进程服务器有什么区别?

  • 多进程服务器

2aXF6U.png

  • 使用IO转接的服务器

步骤一:sever先创建lfd,再将这个转移给select函数调用内核去监听
2aXl6O.png

步骤二:当客户端连接后,有读写就绪事件时, select通知server调用accept去创建cfd套接字。

2aXgNn.png

select的缺点?

受监听上限文件描述符限制,最大1024个,当出现了5000多个客户端需要使用多线程
检测满足条件的fd, 自己添加业务逻辑提高效率, 增加了编码难度。否则就要轮询。

优点:

跨平台。win、linux、macos都支持select的多路IO转接

以上是关于select实现多路IO转接的主要内容,如果未能解决你的问题,请参考以下文章

select实现多路IO转接

select实现多路IO转接

select实现多路IO转接

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证