Linux:五种IO模型

一、五种IO模型

1.1 高效IO的初步理解

IO其实就是"input"和"output" 尤其在网络部分,IO的特性非常明显!

如果是在本地文件,本质上就是将数据写到内核文件缓冲区,具体什么时候刷到磁盘上,是由OS决定的!!而在网络中,本质上也是将数据写到发送缓冲区,但是具体什么时候发送,也是由OS决定的!!

所以应用层进行read或write的时候,本质上是把数据从用户层写给OS!这也是IO的本质!read和write函数的本质其实就是拷贝函数!

但是拷贝并不是一定能立马执行的!比如说read的时候,如果我的接收缓冲区没有数据,我得阻塞,而write的时候,我的发送缓冲区满了,那么我也得阻塞!!

所以要进行拷贝!必须要先判断读写事件是否就绪!

IO=等+拷贝

问题1:什么是读写事件呢??

------>你想读就得等读事件就绪,就是接收缓冲区有数据,想写就得等写事件就绪,就是等发送缓冲区要有足够多的空间,想读就是得读事件就绪,以上统称读写事件就绪!

问题2:什么是高效的IO呢??

------>任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往 往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是单位时间内等待时间的比重减少!

问题3:怎么理解等的比重减少呢?

------>比如说你当前是单进程,如果读写时间没有就绪就会阻塞住,只会等一个文件描述符,而如果是多线程,他可以等待多个文件描述符,此时的IO等待时间不是串型的而是并行的!

1.2 用"钓鱼"理解五种IO模型

接下来我们就要介绍五种IO模型,什么叫模型呢??其实就是规律,未来不管是读文件还是写文件都离不开其中一种!

钓鱼=等+钓(可以比喻IO)

1、张三(新手) 拿着自己的鱼漂(用来主动检测读写事件是否就绪) 鱼竿(相当于文件描述符) 鱼钩坐在椅子上,然后一下钩就死死盯着鱼漂, 鱼漂不动张三也不动,谁找他喊他他都不回应 直到鱼上钩 ----这是阻塞式IO(策略是在内核将数据准备好之前, 系统调用会一直等待所有的套接字, 默认都是阻塞方式.)

2、李四(有两三年钓鱼经验,坐不住)喊张三,张三不理他 他也就坐在那钓鱼了 但是他比较坐不住,他会每隔一段时间检查一下鱼漂,不会一直死死盯着,其他时间他会把视线转移到自己的手机上刷抖音,所以他检测的时候如果检测不到就会立刻做自己的事情 不会一直死盯 检测条件就绪了才钓鱼 ------这是非阻塞等待IO (策略是如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.)

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.

3、王五 (有五年钓鱼经验) 他看张三和李四一个一直动,一个一动不动,觉得他们是菜鸟,他也跟着钓鱼了,然后他在鱼竿上绑了一个铃铛 然后他就把鱼竿插起来不管了 直接躺在旁边玩手机 基本不关注鱼竿,直接等铃铛响 他才会去把鱼钓上来。 我们会发现张三和李四是主动去检测的 而王五的方式就是我不会主动检测,**就是鱼上钩了会自己通知我 ------**信号驱动式IO (策略是内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作. )

4、赵六(富豪、好胜) 所以他拉了一卡车的鱼竿 把所有的鱼竿都插起来 然后他会**来回走动检测(周期性遍历)**哪边有鱼上钩 ------这就是多路转接(策略最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态)

5、田七(世界首富 但是不是很专业) 司机开车带着他经过河边的时候,他发现河边有4个非常奇怪的人 钓鱼的姿势形态各异 于是他就很好奇 也想去钓鱼 然后突然公司打电话要开紧急会议 可是他又想吃鱼 于是他就把司机小王叫了过来 说我要去开会 你帮我钓鱼 等你钓满一桶了打电话给我 我再让人来接你
田七并不是喜欢钓鱼 他是钓鱼行为的发起者 他要的是鱼(数据) 田七这种方式叫做------异步IO (由内核在数据拷贝完成时, 通知应用程序)

因为小王在钓鱼的时候 他正在开会 此时的小王就相当于是OS 桶就相当于是一段缓冲区电话就相当于是一种通知方式 他将IO工作交给了OS 由OS自动去检测然后将数据放在缓冲区里 等缓冲区满了就通知你来取 田七在应用层用就可以了,田七并不参与具体的IO过程 而前四种方式就叫做同步IO

问题1:为什么赵六效率最高呢??拿到鱼竿多效率及高么??

------>假设你是一条鱼 你看到旁边这么多鱼竿 你会咬哪一个呢??显然赵六钓到鱼的机会最大,因为多个鱼竿可以让我们每一个等待的过程在时间上是并行重叠的!!所以整体上等的比重就减少了!!

问题2: 阻塞IOvs非阻塞IO

------>阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

在效率方面没有任何区别(因为IO=等+拷贝 大家的区别只是等的方式不同),我们一般说非阻塞效率会高一点不是IO效率高 而是他在非阻塞轮询的时候可以做其他的事情

问题3:王五有等吗??

------>王五也算一种等!!要不然他为什么不直接回家呢??就算我们说他没等,鱼咬钩的时候他也要参与钓鱼的过程(IO) 只要有参与IO,就一定有同步的过程,所以也是同步IO

问题4:同步IOVS 异步IO

------>同步IO就是有参与O的过程,而异步IO就只是发起IO,但是并不参与IO的过程,OS完成IO后会通知上层拿结果,然后直接用就行了!

问题5:同步通信vs异步通信

------> 同步和异步关注的是消息通信机制.

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;

异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

问题6:同步IOVS 线程同步

------>他俩就是老婆和老婆饼的关系(毫无关联!),同步IO是IO层面的概念,而线程同步是两个线程谁先谁后的问题!!所以以后在看到 "同步" 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步 与互斥的同步.

问题7:异步IO效率不高呢??为什么实际场景多路转接用的多?

------>田七再厉害也只有一套装备 而且异步IO写出来的服务逻辑比较混乱 所以现在已经有很多方法(比如协程)在逐步取代异步IO了 所以这里最值得我们学习的是多路转接和非阻塞!!

问题8:异步IOvs信号驱动

------>异步IO是由OS完成拷贝的过程然后通知上层,而信号驱动是告诉上层可以进行拷贝了

问题9:其他高级IO

------>非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.

二、非阻塞轮询

我们会发现以上接口有一个flag参数,我们可以通过设置来让该事件以非阻塞轮询的方式来访问套接字,但是这种方法太麻烦了!!

因为我们读写本质就是读写文件描述符指向的文件缓冲区,而文件描述符本质上****是下标,所以更通用的做法就是把文件描述符属性设置成非阻塞(其实就是他指向的文件对象struct file里面的一个标志位)告诉内核这个文件描述符我们要以非阻塞的方式来操作!

2.1 fcntl

一个文件描述符, 默认都是阻塞IO.

int fcntl(int fd, int cmd, ... /* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同. fcntl函数有5种功能:

复制一个现有的描述符(cmd=F_DUPFD).

获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).

获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).

获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

2.2 实现函数SetNoBlock

基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.

cpp 复制代码
void SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0)
    {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
    cout << " set " << fd << " nonblock done" << endl;
}

使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).

然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.

2.3 轮询方式读取标准输入

cpp 复制代码
int main()
{
    char buffer[1024];
    SetNonBlock(0);
    sleep(1);
    while (true)
    {
        // printf("Please Enter# ");
        // fflush(stdout);

        ssize_t n = read(0, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n - 1] = 0;
            cout << "echo : " << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "read done" << endl;
            break;
        }
        else
        {
            // 1. 设置成为非阻塞,如果底层fd数据没有就绪,recv/read/write/send, 返回值会以出错的形式返回
            // 2. a. 真的出错 b. 底层没有就绪
            // 3. 我怎么区分呢?通过errno区分!!!
            if (errno == EWOULDBLOCK)
            {
                cout << "0 fd data not ready, try again!" << endl;
                // do_other_thing();
                sleep(1);
            }
            else
            {
                cerr << "read error, n = " << n << "errno code: "
                     << errno << ", error str: " << strerror(errno) << endl;
            }
            // TODO 信号中断IO?
        }
    }

    return 0;
}

问题:如果将文件描述符设置为非阻塞了,如果底层fd数据没有就绪,recv/read/write/send,返回值会以出错(-1)的返回,为什么呢??

------>因为他实在没办法了!!>0表示成功,=0表示关闭,那么只能是<0了

所以此时<0有两种情况(1)真的出错了 (2)底层读写事件没有就绪

那我怎么区分呢??所以规定在返回-1的时候会设置错误码,我们可以通过错误码去判断!

因此一旦被设置为非阻塞了,那么返回-1情况在分类讨论的时候还需要根据错误码加一层判断,不能直接break

当然我们也可以写一个函数让他在轮询的期间去做点别的事情!

三、select-多路转接

以前我们学到的大多数接口是既等又IO,而现在我们可以用一个select专门用来等,并且他一次可以等待多个文件描述符,从而在等的时间上实现并行!!

3.1 select介绍

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

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;

程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

3.1.1 参数解释

nfds:是需要监视的最大的文件描述符值+1;

因为文件描述符是下标,所以可以理解为监听的文件描述符的范围。

rdset,wrset,exset:分别对应于需要检测的 可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;

输入输出型参数! 设置时表示要监听的文件描述符,返回时由内核设置,表示已经就绪的文件描述符

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

输入输出型参数! 比如等待时间是5s,如果2秒就有文件描述符就绪了,那么就会返回3秒

3.1.2 关于timeval结构体

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

关于取值:

NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;

0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。(第一个为单位s,第二个单位为ms)

3.1.3 关于fd_set结构体

这个结构是由内核提供的一种数据类型,其实就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符. (用来给用户和内核做沟通)

他是一个输入输出型参数!!

输入时,由用户告诉内核:我给你的一个或者多个fd,你要帮我关心上面的事件哦!如果就绪了你一定要告诉我哈!!

输出时,由内核告诉用户:你让我关心的多个fd中,有一些已经就绪了哦,用户你赶紧读取吧

所以使用select注定一定有大量位图操作!

用户:这个位图由我自己来操作吗??

OS说:你还是别直接操作了吧,你连他的结构都没搞清楚,还是让我来给你提供一批操作位图的接口吧!!所以提供了一组操作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的全部位

3.1.4 函数返回值

执行成功则返回文件描述词状态已改变的个数

如果返回0代表在描述词状态改变前已超过timeout时间,没有返回

当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的 值变成不可预测

错误值可能为:

EBADF 文件描述词为无效的或该文件已关闭

EINTR 此调用被信号所中断

EINVAL 参数n 为负值。

ENOMEM 核心内存不足

3.2 理解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=2,fd=1,则set变为0001,0011

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

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

3.3 socket就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;

socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;

监听的socket上有新的连接请求;

socket上有未处理的错误;

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;

socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;

socket使用非阻塞connect连接成功或失败之后;

socket上有未读取的错误;

异常就绪

socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段),

3.4 通过编码深入理解

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum
{
    SocketErr = 2,
    BindErr,
    ListenErr,
};

// TODO
const int backlog = 10;

class Sock
{
public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket()
    {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }
    void Bind(uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }
    void Listen()
    {
        if (listen(sockfd_, backlog) < 0)
        {
            lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if(newfd < 0)
        {
            lg(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }
    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if(n == -1) 
        {
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }
    void Close()
    {
        close(sockfd_);
    }
    int Fd()
    {
        return sockfd_;
    }

private:
    int sockfd_;
};

log.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    //              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     // va_list s;
    //     // va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     // va_end(s);

    //     // 格式:默认部分+自定义部分
    //     char logtxt[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暂时打印
    //     printLog(level, logtxt);
    // }
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

Log lg;

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }

Makefile:

cpp 复制代码
select_server:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f select_server

SelectServer.hpp:

cpp 复制代码
#pragma once

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

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port = defaultport) : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultfd;
            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listensock.Socket();
        _listensock.Bind(_port);
        _listensock.Listen();

        return true;
    }
    void Accepter()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
        if (sock < 0) return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // sock -> fd_array[]
        int pos = 1;
        for (; pos < fd_num_max; pos++) // 第二个循环
        {
            if (fd_array[pos] != defaultfd)
                continue;
            else
                break;
        }
        if (pos == fd_num_max)
        {
            lg(Warning, "server is full, close %d now!", sock);
            close(sock);
        }
        else
        {
            fd_array[pos] = sock;
            PrintFd();
            // TODO
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultfd; // 这里本质是从select中移除
        }
    }
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
        {
            int fd = fd_array[i];
            if (fd == defaultfd)
                continue;

            if (FD_ISSET(fd, &rfds))
            {
                if (fd == _listensock.Fd())
                {
                    Accepter(); // 连接管理器
                }
                else // non listenfd
                {
                    Recver(fd, i);
                }
            }
        }
    }
    void Start()
    {
        int listensock = _listensock.Fd();
        fd_array[0] = listensock;
        for (;;)
        {
            fd_set rfds;
            FD_ZERO(&rfds);

            int maxfd = fd_array[0];
            for (int i = 0; i < fd_num_max; i++) // 第一次循环
            {
                if (fd_array[i] == defaultfd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }

            // accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪

            // struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置
            struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置
            // 如果事件就绪,上层不处理,select会一直通知你!
            // select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
            // rfds: 输入输出型参数。 1111 1111 -> 0000 0000
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                // 有事件就绪了,TODO
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
                break;
            }
        }
    }
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_array[i] == defaultfd)
                continue;
            cout << fd_array[i] << " ";
        }
        cout << endl;
    }
    ~SelectServer()
    {
        _listensock.Close();
    }

private:
    Sock _listensock;
    uint16_t _port;
    int fd_array[fd_num_max];   // 数组, 用户维护的!
    // int wfd_array[fd_num_max];
};

Main.cc

cpp 复制代码
#include "SelectServer.hpp"
#include <memory>

int main()
{
    // std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();

    return 0;
}

注意事项:

1、不能直接aceept,因为他大部分时间都在等,一次只能等一个文件描述符!!(listensock上面的时间是新链接到来,就是三次握手完成,链接投递到全连接队列里,然后你再通过accept把链接从底层拿上来),所以新链接来了相当于是读事件就绪!!

2、 定义fd_set类型变量 如果是在栈上定义,可能会出现乱码,所以在使用前要记得先清空!!

3、因为timeout是输入输出型参数!!所以返回之后可能已经修改过了!!所以为了维持他的效果我们就必须周期性重复设置!

4、因为(1)rfds是一个输入输出型参数,每次都会被重新设置,且随着不断获取新链接,套接字的数量会越来越多!不能写死,应是动态计算 (2)select不仅仅要等lisentsock,也要等读的sock

因此需要有一个辅助数组arrry来监控select中的fd,他不仅可以方便我们(1)将文件描述符信息在不同函数之间的传递(2)用于在select 返回后,array作为源数据和fd_set进行FD_ISSET判断。。(3)select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数(4)让左侧是监听套接字,右侧是读套接字

5、辅助数组里有链接就绪和读就绪,我们怎么区分呢??------>确认就绪之后,再加一层判断。证明自己是否是监听套接字

6、关于Dispatcher(事件派发器),就是收到了多个就绪的文件描述符,然后跟array进行判断并派发,如果是连接就绪就交给连接事件处理,如果是读就绪就交给读事件处理。

因为就绪的时间不一定只有一个,所以必须要循环去遍历!

7、关于recver,读的时候不能直接读,因为读的时候内容可能不完整,这就涉及到了协议的内容!

3.5 select缺点

1、等待的fd是有上限的!

可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=4096.

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

2、输入输出型参数比较多,数据拷贝的频率很高,且每次都需要对关心的fd进行重置

3、用户层是,使用第三方数组管理用户的fd,用户层需要多次遍历,内核中检测fd时间就绪也要遍历。

相关推荐
白-胖-子5 分钟前
【生产实践】华为存储XSG1在RHEL 7.x/8.x上的多路径配置操作手册(生产环境)
运维·服务器·华为
Evenurs17 分钟前
【linux】mount命令中,data=writeback参数详细介绍
linux·运维·服务器
真正的醒悟19 分钟前
H3C-W2000-G2【透明代理模式】
网络
科技小E32 分钟前
仓储物流场景下国标GB28181视频平台EasyGBS视频实时监控系统应用解决方案
网络·人工智能
李少兄33 分钟前
深入解析计算机网络核心协议:ARP、DHCP、DNS与HTTP
网络·计算机网络·http·软考
qq_2430507933 分钟前
Siege:开源的 HTTP/FTP 压力测试与基准评估工具!全参数详细教程!Kali Linux教程!
linux·网络·web安全·网络安全·黑客·压力测试·kali linux
芊言芊语38 分钟前
端口 3389 服务 ms - wbt - server 漏洞修复方法
网络
冷冷清清中的风风火火38 分钟前
linux 通过命令将 MinIO 桶的权限设置为 Custom(自定义策略)
linux·服务器·ubuntu
默默无闻的白夜1 小时前
【Docker】存储卷
运维·docker·容器
佳腾_1 小时前
【运维_日常报错解决方案_docker系列】一、docker系统不起来
运维·docker·容器