Linux学习记录——사십일 高级IO(2)--- Select型服务器


1、思路

select就是多路转接IO。select能以某种形式,等待多个文件描述符,只要有哪个fd有数据就可以读取并全部返回。就绪的fd,要让用户知道。select等待的多个fd中,一定有少量或者全部都准备好了数据。

2、select接口

在这里插入图片描述

nfds输入型参数,表示select等待的多个fd中,fd对应的数 + 1

剩下四个参数都是输入输出型参数,意思是用户传入参数,操作系统通过这些参数把结果交给用户。

除去timeout,中间三个是同一个类型的,分别对应读、写、异常,只是用处不同,但思路相同。timeout的类型是一个结构体,表示select应该以什么方式来轮询检测,一共有3种值。NULL/nullptr表示阻塞等待,等待多个fd的时候就是把文件结构体的指针放到阻塞队列中,如果没有就绪的就不返回;{0,0}表示非阻塞等待,也就是等待多个文件描述符时,如果没有就绪的,就等待0s后出错返回;{n,m}表示n秒以内阻塞等待,超过这个时间就timeout一次,也就是非阻塞一次,也就是出错返回,如果n秒内得到了数据并返回,那么timeout此时就表示剩余时间。

返回值是一个int值,大于0表示有几个fd是就绪的,为0表示超时出错返回了,小于0表示等待失败。

readfd参数的类型是fd_set类型,是一个位图结构

在这里插入图片描述

用位图结构来表示多个fd,进行用户和内核之间的信息的互相传递。对于位图结构的操作只能用系统给定的接口。

在这里插入图片描述

FD_CLR把指定的描述符从指定的集合中清除;FD_ISSET查看指定fd是否在集合中;FD_SET添加fd到集合中;FD_ZERO将集合清空。

通过这个位图结构,用户告诉内核,用户关心哪些fd对应的文件有数据,内核要告诉用户,哪些fd的读事件就绪了。

假设用户这样设置,fd_set rfds:0110 111,表明文件描述符为4和7的不管,看fd为012356的文件;如果只有3号fd就绪了,内核传回来的rfds则是0000 1000;用户拿到rfds后,扫描位图,哪个位置为1就说明是哪个文件就绪了。

fd_set是OS提供的固定大小的数据类型,比特位数有上限,所以select能管理的fd也有上限。查看多大

#include <iostream>
#include <sys/select.h>

int main()
{
    fd_set fd;
    std::cout << sizeof(fd) << std::endl;
    return 0;
}

字节数就是sizeof(fd) * 8。

3、实现

1、准备工作

makefile

selectserver:main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f selectserver

main.cc

#include "SelectServer.hpp"
#include <memory>

int main()
{
    //fd_set fd;
    //std::cout << sizeof(fd) << std::endl;
    std::unique_ptr<SelectServer> svr(new SelectServer(3389));
    svr->InitServer();
    svr->Start();
    return 0;
}

SelectServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>

class SelectServer
{
public:
    SelectServer(uint16_t port): port_(port)
    {}

    void InitServer()
    {

    }

    void Start()
    {

    }
    
    ~SelectServer()
    {}
private:
    uint16_t port_;
};

引入之前的Sock.hpp和log.hpp和err.hpp

Sock.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "err.hpp"
#include "log.hpp"

static const int gbacklog = 32;
static const int defaultfd = -1;

class Sock
{
public:
    Sock(): _sock(defaultfd)
    {}

    void Socket()
    {
        _sock= socket(AF_INET, SOCK_STREAM, 0);
        if(_sock < 0)
        {
            logMessage(Fatal, "socket error, code: %d, errstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        //设置地址是可复用的
        int opt = 1;
        setsockopt(_sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
    }

    void Bind(const 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(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
    }

    void Listen()
    {
        if(listen(_sock, gbacklog) < 0)//第二个参数维护了一个队列,发送了连接请求但是服务端没有处理的客户端,服务端开始accept后,就会出现另一个队列,就是服务端接受了请求但还没被accept的客户端
        {
            logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }

    int Accept(std::string* clientip, uint16_t* clientport)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int sock = accept(_sock, (struct sockaddr*)&temp, &len);
        if(sock < 0)
        {
            logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
        }
        else
        {
            *clientip = inet_ntoa(temp.sin_addr);//这个函数就可以从结构体中拿出ip地址,转换好后返回
            *clientport = ntohs(temp.sin_port);
        }
        return sock;
    }

    int Connect(const std::string& serverip, const uint16_t& serverport)//让别的客户端来连接服务端
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());
        return connect(_sock, (struct sockaddr*)&server, sizeof(server));//先不打印消息
    }

    int Fd()
    {
        return _sock;
    }

    void Close()
    {
        if(_sock != defaultfd) close(_sock);
    }

    ~Sock()
    {}
private:
    int _sock;
};

err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    SETSID_ERR,
    OPEN_ERR
};

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

const std::string filename0 = "log/tcpserver.log.Debug";
const std::string filename1 = "log/tcpserver.log.Info";
const std::string filename2 = "log/tcpserver.log.Warning";
const std::string filename3 = "log/tcpserver.log.Error";
const std::string filename4 = "log/tcpserver.log.Fatal";
const std::string filename5 = "log/tcpserver.log.Unknown";


enum
{
    Debug = 0,//调试信息
    Info,//正常信息
    Warning,//告警,不影响运行
    Error,//一般错误
    Fatal,//严重错误
    Unknown
};

static std::string toLevelString(int level, std::string& filename)
{
    switch(level)
    {
    case Debug:
        filename = filename0;
        return "Debug";
    case Info:
        filename = filename1;
        return "Info";
    case Warning:
        filename = filename2;
        return "Warning";
    case Error:
        filename = filename3;
        return "Error";
    case Fatal:
        filename = filename4;
        return "Fatal";
    default:
        filename = filename5;
        return "Unknown";
    }
}

static std::string getTime()
{
    time_t curr = time(nullptr);//拿到当前时间
    struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday, \
        tmp->tm_hour, tmp->tm_min, tmp->tm_sec);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900
    return buffer;
}

//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()以%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{
    //写入到两个缓冲区中
    char logLeft[1024];//用来显示日志等级,时间,pid
    std::string filename;
    std::string level_string = toLevelString(level, filename);
    std::string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
    char logRight[1024];//用来显示消息体
    va_list p;
    va_start(p, format);
    //直接用这个接口来对format进行操作,提取信息
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);
    //打印
    printf("%s%s\n", logLeft, logRight);
    //format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数
    //保存到文件中
    FILE* fp = fopen(filename.c_str(), "a");
    if(fp == nullptr) return ;
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);
    fclose(fp);
    //va_list p;//char*
    //下面是三个宏函数
    //int a = va_arg(p, int);//根据类型提取参数
    //va_start(p, format);//让p指向可变参数部分的起始地址
    //va_end(p);//把p置为空, p = NULL
}

2、实现等待多个fd

#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"

class SelectServer
{
public:
    SelectServer(uint16_t port): port_(port)
    {}

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
    }

    void Start()
    {
        listensock_.Accept();
    }

    ~SelectServer()
    {}
private:
    uint16_t port_;
    Sock listensock_;
};

最一开始,服务器只有一个socket,就是上面这些代码创建出来的。如果没创建,服务器根本就没有socket,那么这时候Accept也没有检测到链接,只能阻塞着。所以不能直接Accept,要先有链接。在网络中,新链接当作读事件来就绪。我们先添加套接字到select中再处理。

#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include <cstring>
#include "Sock.hpp"
#include "log.hpp"
#include "err.hpp"

class SelectServer
{
public:
    SelectServer(uint16_t port): port_(port)
    {}

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
    }

    void Start()
    {
        fd_set rfds;
        FD_ZERO(&rfds); // 先清空,即使是新的rfds
        FD_SET(listensock_.Fd(), &rfds);
        while (true)
        {
        	struct timeval timeout = {2, 0};//timeout是输出型参数,每一次都需要重新设置,不然后续都会变成0而直接打印第一条语句
            int n = select(listensock_.Fd() + 1, &rfds, nullptr, nullptr, &timeout);
            switch (n)
            {
            case 0:
                logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
                break;
            case -1:
                logMessage(Warning, "%d: %s", errno, strerror(errno));
                break;
            default:
                logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
                break;
            }
        }
    }

    ~SelectServer()
    {
        listensock_.Close();
    }
private:
    uint16_t port_;
    Sock listensock_;
};

事件现在已经可以检测到是否就绪了。

    void HandlerEvent(fd_set& rfds)
    {
        if(FD_ISSET(listensock_.Fd(), &rfds))
        {
            std::cout << "有一个新连接到了" << std::endl;
            //进行Accept
        }
    }

            default:
                logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
                HandlerEvent(rfds);//rfds已经设置好了
                break;

然后完善处理

    void HandlerEvent(fd_set& rfds)
    {
        if(FD_ISSET(listensock_.Fd(), &rfds))
        {
            std::cout << "有一个新连接到了" << std::endl;
            //进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
            std::string clientip;
            uint16_t clientport;
            int sock = listensock_.Accept(&clientip, &clientport);//获取连接
            if(sock < 0) return ;
            //到这里已得到了新连接对应的fd,但不能直接读取数据
            //将sock添加到select的rfds中,让select来管理
            //打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
            logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
            //select对fd要有持续监听能力,不能有一个就绪其它全部重置
        }
    }

获取连接代表新增了一个fd,可以用套接字来进行IO服务,但还不知道数据是否就绪,只知道有连接。客户端发送请求,和我们的服务端建立连接后,客户端可以不发数据,这时调用read接口就会阻塞在这里。

获得的sock不能直接添加到rfds,以后越来越多的sock都要处理,时间耗费长,且有个前提,应当处理用户关心的fd。无脑设置进去,当我们关心的fd就绪后,其它都会清0,只有那个fd的位置会是1,所以也无用,所以select对fd要有持续监听能力,不能有一个就绪其它全部清零。

这里的思路其实也不难,要持续监听,我们就维护一个数组保护所有获得连接的sock就行。

const static int gport = 8888;
typedef int type_t;

class SelectServer
{
    static const int N = (sizeof(fd_set) * 8);
public:
    SelectServer(uint16_t port = gport) : port_(port)
    {
    }
//...
private:
    uint16_t port_;
    Sock listensock_;
    type_t fdarray_[N];

初始化

static const int defaultfd = -1;//在之前的Sock.hpp中就已经有了这个全局变量,所以SelectServer.hpp中可以不设置这个,不过Sock.hpp中不要加static,这样两个文件都可以用

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for(int i = 0; i < N; i++) fdarray_[i] = defaultfd;
    }

Start

    void Start()
    {
        fdarray_[0] = listensock_.Fd();
        while (true)
        {
            struct timeval timeout = {2, 0};
            //rfds是输入输出参数,所以rfds每次都要重置
            //服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd
            //所以rfds相关操作要在循环里做
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = fdarray_[0];//用max_fd来表示fd中的最大值,看select接口那里的说明
            for(int i = 0; i < N; i++)
            {
                if(fdarray_[i] == defaultfd) continue;
                //合法fd
                FD_SET(fdarray_[i], &rfds);
                if(maxfd < fdarray_[i]) maxfd = fdarray_[i];
            }
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
            switch (n)
            {
            case 0:
                logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
                break;
            case -1:
                logMessage(Warning, "%d: %s", errno, strerror(errno));
                break;
            default:
                logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
                HandlerEvent(rfds);//rfds已经设置好了
                break;
            }
        }
    }

接下来就把sock放到fdarray_中。

    void HandlerEvent(fd_set& rfds)
    {
        if(FD_ISSET(listensock_.Fd(), &rfds))
        {
            std::cout << "有一个新连接到了" << std::endl;
            //进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
            std::string clientip;
            uint16_t clientport;
            int sock = listensock_.Accept(&clientip, &clientport);//获取连接
            if(sock < 0) return ;
            //到这里已得到了新连接对应的fd,但不能直接读取数据
            //将sock添加到select的rfds中,让select来管理
            //打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
            logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
            //select对fd要有持续监听能力,不能有一个就绪其它全部重置

            //让select来管理,把sock添加到fdarray_[]中
            int pos = 1;
            for( ; pos < N; pos++)
            {
                if(fdarray_[pos] == defaultfd) break;
            }
            if(pos >= N)
            {
                close(sock);//说明不在工作范围内,数组已经满了,那就关闭这个连接
                logMessage(Warning, "sockfd array[] full");
            }
            else fdarray_[pos] = sock;
        }

//...
            default:
                logMessage(Debug, "有一个就绪事件发生了");//这时候就说明至少有一个fd就绪了
                HandlerEvent(rfds);//rfds已经设置好了
                DebugPrint();
                break;
            }
        }
    }

    void DebugPrint()
    {
        std::cout << "fdarray[]: ";
        for(int i = 0; i < N; ++i)
        {
            if(fdarray_[i] == defaultfd) continue;
            std::cout << fdarray_[i] << " ";
        }
        std::cout << "\n";
    }

以上的代码就已经能够做到等到多个fd并管理好它们了。

3、辨别连接和简单处理读事件

能够处理很多个连接,也都能把它们一次次的设置进集合里,但我们需要有条件地接收,要有目的地去接收。
先改一下形式

    void Accepter()
    {
        std::cout << "有一个新连接到了" << std::endl;
        //进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
        std::string clientip;
        uint16_t clientport;
        int sock = listensock_.Accept(&clientip, &clientport);//获取连接
        if(sock < 0) return ;
        //到这里已得到了新连接对应的fd,但不能直接读取数据
        //将sock添加到select的rfds中,让select来管理
        //打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
        logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
        //select对fd要有持续监听能力,不能有一个就绪其它全部重置

        // 让select来管理,把sock添加到fdarray_[]中
        int pos = 1;
        for (; pos < N; pos++)
        {
            if (fdarray_[pos] == defaultfd)
                break;
        }
        if (pos >= N)
        {
            close(sock); // 说明不在工作范围内,数组已经满了,那就关闭这个连接
            logMessage(Warning, "sockfd array[] full");
        }
        else
            fdarray_[pos] = sock;
    }

    void HandlerEvent(fd_set &rfds)
    {
        if (FD_ISSET(listensock_.Fd(), &rfds))
        {
            Accepter();
        }
    }

HandlerEvent除了放入连接,还得处理一下连接。

    void Recver(int index)
    {
        int fd = fdarray_[index];
        // 用户不关心的fd就绪了
        char buffer[1024];
        // 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的
        // 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完
        ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s - 1] = 0;
            std::cout << "client# " << buffer << std::endl;
            // 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满
            std::string echo = buffer;
            echo += " [select server echo]";
            send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式
        }
        else // 读完或者出错
        {
            if (s == 0)
                logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);
            else
                logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[i], defaultfd);
            // 清空当前这个连接,关闭fd
            close(fdarray_[index]);
            fdarray_[index] = defaultfd;
        }
    }

    void HandlerEvent(fd_set &rfds)
    {
        //这个函数并不知道rfds的哪些fd就绪了,所以得循环检测
        for(int i = 0; i < N; i++)
        {
            if(fdarray_[i] == defaultfd) continue;
            //就绪的fd属于用户关心的,且它存在与rfds集合中
            if ((fdarray_[i] == listensock_.Fd()) && FD_ISSET(listensock_.Fd(), &rfds))
            {
                Accepter();
            }
            else if((fdarray_[i] != listensock_.Fd()) &&FD_ISSET(fdarray_[i], &rfds))
            {
                Recver(i);
            }
        }
    }

4、简单处理写、读事件

为了方便,先建立一个结构体

#define READ_EVENT (0X1)
#define WRITE_EVENT (0X1<<1)
#define EXCEPT_EVENT (0X1<<2)

typedef struct FdEvent
{
    int fd;
    uint8_t event;
    std::string clientip;
    uint16_t clientport;
}type_t;

static const int defaultevent = 0;

这样fdarray_这个数组就变成结构体类型的数组了。代码很多地方也要改一下,比如原本fdarray_[i]改成fdarray_[i].fd。

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        for (int i = 0; i < N; i++)//初始化一下
        {
            fdarray_[i].fd = defaultfd;
            fdarray_[i].event = defaultevent;
            fdarray_[i].clientport = 0;
        }
    }

    void Start()
    {
        fdarray_[0].fd = listensock_.Fd();
        fdarray_[0].event = READ_EVENT;
        while (true)
        {
            struct timeval timeout = {2, 0};
            // rfds是输入输出参数,所以rfds每次都要重置
            // 服务器在运行中,套接字对应的fd的值一直在动态变化,所以select中第一个参数的值也得变化,否则无法照顾到其它fd
            // 所以rfds相关操作要在循环里做
            fd_set rfds;
            fd_set wfds;
            FD_ZERO(&rfds);
            FD_ZERO(&wfds);
            int maxfd = fdarray_[0].fd; // 用max_fd来表示fd中的最大值,看select接口那里的说明
            for (int i = 0; i < N; i++)
            {
                if (fdarray_[i].fd == defaultfd)
                    continue;
                // 合法fd
                if(fdarray_[i].event & READ_EVENT) FD_SET(fdarray_[i].fd, &rfds);
                if(fdarray_[i].event & WRITE_EVENT) FD_SET(fdarray_[i].fd, &wfds);
                if (maxfd < fdarray_[i].fd)
                    maxfd = fdarray_[i].fd;
            }
            int n = select(maxfd + 1, &rfds, &wfds, nullptr, &timeout);
            switch (n)
            {
            case 0:
                logMessage(Debug, "timeout, %d: %s", errno, strerror(errno));
                break;
            case -1:
                logMessage(Warning, "%d: %s", errno, strerror(errno));
                break;
            default:
                logMessage(Debug, "有一个就绪事件发生了"); // 这时候就说明至少有一个fd就绪了
                HandlerEvent(rfds, wfds);                  
                DebugPrint();
                break;
            }
        }
    }

    void DebugPrint()
    {
        std::cout << "fdarray[]: ";
        for (int i = 0; i < N; ++i)
        {
            if (fdarray_[i].fd == defaultfd)
                continue;
            std::cout << fdarray_[i].fd << " ";
        }
        std::cout << "\n";
    }

上面加入写事件,虽然我们还是关心读事件,但是写事件也要管理。以及处理用的函数部分

    void Accepter()
    {
        std::cout << "有一个新连接到了" << std::endl;
        //进行Accept,这时候不会阻塞,因为走到这里就说明已经有连接了
        std::string clientip;
        uint16_t clientport;
        int sock = listensock_.Accept(&clientip, &clientport);//获取连接
        if(sock < 0) return ;
        //到这里已得到了新连接对应的fd,但不能直接读取数据
        //将sock添加到select的rfds中,让select来管理
        //打印出来的sock就是可用的fd,使用多个客户端连接时,clientport会变,说明连接成功,sock就是当前可用的fd
        logMessage(Debug, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
        //select对fd要有持续监听能力,不能有一个就绪其它全部重置

        // 让select来管理,把sock添加到fdarray_[]中
        int pos = 1;
        for (; pos < N; pos++)
        {
            if (fdarray_[pos].fd == defaultfd)
                break;
        }
        if (pos >= N)
        {
            close(sock); // 说明不在工作范围内,数组已经满了,那就关闭这个连接
            logMessage(Warning, "sockfd array[] full");
        }
        else
        {
            fdarray_[pos].fd = sock;
            //fdarray_[pos].event = (READ_EVENT | WRITE_EVENT);//读写都关心,如果只关心一个,那就写一个
            fdarray_[pos].event = READ_EVENT;
            fdarray_[pos].clientip = clientip;
            fdarray_[pos].clientport = clientport ;
        }
    }

    void Recver(int index)
    {
        int fd = fdarray_[index].fd;
        // 用户不关心的fd就绪了
        char buffer[1024];
        // 读取不会被阻塞,因为经过上面的筛选,select已经确定这个fd是已经有数据了的
        // 不过只有这一次不会阻塞,之后不确定,因为一次不保证数据全部读完
        ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s - 1] = 0;
            std::cout << fdarray_[index].clientip << ":" << fdarray_[index].clientport << "# " <<buffer << std::endl;
            // 写一下读了再发回去,发回去也需要select管理,因为不知道自己发送条件是否就绪,比如自己的发送缓冲区是否满
            std::string echo = buffer;
            echo += " [select server echo]";
            send(fd, echo.c_str(), echo.size(), 0); // 先用阻塞式
        }
        else // 读完或者出错
        {
            if (s == 0)
                logMessage(Info, "client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);
            else
                logMessage(Warning, "recv error, client quit..., fdarray_[i] -> defaultfd: %d->%d", fdarray_[index], defaultfd);
            // 清空当前这个连接,关闭fd
            close(fdarray_[index].fd);
            fdarray_[index].fd = defaultfd;
            fdarray_[index].event = defaultevent;
            fdarray_[index].clientip.resize(0);
            fdarray_[index].clientport = 0;
        }
    }

    void HandlerEvent(fd_set &rfds, fd_set &wfds)
    {
        //这个函数并不知道rfds的哪些fd就绪了,所以得循环检测
        for (int i = 0; i < N; i++)
        {
            if (fdarray_[i].fd == defaultfd)  continue;
            // 关心读事件且读事件已经就绪
            if ((fdarray_[i].event & READ_EVENT) && FD_ISSET(fdarray_[i].fd, &rfds))
            {
                if (fdarray_[i].fd == listensock_.Fd())
                {
                    Accepter();
                }
                else if (fdarray_[i].fd != listensock_.Fd())
                {
                    Recver(i);
                }
                else {}
            }
            // 关心写事件且读事件已经就绪
            else if ((fdarray_[i].event & WRITE_EVENT) && FD_ISSET(fdarray_[i].fd, &wfds))
            {}
            else {}
        }
    }

4、特点

可监控,能关心的fd个数有上限;将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集合来告知系统用户关心哪些fd,不方便,且要把fd集合从用户态拷贝到内核态,每次都这样,开销不算小。以及在代码中可以发现,操作系统底层不知道fd对应的文件里是否有数据,它就得遍历用户关心的全部fd,而用户层也要遍历,系统在遍历时,如果没有fd就绪且是非阻塞,就直接返回了,如果有timeout,那就过了时间还没有就绪再返回,如果是阻塞的,那么没有就绪的就挂起全部进程,直到有就绪的,就再遍历一次找到这个就绪的,除此之外,我们的代码中也遍历了好多次。select只是相对于之前的IO方式高效,但也并不是好方案。

select能等待的fd数量太少。存储文件描述符的一般是数组,也就是数组下标表示fd,数组有上限,如果做成服务器,这个数组会调最大,所以有上限是必然的。一个进程能等的fd数量有限,但select能等的更少,所以select等的少是select设计时的限制,和进程无关。select上限就不高。


下一篇写poll型服务器,会在select代码的基础上进行更改。

本篇gitee

结束。

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值