0
点赞
收藏
分享

微信扫一扫

请求响应-06.请求-路径参数

目录

前言 

一,select函数

1.1.参数一:nfds

1.2.参数二: readfds, writefds, exceptfds

1.2.1.fd_set类型和相关操作宏

1.2.2.readfds, writefds, exceptfds

1.2.3.怎么理解 readfds, writefds, exceptfds是输入输出型参数

1.3.参数三: timeout

1.3.1.timeval结构体

1.3.1.timeout参数的设定

1.4.返回值 

1.5.select的工作流程

 二,select版TCP服务器

2.1.编写准备

2.2.SelectServer.hpp的编写

2.2.1.为什么要设置辅助数组

2.2.2.select的优缺点 

 2.3.源代码


 

前言 

我们今天要讲的select,select的原理就像下面的赵六一样。

        赵六去钓鱼,他是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。

一,select函数

        select是我们学习的第一个多路转接IO接口,我们知道IO是由等待和拷贝两部分组成的。select只负责IO过程中等待的这一步,也就是说,用户可能关心一些sock上的读事件,想要从sock中读取数据,直接读取,可能recv调用会阻塞,等待数据到来,而此时服务器进程就会被阻塞挂起,但服务器挂起就完蛋了,服务器就无法给客户提供服务,可能会产生很多无法预料的不好影响,万一客户正转账呢,服务器突然挂起了,客户的钱没了,但商家这里又没有收到钱,客户找谁说理去啊,所以服务器挂起是一个问题,我们要避免产生这样的问题。

select函数是I/O多路复用的经典实现,其基本原型如下:

select函数的功能

        select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。

 参数详解

1.1.参数一:nfds

        当程序运行时,程序其实会在select这里进行等待,遍历一次底层的多个fd,看其中哪个fd就绪了,然后就将就绪的fd返回给上层,select的第一个参数nfds代表监视的fd中最大的fd值+1,其实就是select在底层需要遍历所有监视的fd,而这个nfds参数其实就是告知select底层遍历的范围是多大

1.2.参数二: readfds, writefds, exceptfds

1.2.1.fd_set类型和相关操作宏

fd_set是一个通过位图来管理文件描述符集合的数据结构,它允许高效地测试和修改集合中的成员。

  • fd_set类型本质是一个位图,位图的位置 表示 相对应的文件描述符,内容表示该文件描述符是否有效,1代表该位置的文件描述符有效,0则表示该位置的文件描述符无效。
  • 如果将文件描述符2,3设置位图当中,则位图表示的是为1100。
  • fd_set的上限是1024个文件描述符。

由于文件描述符是整数,且通常范围有限(尤其是在UNIX和类UNIX系统中),因此使用位图来表示它们是一种非常有效的空间和时间优化方法。

  1. FD_SET(fd, &set):此宏将文件描述符fd添加到set集合中。它实际上是将set中与fd对应的位设置为1。
  2. FD_CLR(fd, &set):此宏从set集合中移除文件描述符fd。它实际上是将set中与fd对应的位清零。
  3. FD_ISSET(fd, &set):此宏检查文件描述符fd是否已经被加入到set集合中。如果set中与fd对应的位为1,则返回非零值(真),否则返回0(假)。
  4. FD_ZERO(&set):此宏用于清空set集合中的所有文件描述符,即将集合中的所有位都设置为0。这是在使用set之前的一个好习惯,以确保集合从一个已知的状态开始。

我们看个例子

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/select.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
  
int main() {  
    // 假设fd是一个已经打开的文件描述符,这里我们用socket作为示例  
    int fd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket,实际使用中需要设置地址并连接  
    if (fd == -1) {  
        perror("socket");  
        exit(EXIT_FAILURE);  
    }  
  
    // 创建一个文件描述符集合  
    fd_set readfds;  
  
    // 清空集合  
    FD_ZERO(&readfds);  
  
    // 将文件描述符fd添加到集合中  
    FD_SET(fd, &readfds);  
  
    // 假设我们想要等待这个fd变得可读,最长等待时间为5秒  
    struct timeval tv;  
    tv.tv_sec = 5; // 秒  
    tv.tv_usec = 0; // 微秒  
  
    // 使用select等待文件描述符变得可读  
    int ret = select(fd + 1, &readfds, NULL, NULL, &tv);  
    if (ret == -1) {  
        perror("select");  
        close(fd); // 不要忘记关闭文件描述符  
        exit(EXIT_FAILURE);  
    } else if (ret == 0) {  
        printf("Timeout occurred! No data after 5 seconds.\n");  
        close(fd); // 即使没有数据,也要关闭文件描述符  
    } else {  
        // 检查fd是否就绪  
        if (FD_ISSET(fd, &readfds)) {  
            printf("Data is available now on fd %d.\n", fd);  
            // 在这里处理数据,例如使用read()函数读取数据  
  
            // 假设处理完数据后,我们不再需要等待这个fd  
            // 可以在这里调用FD_CLR来从集合中移除它,但在这个简单的例子中我们直接关闭它  
            close(fd);  
        }  
    }  
  
    return 0;  
}

1.2.2.readfds, writefds, exceptfds

这三个参数都是输入输出型参数

 readfds

  1. readfds:这是一个指向fd_set的指针,用于指定程序关心的、希望进行读操作的文件描述符集合。如果select函数返回时,某个文件描述符在该集合中被标记为就绪(即可以进行无阻塞的读操作),则可以通过FD_ISSET宏来检查。
  2. readfds是 等待读事件的文件描述符集合,.如果不关心读事件(缓冲区有数据),则可以传NULL值。
  3. 应用进程和内核都可以设置readfds,应用进程设置readfds是为了通知内核去等待readfds中的文件描述符的读事件,而内核设置readfds是为了告诉应用进程哪些读事件生效

 writefds

  1. writefds:同样是一个指向fd_set的指针,但这次它用于指定程序希望进行写操作的文件描述符集合。如果select返回时某个文件描述符在该集合中被标记为就绪(即可以进行无阻塞的写操作),则同样可以通过FD_ISSET宏来检查。
  2. 与readfds类似,writefds是等待写事件(缓冲区中是否有空间)的集合,如果不关心写事件,则可以传值NULL。

exceptfds

  1. exceptfds:这个参数也是指向fd_set的指针,用于指定程序希望监视异常条件的文件描述符集合。这里的“异常”通常指的是网络套接字上的带外数据(out-of-band data)到达,或者其他一些非标准的I/O事件。
  2. 如果内核等待相应的文件描述符发生异常,则将失败的文件描述符设置进exceptfds中,如果不关心错误事件,可以传值NULL。​​​​

1.2.3.怎么理解 readfds, writefds, exceptfds是输入输出型参数

  1. 输入方面
    • 在调用 select 之前,调用者会设置这三个参数指向的 fd_set 集合,以指定哪些文件描述符(fd)是调用者感兴趣的。具体来说,readfds 集合包含了调用者想要检查是否有数据可读的文件描述符,writefds 集合包含了调用者想要检查是否可以写入数据的文件描述符,而 exceptfds 集合则包含了调用者想要检查是否有异常条件(如带外数据、连接挂断等)的文件描述符。
  2. 输出影响方面
    • 当 select 调用返回时,这三个集合会被 select 函数内部修改,以反映哪些文件描述符在调用期间变得就绪或遇到异常条件。具体来说,如果某个文件描述符在 select 等待期间变得可读、可写或出现异常,那么相应的集合中的该文件描述符的位将被设置(如果它之前没有被设置的话)。但是,这并不意味着 select 在这些集合中添加了新的文件描述符或移除了原有的文件描述符;它只是在修改集合中文件描述符的“就绪”状态位。

1.3.参数三: timeout

1.3.1.timeval结构体

        struct timeval 是一个在多种编程环境中,尤其是在 UNIX 和类 UNIX 系统(包括 Linux)的 C 语言标准库中定义的结构体,用于表示时间间隔或时间点。他的定义如下

struct timeval {
    long tv_sec;  // seconds
    long tv_usec; // microseconds
};

        它通常与需要精确到微秒(microseconds)的时间操作的函数一起使用,比如 select(), gettimeofday(), setitimer(), 和 utimes() 等。

这个结构体包含两个成员:

  1. long tv_sec;:这个成员表示自 Unix 纪元(即 1970 年 1 月 1 日 00:00:00 UTC)以来的秒数。它是一个长整型(long),通常可以存储非常大的数,足以表示从 Unix 纪元到现在的时间(以秒为单位)。
  2. long tv_usec;:这个成员表示秒之后的微秒数。它也是一个长整型(long),但用于存储 0 到 999999 之间的值,表示在 tv_sec 所表示的秒之后,再过去多少微秒。

这两个成员结合起来,就可以精确地表示一个时间点或时间间隔,精确到微秒级别。

例如,如果你想要表示一个从 Unix 纪元开始算起,经过了 123 秒又 456789 微秒的时间点,你可以这样设置 struct timeval 结构体:

 struct timeval time;  
 time.tv_sec = 123;  
 time.tv_usec = 456789; 

这个结构体经常与 gettimeofday() 函数一起使用,以获取当前时间(从 Unix 纪元开始的时间,精确到微秒)。例如:

 #include <sys/time.h>  
 #include <stdio.h>  
   int main() {  
 struct timeval now;  
 gettimeofday(&now, NULL);  
 printf("Current time: %ld.%06ld\n", now.tv_sec, now.tv_usec);  
 return 0;  
 } 

这段代码会输出当前的时间,格式为秒数和微秒数(微秒数前面补零至6位)。

1.3.1.timeout参数的设定

这是一个输入型参数!!

1.4.返回值 

1.5.select的工作流程

        应用进程内核都需要从readfds和writefds获取信息,其中,内核需要从readfds和writefds知道哪些文件描述符需要等待,应用进程需要从readfds和writefds中知道哪些文件描述符的事件就绪.

        如果我们要不断轮询等待文件描述符,则应用进程需要不断的重新设置readfds和writefds因为每一次调用select,内核会修改readfds和writefds,所以我们需要在 应用程序 中 设置一个数组 来保存程序需要等待的文件描述符,保证调用 select 的时候readfds 和 writefds中的将如下:

 二,select版TCP服务器

接下来我们将用select来重新编写一下我们的TCP服务器。

2.1.编写准备

还记得TCP服务器怎么写吗?

为了节约我们的时间,我们复制一下我们之前封装好的Socket.hpp

#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>  
 
  
// 定义一些错误代码  
enum  
{  
    SocketErr = 2,    // 套接字创建错误  
    BindErr,          // 绑定错误  
    ListenErr,        // 监听错误  
};  
  
// 监听队列的长度  
const int backlog = 10;  
  
class Sock  //服务器专门使用
{  
public:  
    Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字  
    {  
    }  
    ~Sock()  
    {  
        // 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源  
    }  
  
    // 创建套接字  
    void Socket()  
    {  
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);  
        if (sockfd_ < 0)  
        {  
            printf("socket error, %s: %d", strerror(errno), errno); //错误  
            exit(SocketErr); // 发生错误时退出程序  
        } 
        int opt=1;
        setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //关闭后快速重启
    }  
  
    // 将套接字绑定到指定的端口上  
    void Bind(uint16_t port)  
    {  
        //让服务器绑定IP地址与端口号
        struct sockaddr_in local;  
        memset(&local, 0, sizeof(local));//清零  
        local.sin_family = AF_INET;  // 网络
        local.sin_port = htons(port);  // 我设置为默认绑定任意可用IP地址
        local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口  
  
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)  //让自己绑定别人
        {  
            printf("bind error, %s: %d", strerror(errno), errno);  
            exit(BindErr);  
        }  
    }  
  
    // 监听端口上的连接请求  
    void Listen()  
    {  
        if (listen(sockfd_, backlog) < 0)  
        {  
            printf("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)  
        {  
            printf("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; // 返回新的套接字文件描述符  
    }  
  
    // 连接到指定的IP和端口——客户端才会用的  
    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_; // 套接字文件描述符  
};

首先我们要创建一个SelectServer.hpp,main.cc,makefile

select_server:main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf select_server
#pragma once
#include<iostream>
#include"Socket.hpp"

const uint16_t default_port = 8877;       // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP

class SelectServer
{
public:
    SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
        : ip_(ip), port_(port)
    {
    }
    ~SelectServer()
    {
        listensock_.Close();
    }

     bool Init()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();     

        return true;   
    }

    void Start()
    {

    }

private:
    uint16_t port_;//绑定的端口号
    Sock listensock_;//专门用来listen的
    std::string ip_;  // ip地址
};
#include"SelectServer.hpp"
#include<memory>

int main()
{
    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();
}

2.2.SelectServer.hpp的编写

 接下来我们就只剩下

class SelectServer{

void Start()
{
    }
};

没有编写了。

我们可以看看我们之前编写的TCP服务器是怎么编写的。

void Start()
    {
        while(true)
        {
            std::string clientip;
            uint16_t clientport;
 
            int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字
            if(socket<0) 
                continue;
 
            //提供服务
            if(fork()==0)
            {
                listensock_.Close();
                //通过sockfd使用提供服务
        
                 std::string inbuf;
                while (1)
                {
                    
                    char buf[1024];
                    // 1.读取客户端发送的信息
                    ssize_t s = read(sockfd, buf, sizeof(buf) - 1);
                    if (s == 0)
                    { // s == 0代表对方发送了空消息,视作客户端主动退出
                        printf("client quit: %s[%d]", clientip.c_str(), clientport);
                        break;
                    }
                    else if (s < 0)
                    {
                        // 出现了读取错误,打印错误后断开连接
                        printf("read err: %s[%d] = %s", clientip.c_str(), clientport, strerror(errno));
                        break;
                    }
                    else // 2.读取成功
                    {
 
                        
                    }
                }
 
                exit(0);//子进程退出
            }
 
            close(sockfd);//
        }
    }

我们发现,我们首先进行的就是accept啊!!那我们这里能不能里面进行accept呢?答案是不能的。accept本质就是检测并建立listen上面有没有新连接的到来。

还记得我们最开始讲的例子吗?

        赵六去钓鱼,他是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。

这个新链接就是鱼啊!!!新连接的到来就相当于鱼咬钩了。所以我们处理新连接的时候就得采用IO多路复用思想。

        如果是一个select服务器进程,则服务器进程会不断的接收有新链接,每个链接对应一个文件描述符如果想要我们的服务器能够同时等待多个链接的数据的到来,我们监听套接字listen_sock读取新链接的时候,我们需要将新链接的文件描述符保存到read_arrys数组中,下次轮询检测的就会将新链接的文件描述符设置进readfds中,如果有链接关闭,则将相对应的文件描述符从read_arrys数组中拿走。

一张图看懂select服务器:

按照上面的思路,我们暂且写出了下面这个

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

const uint16_t default_port = 8877;       // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP

class SelectServer
{
public:
    SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
        : ip_(ip), port_(port)
    {
    }
    ~SelectServer()
    {
        listensock_.Close();
    }

     bool Init()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();     

        return true;   
    }

    void Start()
    {
        int listensock=listensock_.Fd();
        struct timeval timeout ={5,0};
        for(;;)
        {
            fd_set rfds;

            FD_ZERO(&rfds);
            FD_SET(listensock,&rfds);

            int n=select(listensock+1,&rfds,NULL,NULL,&timeout);//刚开始的时候只有1个连接啊!!!
            switch(n)
            {
                case 0:
                    std::cout<<"time out"<<std::endl;
                    break;
                case -1:
                    std::cout<<"select error"<<std::endl;
                    break;
                default:
                    //有事件就绪
                    break;
            }
        }

    }

private:
    uint16_t port_;//绑定的端口号
    Sock listensock_;//专门用来listen的
    std::string ip_;  // ip地址
};

这里需要补充一些知识:

当一个新的连接请求到达监听套接字时,操作系统会接受这个请求(但不在用户空间的应用程序中立即处理它),并将监听套接字的状态标记为可读。这是因为从技术上讲,新的连接请求会导致监听套接字上有一些数据可读——具体来说,是新的连接的信息(例如,客户端的地址和端口号),这些信息将用于后续的 accept 调用。

 我们编译运行一下

我们看看

我们回去再看看我们运行情况 

 嗯?什么情况?为什么一直在打印time out?这个是因为timeout参数是个输入输出型参数

 我们现在需要修改一下代码

 void Start()
    {
        int listensock=listensock_.Fd();
        
        for(;;)
        {
            fd_set rfds;

            FD_ZERO(&rfds);
            FD_SET(listensock,&rfds);

            struct timeval timeout ={5,0};//注意这里

            int n=select(listensock+1,&rfds,NULL,NULL,&timeout);//刚开始的时候只有1个连接啊!!!
            switch(n)
            {
                case 0:
                    std::cout<<"time out"<<std::endl;
                    break;
                case -1:
                    std::cout<<"select error"<<std::endl;
                    break;
                default:
                    //有事件就绪
                    break;
            }
        }

    }

 现在我们再去编译一下

现在就不会变了。 一直为5秒了。

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

const uint16_t default_port = 8877;       // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP

class SelectServer
{
public:
    SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
        : ip_(ip), port_(port)
    {
    }
    ~SelectServer()
    {
        listensock_.Close();
    }

     bool Init()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();     

        return true;   
    }

    void Start()
    {
        int listensock=listensock_.Fd();
        
        for(;;)
        {
            fd_set rfds;

            FD_ZERO(&rfds);
            FD_SET(listensock,&rfds);

            int n=select(listensock+1,&rfds,NULL,NULL,nullptr);
            switch(n)
            {
                case 0:
                    std::cout<<"time out"<<std::endl;
                    break;
                case -1:
                    std::cout<<"select error"<<std::endl;
                    break;
                default:
                    //有事件就绪
                    std::cout<<"get a new link"<<std::endl;
                    break;
            }
        }

    }

private:
    uint16_t port_;//绑定的端口号
    Sock listensock_;//专门用来listen的
    std::string ip_;  // ip地址
};

 我们编译运行一下

我们发现程序怎么一直打印get a new link啊?这是因为我们没有把连接处理。

其实这个是select的特点:如果事件就绪,上层不处理的话,select会一直通知你!!!

如果select告诉我们就绪,接下来的一次读取,我们读取fd的时候,不会被阻塞

接下来我们就要来处理这个连接了!!!

为了让代码看起来更好看一点,我们将处理连接这部分封装起来。

   void HandlerEvent(fd_set& rfds)
    {
        // 检查监听套接字是否就绪 
        if(FD_ISSET(listensock_.Fd(),&rfds))
                // 监听套接字上有新的连接请求  
                // 调用accept来接受连接  
        {
            //我们的连接事件就绪了
            std::string clientip;
            uint16_t clientport;
 
            int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字
            //请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
            if(sockfd<0)
                return;
        }
    }

注意:

  • FD_ISSET(fd, &set):此宏检查文件描述符fd是否已经被加入到set集合中。如果set中与fd对应的位为1,则返回非零值(真),否则返回0(假)。 

 现在有一个问题,

我们现在可不可以在后面使用read对socked直接进行读数据呢?

答案是不可以。!!!!!

为什么呢?

        因为我们一旦调用read,万一客户端没有发数据过来,服务器进程就会阻塞在read这里!!这样子就会导致HandlerEvent函数调用不会返回,继而导致Start函数的循环阻塞,无法调用select函数监视新加入的连接。

我们发现

所以在accept函数后面我们不能直接调用read函数,而是将新连接加入到select中。

        可是我们发现,我们的select和我们的accept在不同的函数里面,我们怎么让select来设置我们的文件描述符呢?这个时候我们就要设置一个辅助数组了。

2.2.1.为什么要设置辅助数组

 我们可以让新增的fd都添加进辅助数组中,然后让select每次动态设置max_fd,以及三个位图(新增操作在"处理函数"中介绍)

  • 可以固定监听套接字(也就是我们创建的第一个套接字)作为数组的第一项,方便我们后续区分[获取新连接] 和 [读写事件]。
  • 因为在过程中,可能会陆陆续续关掉一些文件(断开连接时),所以原本添加进的连续fd,会变成零零星星的,所以需要我们每次都重新整理一下这个数组,把有效的fd统一放在左侧,我们每次在循环开头就处理数组中的值,合法的fd就让它设置进位图中
  • 不仅如此,在这个过程中,我们还可以找到fd中的最大值,来填充select的第一个参数

接下来我们就修改一下Start函数 

void Start()
    {
        int listensock = listensock_.Fd();
        
        fd_arry[0] = listensock; // 将监听套接字加入辅助数组
        for (;;)
        {
            fd_set rfds;//每调用一次select函数rfds需要重新设定
            FD_ZERO(&rfds);

            int maxfd = fd_arry[0]; // 最大有效数组下标

            for (int i = 0; i < fd_num_max; ++i)
            {
                if (fd_arry[i] == default_fd)
                {
                    continue;
                }
                FD_SET(fd_arry[i], &rfds);
                //注意辅助数组第一个元素是listen套接字,所以最开始的时候一定会执行到这里

                if (maxfd<fd_arry[i])//如果有更大的文件描述符,就替换掉maxfd
                {
                    maxfd = fd_arry[i];
                }
            }

            int n = select(maxfd + 1, &rfds, NULL, NULL, nullptr); // 注意这里会修改rfds,返回的rfds有效位代表着哪个位置的有效事件就绪了

            switch (n)
            {
            case 0:
                std::cout << "time out" << std::endl;
                break;
            case -1:
                std::cout << "select error" << std::endl;
                break;
            default:
                // 有事件就绪
                std::cout << "get a new link" << std::endl;
                HandlerEvent(rfds); // 处理事件
                break;
            }
        }
    }

接下来需要修改一下我们的HandlerEvent函数,我们accept新连接后不能直接读取,会阻塞,我们需要将这个新连接加入我们的select函数的范围,这就需要我们借助辅助数组了

当我们识别到有事件就绪,获取连接后获得新套接字fd,之后就该将该fd设置进辅助数组中

  • 需要我们遍历数组,找到空位(值为-1/其他你设定的[数组内的初始值]),然后添加进去
  • 但是要注意位图还有没有空位置(别忘了位图是有上限的)
  • 所以,还需要加个判断
 void HandlerEvent(fd_set &rfds)
    {
        // 1.判断哪个读事件就绪
        if (FD_ISSET(listensock_.Fd(), &rfds)) //
        {
            // 我们的连接事件就绪了
            std::string clientip;
            uint16_t clientport;

            int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
            // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
            if (sockfd <0)
                return;
            else // 把新fd加入位图
            {
                int i = 1;
                for (; i < fd_num_max; i++)//为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
                {
                    if (fd_arry[i] !=default_fd ) // 没找到空位
                    {
                        continue;;
                    }
                    else{//找到空位,但不能直接添加
                        break;
                    }
                }
                if (i != fd_num_max)//没有满
                {
                    fd_arry[i] = sockfd;//把新连接加入数组
                }
                else  // 满了
                { 
                    close(sockfd);//处理不了了,直接关闭连接吧
                }
            }
        }
    }

一旦有新连接的到来,我们就是只先把连接放到辅助数组里面。

 大家有没有发现,这个辅助数组里面的事件有两类啊!!!!就是[新连接]和[读写事件],如何区分fd集上的事件就绪究竟是[新连接]还是[读写事件]呢?

如何区分fd集上的事件就绪究竟是[新连接]还是[读写事件]呢?

  • 前面我们提到,将监听套接字固定在数组第一项,就是为了区分两者,所以写个判断语句就行
 void HandlerEvent(fd_set &rfds)
    {
        for (int n = 0; n < fd_num_max; n++)
        {
            int fd = fd_arry[n];
            if (fd == default_fd) // 无效的
                continue;

            if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
            {

                // 1.是listen套接字就绪了
                if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                {
                    // 我们的连接事件就绪了
                    std::string clientip;
                    uint16_t clientport;

                    int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
                    // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
                    if (sockfd < 0)
                        continue;
                    else // 把新fd加入位图
                    {
                        int i = 1;
                        for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
                        {
                            if (fd_arry[i] != default_fd) // 没找到空位
                            {
                                continue;
                            }
                            else
                            { // 找到空位,但不能直接添加
                                break;
                            }
                        }
                        if (i != fd_num_max) // 没有满
                        {
                            fd_arry[i] = sockfd; // 把新连接加入数组
                            Printfd();
                        }
                        else // 满了
                        {
                            close(sockfd); // 处理不了了,直接关闭连接吧
                        }
                    }
                }

                // 2.是通信的套接字就绪了,fd不是listen套接字
                else // 读事件
                {
                    char in_buff[1024];
                    int n = read(fd, in_buff, sizeof(in_buff) - 1);
                    if (n > 0)
                    {
                        in_buff[n] = 0;
                        std::cout << "get message: " << in_buff << std::endl;
                    }
                    else if (n == 0) // 客户端关闭连接
                    {
                        close(fd);//我服务器也要关闭
                        fd_arry[n] = default_fd; // 重置数组内的值
                    }
                    else
                    {
                        close(fd);//我服务器也要关闭
                        fd_arry[n] = default_fd; // 重置数组内的值
                    }
                }
               
            }
        }
    }

 我们做个实验

 

很完美啊

很好!!

我们也可以把处理过程单拎出来封装成两个函数

  • 就相当于我们把收到的事件根据类型不同,派发给不同的模块进行处理
   void Accept()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport;

        int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
        // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
        if (sockfd < 0)
            return;
        else // 把新fd加入位图
        {
            int i = 1;
            for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
            {
                if (fd_arry[i] != default_fd) // 没找到空位
                {
                    continue;
                }
                else
                { // 找到空位,但不能直接添加
                    break;
                }
            }
            if (i != fd_num_max) // 没有满
            {
                fd_arry[i] = sockfd; // 把新连接加入数组
                Printfd();
            }
            else // 满了
            {
                close(sockfd); // 处理不了了,直接关闭连接吧
            }
        }
    }

    void Receiver(int fd, int i)
    {
        char in_buff[1024];
        int n = read(fd, in_buff, sizeof(in_buff) - 1);
        if (n > 0)
        {
            in_buff[n] = 0;
            std::cout << "get message: " << in_buff << std::endl;
        }
        else if (n == 0) // 客户端关闭连接
        {
            close(fd);               // 我服务器也要关闭
            fd_arry[i] = default_fd; // 重置数组内的值
        }
        else
        {
            close(fd);               // 我服务器也要关闭
            fd_arry[i] = default_fd; // 重置数组内的值
        }
    }

    void HandlerEvent(fd_set &rfds)
    {
        for (int n = 0; n < fd_num_max; n++)
        {
            int fd = fd_arry[n];
            if (fd == default_fd) // 无效的
                continue;

            if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
            {
                // 1.是listen套接字就绪了
                if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                {
                    Accept();
                }
                // 2.是通信的套接字就绪了,fd不是listen套接字
                else // 读事件
                {
                    Receiver(fd,n);
                }
            }
        }
    }

2.2.2.select的优缺点 

总的来说,select最重要的就是思维方式 -- 我们要将所有等待的过程都交给select

并且优缺点很明显

优点

  1. 确实实现了多路转接,可以等待多个fd
  2. 代码简单明了

缺点

  1. 等待的fd数量有上限
  2. 输入输出型参数较多,需要频繁进行用户和内核之间的数据拷贝操作 , 以及每次都要重复设置
  3. 使用了第三方数组,用户层在读取fd时需要很多次遍历
  4. 除了用户层,内核层也要经过多次遍历 -- 内核需要知道自己要关注的fd(遍历位图),关注的过程其实就是在遍历文件描述符表(遍历范围上限=max_fd+1,因为fd本身就是下标,+1就是数量了)

 2.3.源代码

#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>  
 
  
// 定义一些错误代码  
enum  
{  
    SocketErr = 2,    // 套接字创建错误  
    BindErr,          // 绑定错误  
    ListenErr,        // 监听错误  
};  
  
// 监听队列的长度  
const int backlog = 10;  
  
class Sock  //服务器专门使用
{  
public:  
    Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字  
    {  
    }  
    ~Sock()  
    {  
        // 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源  
    }  
  
    // 创建套接字  
    void Socket()  
    {  
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);  
        if (sockfd_ < 0)  
        {  
            printf("socket error, %s: %d", strerror(errno), errno); //错误  
            exit(SocketErr); // 发生错误时退出程序  
        } 
        int opt=1;
        setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //服务器主动关闭后快速重启
    }  
  
    // 将套接字绑定到指定的端口上  
    void Bind(uint16_t port)  
    {  
        //让服务器绑定IP地址与端口号
        struct sockaddr_in local;  
        memset(&local, 0, sizeof(local));//清零  
        local.sin_family = AF_INET;  // 网络
        local.sin_port = htons(port);  // 我设置为默认绑定任意可用IP地址
        local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口  
  
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)  //让自己绑定别人
        {  
            printf("bind error, %s: %d", strerror(errno), errno);  
            exit(BindErr);  
        }  
    }  
  
    // 监听端口上的连接请求  
    void Listen()  
    {  
        if (listen(sockfd_, backlog) < 0)  
        {  
            printf("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)  
        {  
            printf("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; // 返回新的套接字文件描述符  
    }  
  
    // 连接到指定的IP和端口——客户端才会用的  
    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_; // 套接字文件描述符  
};
#pragma once
#include <iostream>
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>

const uint16_t default_port = 8877;       // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int fd_num_max = sizeof(fd_set) * 8;
const int default_fd = -1;

class SelectServer
{
public:
    SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
        : ip_(ip), port_(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_arry[i] = -1; // 辅助数组所有元素都是-1;
        }
    }
    ~SelectServer()
    {
        listensock_.Close();
    }

    bool Init()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();

        return true;
    }

    void Accept()
    {
        // 我们的连接事件就绪了
        std::string clientip;
        uint16_t clientport;

        int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
        // 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
        if (sockfd < 0)
            return;
        else // 把新fd加入位图
        {
            int i = 1;
            for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
            {
                if (fd_arry[i] != default_fd) // 没找到空位
                {
                    continue;
                }
                else
                { // 找到空位,但不能直接添加
                    break;
                }
            }
            if (i != fd_num_max) // 没有满
            {
                fd_arry[i] = sockfd; // 把新连接加入数组
                Printfd();
            }
            else // 满了
            {
                close(sockfd); // 处理不了了,直接关闭连接吧
            }
        }
    }

    void Receiver(int fd, int i)
    {
        char in_buff[1024];
        int n = read(fd, in_buff, sizeof(in_buff) - 1);
        if (n > 0)
        {
            in_buff[n] = 0;
            std::cout << "get message: " << in_buff << std::endl;
        }
        else if (n == 0) // 客户端关闭连接
        {
            close(fd);               // 我服务器也要关闭
            fd_arry[i] = default_fd; // 重置数组内的值
        }
        else
        {
            close(fd);               // 我服务器也要关闭
            fd_arry[i] = default_fd; // 重置数组内的值
        }
    }

    void HandlerEvent(fd_set &rfds)
    {
        for (int n = 0; n < fd_num_max; n++)
        {
            int fd = fd_arry[n];
            if (fd == default_fd) // 无效的
                continue;

            if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
            {
                // 1.是listen套接字就绪了
                if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
                {
                    Accept();
                }
                // 2.是通信的套接字就绪了,fd不是listen套接字
                else // 读事件
                {
                    Receiver(fd,n);
                }
            }
        }
    }
    void Printfd()
    {
        std::cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_arry[i] == default_fd)
                continue;
            else
            {
                std::cout << fd_arry[i] << " ";
            }
        }
        std::cout << std::endl;
    }

    void Start()
    {
        int listensock = listensock_.Fd();

        fd_arry[0] = listensock; // 将监听套接字加入辅助数组
        for (;;)
        {
            fd_set rfds; // 每调用一次select函数rfds需要重新设定
            FD_ZERO(&rfds);

            int maxfd = fd_arry[0]; // 最大有效数组下标

            for (int i = 0; i < fd_num_max; ++i)
            {
                if (fd_arry[i] == default_fd)
                {
                    continue;
                }
                FD_SET(fd_arry[i], &rfds);
                // 注意辅助数组第一个元素是listen套接字,所以最开始的时候一定会执行到这里

                if (maxfd < fd_arry[i]) // 如果有更大的文件描述符,就替换掉maxfd
                {
                    maxfd = fd_arry[i];
                    std::cout << "max_fd:" << maxfd << std::endl;
                }
            }

            int n = select(maxfd + 1, &rfds, NULL, NULL, nullptr); // 注意这里会修改rfds,返回的rfds有效位代表着哪个位置的有效事件就绪了

            switch (n)
            {
            case 0:
                std::cout << "time out" << std::endl;
                break;
            case -1:
                std::cout << "select error" << std::endl;
                break;
            default:
                // 有事件就绪
                std::cout << "get a new link" << std::endl;
                HandlerEvent(rfds); // 处理事件
                break;
            }
        }
    }

private:
    uint16_t port_;          // 绑定的端口号
    Sock listensock_;        // 专门用来listen的
    std::string ip_;         // ip地址
    int fd_arry[fd_num_max]; // 辅助数组——方便文件描述符在不同函数间传递
};
#include"SelectServer.hpp"
#include<memory>

int main()
{
    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();
}

 

举报

相关推荐

0 条评论