多路转接(使用select实现)

1.select的作用

在多路转接类型的IO的过程中,select的作用只有一个,那就是等待。当等到fd就绪之后就会通知上层进行读取或者写入(本质是进行拷贝)。
select没有读取和写入数据的功能。
而对于read,write,recvfrom,send等函数,既有等待的功能也有拷贝的功能,它们的区别是只能等待一个fd,而select可以同时等待多个fd。
关于select的核心点有两点:
1.用户告知内核,你要帮助我关心哪些fd上的事件。
2.内核告知用户,哪些fd上的事件已经准备就绪了。

2.select函数原型

2.1函数构成

在这里插入图片描述

2.2参数

2.2.1 nfds

其中nfds表示的是select所等待的最大的文件描述符**+1**。

2.2.2 readfds等

其中readfds,writefds以及exceptfds的类型是fd_set,要理解它们就需要先了解fd_set这个类型。
fd_set是一个位图结构,它的数组下标表示的是文件描述符的编号。
对于select来说,他等待的结果有三种,分别是读就绪,写就绪以及异常就绪。因此分别对应的readfds,writefds,exceptfds。它们分别关心读,写于异常,我们使用读来举例。
readfds是一个输入输出型的参数:

1,输入:用户向内核输入,需要关心哪些文件描述符。
2.输出:内核向用户输出,哪些文件描述符已经就绪了。

由于fd_set是一个位图类型,所以用户只需要将需要关心的文件描述符置为1,并交给操作系统即可。而操作系统发现哪些文件描述符就绪了只需要将其对应的位图结构中的数据置为1。
假设fd_set有5个比特位,用户让操作系统关心0到4这5个文件描述符的读取。那么用户向select传入的readfds就是这样的:
在这里插入图片描述
如果这几个文件描述符中2好文件描述符有数据就绪了,操作系统就会把值置为这样:
在这里插入图片描述
它们的意义就在于,用户告诉内核哪些文件描述符需要关心,而内核告诉数据你关心的哪些文件描述符上的数据已经就绪。

2.2.3timeout

timeout表示的select等待文件描述符就绪的策略,等待分为阻塞等待和非阻塞等待。timeout是一个结构体,我们可以看一下这个结构体:
在这里插入图片描述
其中,第一个参数代表的是秒,第二个参数代表的是微秒。
当timeout设为NULL的时候表示的是阻塞等待:只要数据不就绪,那么select就不返回(注意select是代替read等函数来执行等待的策略的)。
当将timeout设为{0,0}时,表示的是非阻塞等待,数据不就绪,就发生返回。
当将timeout设为{a,b}时,表示的是阻塞等待a+b个时间,如果数据还没有就绪,则进入非阻塞等待,即直接返回。这种情况称为超时返回。

2.2.4返回值

返回值大于0,表示有几个文件描述符就绪了。返回值为-1表示的是出错。返回值等于0表示的是超时返回,对于阻塞返回来说,不存在返回值为0的情况,即没有超时返回。

2.3设置fd_set

操作系统不信任任何用户,只能使用一些系统调用接口来对fd_set来进行设置。
它提供了这样一批接口:

void FD_CLR(int fd,fd_set set);//用来清除词组set中相关fd的位。
int FD_ISSET(int fd,fd_set
set);//用来测试描述词组set中相关fd的位是否为真。
void FD_SET(int fd,fd_set* set);//用来设置描述词组set中相关fd的位。
void FD_ZERO(fd_set* set);//用来清除描述词组set的全部位。

2.4select的执行过程

1.假设执行fd_set set;set的大小是8,先使用FD_ZERO将其置为全0状态。
2.若fd=5,则执行FD_SET(fd,&set),此时fd变成0010,0000。
3.若再加入fd=2,fd=1,则set变为0010,0110。
4.执行select(6,&set,0,0,0)阻塞等待。
5.若fd=1,fd=2都发生可读事件,则select返回,此时set变为0000,0110。注意,没有事件就绪的fd=5被清空。

2.5一次select存在的问题

在select执行的过程中,直接就将set中的5清空了,那么当5就绪了怎么办呢?
select由于是输入输出型特性,导致后面每一次都要对set的值进行重置。
用户必须使用数组或者其他数据结构,来对历史的fd全部封装保存起来。
这也是select使用起来的麻烦之处。

3.select实现多路转接

3.1书写思路

在实现网络通信时,我们发现一共有两种套接字文件需要进行等待。一种是等待链接就绪,然后将链接拿上来;一种是等待数据就绪,然后将数据拿上来。当链接就绪并且拿上来之后,会新建立一个存放数据的套接字,还需要对该新的套接字进行数据等待。
以上这两种等待都需要交给select来进行,accept,recvfrom等函数就直接进行数据的拷贝了,不再需要进行等待的操作。
1.首先明确我们所写的代码一定是一个大循环,因为select的fd_set修改之后需要进行更新。同时我们需要建立一个数组arr来存放关心的文件套接字。并对该数组进行初始化,如果某一位置没有文件套接字,那么就设为-1。
2.从select的参数入手,第一个参数是最大fd+1,我们就可以先遍历数组找出最大的fd+1,传入;第二个参数是fd_set类型,我们上一次循环之后需要关心的套接字都被保存在arr中,因此可以通过arr来设置fd_set;第三个第四个参数代表写与异常,我们目前只考虑读,因此暂时不做处理。最后一个参数我们暂时设为0,即阻塞等待。
3.根据select的返回值来判断,它所监听的套接字已经有就绪的了。如果是-1表示等待出错,如果是0表示超时等待。
4.将套接字就绪为两种情况,当是listen_sock的套接字就绪,说明有链接到来了,可以直接进行accept,然后将accept返回的套接字存放在套接字数组中。即select下一次等待的时候需要关心该套接字了。如果数组已经满了说明服务器满载,则直接断开链接,关闭其套接字。
当是传输数据的套接字就绪的时候,直接进行数据传输即可。并根据recvfrom的返回值,可以分为三种情况,当大于0的时候,说明数据读取成功;当等于0的时候说明对端关闭了,此时服务端也需要关闭套接字,并将array的值设为-1,完成四次挥手。当小于0的时候读取失败。关闭套接字。并将array中对应值设为-1。

3.2Sock.hpp

这一部分代码为网络通信代码,在之前的TCP通信中已经做了详细解释,这里就不多进行赘述:

#include <iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
using namespace std;
namespace ns_Sock
{
    class Sock
    {
    private:
    public:
        static int Socket()
        {
            int sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                cerr << "创建套接字失败" << endl;
                exit(-2);
            }
            else
            {
                return sock;
            }
        }
        static int Listen(int sock)
        {
            if(listen(sock,5)<0)
            {
                cerr<<"listen error"<<endl;
                exit(-3);
            }
        }
        static int Accept(int sock)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int fd=accept(sock,(struct sockaddr*)&peer,&len);
            if(fd>=0)
            {
                return fd;
            }
            else
            {
                exit(-5);
            }
        }
        static void Bind(int sock,uint16_t port)
        {
            sockaddr_in local;
            local.sin_family=AF_INET;
            local.sin_port=htons(port);
            local.sin_addr.s_addr=INADDR_ANY;
            if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
            {
                cerr<<"bind error"<<endl;
                exit(-4);
            }
        }
        static void Connect(int sock,string ip,uint16_t port)
        {
            struct sockaddr_in server;
            memset(&server,0,sizeof(server));
            server.sin_family=AF_INET;
            server.sin_port=htons(port);
            server.sin_addr.s_addr=inet_addr(ip.c_str());
            if(connect(sock,(struct sockaddr*)&server,sizeof(server))==0)
            {
                cout<<"connect success!"<<endl;
            }
            else
            {
                cout<<"connect fail"<<endl;
                exit(-5);
            }
        }
    };
}

3.3多路转接代码

#include "Sock.hpp"
using namespace ns_Sock;
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM]; //全局,如果是-1则没有fd
void Usage(char *proc)
{
    cout << proc << "Port" << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(-1);
    }
    uint16_t port = (uint16_t)atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    for (int i = 0; i < NUM; i++)
    {
        fd_array[i] = -1; //将存放fd的数组进行初始化
    }
    fd_array[0] = listen_sock;
    int max_fd = fd_array[0];
    fd_set rfds;
    for (;;)
    {
        FD_ZERO(&rfds);
        //找出最大的fd作为select的参数
        for (int i = 0; i < NUM; i++)
        {
            if (fd_array[i] == -1)
            {
                continue; //说明不是用户关心的文件描述符
            }
            FD_SET(fd_array[i], &rfds); //要关心的fd添加到rfds中
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i]; //更新最大的fd
            }
        }
        struct timeval timeout = {5, 0};                              //设为5s超时
        int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //进行阻塞等待

        switch (n)
        {
        case -1:
            cerr << "select error" << endl;
            break;
        case 0:
            cout << "select timeout" << endl;
            break;
        default:
            cout << "有fd对应的事件就绪了" << endl;
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == -1)
                {
                    continue; //下面的是合法的但不一定就绪
                }
                if (FD_ISSET(fd_array[i], &rfds)) //判读文件描述符是否被设置了
                {
                    if (fd_array[i] == listen_sock)
                    {
                        cout << "有新链接链接上来" << endl;
                        int sock = Sock::Accept(listen_sock);
                        if (sock >= 0)
                        {
                            cout << "新链接创建成功" << endl;
                            int pos = 1;
                            for (pos = 1; pos < NUM; pos++)
                            {
                                if (fd_array[pos] == -1)
                                {
                                    break;
                                }
                            }
                            //找到了一个位置还没有被使用
                            if (pos < NUM)
                            {
                                cout << "新链接" << sock << "已经被填到了数组[" << pos << "]的位置" << endl;
                                fd_array[pos] = sock;
                            }
                            else
                            {
                                cout << "服务器满载了" << endl;
                                close(sock);
                            }
                        }
                    }
                    //普通文件描述符
                    else
                    {
                        cout<<"sock:"<<fd_array[i]<<"上面有普通的读取"<<endl;
                        char recv_buffer[1024]={0};
                        ssize_t s=recv(fd_array[i],recv_buffer,sizeof(recv_buffer)-1,0);//虽然这里设为1但是不会发生阻塞,因为select已经阻塞完了
                        if(s>0)
                        {
                            recv_buffer[s]='\0';
                            cout<<"client["<<fd_array[i]<<"]#"<<recv_buffer<<endl;
                        }
                        else if(s==0)//说明对端关闭了
                        {
                            cout<<"sock"<<fd_array[i]<<"客户端关闭了"<<endl;
                            close(fd_array[i]);
                            fd_array[i]=-1;
                            cout<<"已经在fd_array["<<i<<"]中去掉了"<<fd_array[i]<<endl;
                        }
                        else
                        { 
                            //读取失败
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"<< "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                    }
                }
            }
            break;
        }
    }
    return 0;
}

4.select实现多路转接优缺点

优点:与多线程多进程对比,可以一个进程一次等待多个fd,一定程度上提高了IO的效率。
缺点:1.每一次都需要重新对fd_set进行设置,需要遍历检测。
2.fd_set它能够让select同时检测的fd是有上限的。
3.select底层需要轮询式间额哪些fd已经就绪了。
4.select可能会较为高频率进行用户态和内核态切换,降低性能。

posted @ 2022-09-24 20:28  卖寂寞的小男孩  阅读(45)  评论(0)    收藏  举报