高级IO -- 多路转接之 select

高级IO – 多路转接之 select

初识select

系统提供select函数来实现多路复用 输入/输出 模型

  • select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在 select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化。

select 函数原型

select 的函数原型如下:

#include <sys/select.h> 
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);

参数解释:

nfds:

select 是用来等待多个文件描述符变化的,多个文件描述符在底层是用数组来保存的。

因此select要有权力知道当前文件描述符的,因此第一个参数为最大文件描述符+1。

为什么是maxfd+1

​ 因为多个文件描述符是通过数组传来的,而文件描述符和数组的下标是一一对应的,都是从0开始的,因此我们需要让select知道当前管理的文件描述符的范围,因此传入最大的文件描述符+1。这里传入+1,是在循环的时候保证左闭右开罢了。

readfds/writefds/exceptfds :

这三个参数分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。

在此使用readfds举例:

readfds是一个输入输出型参数:

  • 输入:用户告诉操作系统,你要帮我关心一下所设置的多个文件描述中读事件是否就绪,如果读事件就绪就立马告诉我。
  • 输出:内核告诉用户,用户曾经让我关心的文件描述符有哪些已经就绪了

关于fd_set结构

fd_set:其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.

  • 比特位的位置代表fd的编号
  • 比特位的内容代表 ‘是否’ 的概念,其中 1表示是,0表示否。

提供了一组操作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的全部位

那么这里就会存在一个问题:输入和输出用的是同一张位图吗?

答案:是一张位图,只不过需要重新添加,再次设置。因为同一张位图是覆盖式的,每次重新使用都需要重新设置 [在下面select的特点中有提到]

timeout: 的结构为 timeval,是用来设置select()的等待时间的

  • NULL:则表示select()没有timeoutselect将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

函数返回值:

  • 执行成功则返回文件描述词状态已经改变的个数。
  • 返回0代表在描述词状态改变前已超过timeout时间,没有返回。
  • 当有错误发生时返回-1。错误原因存于 errno,此时参数readfdswritefds, exceptfdstimeout的值变成不可预测。

select使用示例

编写Sock

基本的tcp套接字代码,这里做了封装,不再赘述。

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cerrno>
#include <cassert>

class Sock
{
public:

    static const int gbacklog = 20;

    static int Socket()
    {
        int listenSock = socket(PF_INET,SOCK_STREAM,0);
        if(listenSock < 0){
            exit(1);
        }
        int opt = 1;
        setsockopt(listenSock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
        return listenSock;
    }

    static void Bind(int socket,uint16_t port)
    {
        struct sockaddr_in local;//用户栈
        memset(&local,0,sizeof(local));
        local.sin_family = PF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        
        if(bind(socket,(const struct sockaddr *)&local,sizeof(local))<0)
        {
            exit(2);
        }
    }

    static void Listen(int socket)
    {
        if(listen(socket,gbacklog) < 0)
        {
            exit(3);
        }
    }

    static int Accept(int socket,std::string * clientip,uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        int serviceSock = accept(socket,(struct sockaddr*)&peer,&len);
        if(serviceSock <0){
            return -1;//获取链接失败
        }
        if(clientport) *clientport = ntohs(peer.sin_port);
        if(clientip) *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

编写selectService

#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"

int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存历史上所有的合法fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);

#define DFL -1

using namespace std;

static void showArray(int arr[], int num)
{
    cout << " 当前合法sock list# ";
    for (int i = 0; i < num; i++)
    {
        if (arr[i] == DFL)
            continue;
        else
            cout << arr[i] << " ";
    }
    cout << endl;
}

static void HandlerEvent(int listensock, fd_set &readfds)
{
    for (int i = 0; i < gnum; i++)
    {
        if (fdsArray[i] == DFL)
            continue;
        if (i == 0 && fdsArray[i] == listensock)
        {
            if (FD_ISSET(listensock, &readfds))
            {
                cout << "已经有一个新连接到来了,需要进行获取了" << endl;
                string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(listensock, &clientip, &clientport);
                if (sock < 0)
                    return;
                cout << "获取新连接成功: " << clientip << " : " << clientport << " | sock: " << sock << endl;

                // read/write -- 不能 因为read不知道底层数据是否就绪 select知道
                // 需要将fd托管给select
                int i = 0;
                for (; i < gnum; i++)
                {
                    if (fdsArray[i] == DFL)
                        break;
                }
                if (i == gnum)
                {
                    cerr << "我的服务器已经到达最大上限了,无法承载更多同时保持的链接了" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i] = sock;        // 将sock添加到select中,进行进一步的监听就绪事件了
                    showArray(fdsArray, gnum); // 打印
                }
            }
        }
        else
        {
            // 处理普通sock的IO事件
            if (FD_ISSET(fdsArray[i], &readfds))
            {
                char buffer[1024];
                ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0); // 不会阻塞
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[ " << fdsArray[i] << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << " client[ " << fdsArray[i] << "] quit,server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL;
                    showArray(fdsArray, gnum);
                }
                else
                {
                    cout << " client[ " << fdsArray[i] << "] error,server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL;
                    showArray(fdsArray, gnum);
                }
            }
        }
    }
}

static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}

// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);

    // 初始化全为-1 表示均可用
    for (int i = 0; i < gnum; i++)
    {
        fdsArray[i] = DFL;
    }
    fdsArray[0] = listensock;
    while (true)
    {
        // 在每次进行select的时候进行参数的重新设定
        int maxFd = DFL;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < gnum; i++)
        {
            if (fdsArray[i] == DFL)
                continue;                  // 1.过滤掉不合法的fd
            FD_SET(fdsArray[i], &readfds); // 2.添加所有的合法fd到readfds中,方便select统一进行就绪监听
            if (maxFd < fdsArray[i])
            {
                maxFd = fdsArray[i]; // 3.更新maxFd
            }
        }

        struct timeval timeout = {100, 0};
        int n = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);
        switch (n)
        {
        case 0:
            cout << "time out .... " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listensock, readfds);
            break;
        }
    }

    return 0;
}

测试

测试

理解select执行过程

理解select模型的关键在于理解fd_set,为方便说明,取fd_set长度为1字节,fd_set中的每一个bit可以对应一个文件描述符fd,则1字节长的fd_set最大可以对应8个fd

(1)执行fd_set set; FD_ZERO(&set);则set用位图表示是0000,0000

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2fd=1,则set变为0001,0011

(4)执行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件 描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 每bit表示一个文件描述符,假如一个服务器上sizeof(fd_set) = 512 则该服务器上支持的最大文件描述符是512*8=4096.
  • fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd
    • 一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
    • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注: fd_set的大小可以调整,可能涉及到重新编译内核…

select的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

备注: fd_set的大小可以调整,可能涉及到重新编译内核…

select的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

(本篇完)

猜你喜欢

转载自blog.csdn.net/qq_58325487/article/details/131115078