第四章:Linux中I/O多路复用的select技术

select

主旨思想:

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回。
    a.这个函数是阻塞的

    b.函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
// sizeof(fd_set) = 128(个字节) 1024(个bit位)
#include 
#include 
#include 
#include 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    - 参数:
    	- nfds : 委托内核检测的最大文件描述符的值 + 1,传这个参数是是为了提高效率,没必要遍历最大文件描述符之后的,+1是底层实现的逻辑规定的要+1,我猜测可能类似于 for(int i = 0 ; i < nfds + 1 ;++i),这样刚好最后一个能被遍历到
    	- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
    			 - 一般检测读操作
    			 - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区是否有数据,有的话就可以进行读取
    			 - 是一个传入传出参数
    	- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
            	  - 一般不检测写操作 
    			 - 委托内核检测写缓冲区是不是还可以写数据,没有满就可以继续向其中写入数据
    	- exceptfds : 检测发生异常的文件描述符的集合
    	- timeout : 设置的超时时间
        struct timeval {
            long tv_sec; /* seconds */
            long tv_usec; /* microseconds */
        };
            - NULL : 永久阻塞,直到检测到了文件描述符有变化,才会往下执行并且返回
            - tv_sec = 0 tv_usec = 0, 不阻塞
            - tv_sec > 0 tv_usec > 0, 阻塞对应的时间
            - 返回值 :
                  -1 : 失败
                  >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

在我们的例子当中,我们需要检测的是文件描述符中读的属性,因此我们就将 fd_set 类型中对应要检测的文件描述符的对应的标志位设为1表示我要检测,然后传给select()函数遍历,如果文件描述符为0则表示不用检测跳过,为1则委托内核去帮我们进行检测,如果确实有数据来了就将该标志位仍保持为1,没有则修改为0,最后把修改之后的 readfds 返回,就得到了有数据的集合,但是select()的返回值不会告诉我们哪些值发生了变化,只会告诉我们有几个,n个返回n,至于是那些需要我们自己遍历

工作过程分析

在函数执行的过程中,系统先把用户区的这份文件描述符集合拷贝一份到内核当中,然后在内核当中检测标志位并且根据实际情况(比如这里就是哪些文件描述符的读端数据到达了)然后修改标志位,0就是没有,1就是有,然后从内核态重新拷贝到用户态,工作过程大致就是这样

第四章:Linux中I/O多路复用的select技术_第1张图片

代码
// Client_Info.h
#ifndef _CLIENT_INFO_
#define _CLIENT_INFO_

#include 

#include 

#define MAX_IPV4_STRING 16

class Client_Info {
public:
    Client_Info() {
        __init__();
    };

    Client_Info& operator=(const Client_Info& _cli_info) {
        strcpy(this->client_ip, _cli_info.client_ip);
        this->client_port = _cli_info.client_port;

        return *this;
    }

    Client_Info(const char* _ip, const in_port_t& _port) {
        strcpy(this->client_ip, _ip);
        this->client_port = _port;
    }

    Client_Info(const Client_Info& _cli_info) {
        *this = _cli_info;
    }

    void __init__() {
        bzero(this->client_ip, sizeof(this->client_ip));
        this->client_port = 0;
    }

public:
    char client_ip[MAX_IPV4_STRING];
    in_port_t client_port;
};

#endif

以下是服务端和客户端

// server.cpp
#include 
#include 
using namespace std;
#include 
#include 

#include "Client_Info.h"

#define MAXSIZE 1024
#define MAX_CLIENT_SIZE 1024

// 全局存放客户端连接的IP和端口
class Client_Info cli_infos[MAX_CLIENT_SIZE];

// 全局存放需要检测的文件描述符的数组
fd_set read_set;

int bigger(const int& val1, const int& val2) {
    return val1 > val2 ? val1 : val2;
}

void Communicate(const int& _connect_fd) {
    char* _client_ip = cli_infos[_connect_fd].client_ip;
    in_port_t& _client_port = cli_infos[_connect_fd].client_port;

    char buf[MAXSIZE] = {0};
    // 读
    bzero(buf, sizeof(buf));
    int len = read(_connect_fd, buf, sizeof(buf) - 1);
    if (-1 == len) {
        perror("read");
        exit(-1);
    }
    if (len > 0)
        printf("recv client (ip : %s , port : %d) : %s", _client_ip, _client_port, buf);
    else if (0 == len) {  // 客户端关闭
        printf("client ip : %s , port : %d has closed...\n", _client_ip, _client_port);
        // 这里关闭之后需要移除文件描述符集合中的标志位表示我不需要监听这个了
        FD_CLR(_connect_fd, &read_set);
        // 关闭文件描述符
        close(_connect_fd);
        return;
    }
    // 写
    write(_connect_fd, buf, strlen(buf));
}

int main() {
    // 1.创建socket
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listen_fd) {
        perror("socket");
        return -1;
    }

    // 设置一下端口复用
    int _optval = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &_optval, sizeof(_optval));

    // 2.绑定IP和端口
    struct sockaddr_in server_addr;
    // 地址族
    server_addr.sin_family = AF_INET;
    // IP
    server_addr.sin_addr.s_addr = INADDR_ANY;
    // 端口
    server_addr.sin_port = htons(9999);

    int ret = bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (-1 == ret) {
        perror("bind");
        return -1;
    }

    printf("server has initialized.\n");

    // 3.开始监听
    ret = listen(listen_fd, 8);
    if (-1 == ret) {
        perror("listen");
        return -1;
    }

    // 使用NIO模型,创建fd_set集合,存放的是需要检测的文件描述符
    // 全局定义 read_set
    // 初始化
    FD_ZERO(&read_set);
    // 添加需要检测的文件描述符
    FD_SET(listen_fd, &read_set);
	// 定义最大的文件描述符序号(参数里面要加1)
    int max_fd = listen_fd;

    // 这个地方我不能把read_set集合拿进去让内核进行拷贝修改然后覆盖我的这个
    // 我们设想这样一种情况,AB都检测,A发数据,B的被修改为0,但是下一次我肯定还要检测B的啊

    while (1) {
        fd_set tmp_set = read_set;
        // 调用select系统函数,让内核帮忙检测哪些文件描述符有数据
        // 这里是在检测listen_fd,因为如果有客户端请求连接了,那么这里listen_fd肯定会有数据进来
        ret = select(max_fd + 1, &tmp_set, nullptr, nullptr, nullptr);
        if (-1 == ret) {
            perror("select");
            return -1;
        } else if (0 == ret)
            // 为0表示超时并且没有检测到有改变的
            continue;  // 这里我们的设置因为是阻塞的,所以不会走到这里
        else if (ret > 0) {
            // 说明检测到了有文件描述符对应缓冲区的数据发生了改变
            if (FD_ISSET(listen_fd, &tmp_set)) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);

                if (-1 == connect_fd) {
                    perror("accept");
                    return -1;
                }

                // 获取客户端的信息
                char ip[MAX_IPV4_STRING] = {0};
                inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip));

                in_port_t port = ntohs(client_addr.sin_port);

                // 打印信息
                printf("client ip : %s , port : %d has connected...\n", ip, port);

                // 将客户端的信息保存到全局数组中
                cli_infos[connect_fd] = Client_Info(ip, port);

                // 将新的文件描述符加入到集合中,这样select()就可以监听客户端的数据了
                FD_SET(connect_fd, &read_set);
                // 更新max_fd
                max_fd = bigger(connect_fd, max_fd);
            }

            // 看完监听的文件描述符,还要看其他的文件描述符标识位
            for (int i = listen_fd + 1; i < max_fd + 1; ++i) {
                if (FD_ISSET(i, &tmp_set))
                    // 表示有数据到来,进行通信,服务端只处理一次,然后又重新检测是否有数据,有数据则又走这段代码
                    // 并且如果服务端里面处理用循环处理,那么这个客户端一直抢占者服务端,其他服务端没办法发送数据
                    Communicate(i);
            }
        }
    }

    // 4.关闭连接
    close(listen_fd);

    return 0;
}
// client.cpp
#include 
#include 
using namespace std;
#include 
#include 

#define MAXSIZE 1024

int main() {
    // 1.创建套接字
    int connect_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == connect_fd) {
        perror("socket");
        return -1;
    }

    // 2.建立连接
    struct sockaddr_in server_addr;
    // 地址族
    server_addr.sin_family = AF_INET;
    // 端口
    server_addr.sin_port = htons(9999);
    // IP
    inet_pton(AF_INET, "127.0.0.2", &server_addr.sin_addr.s_addr);

    int ret = connect(connect_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (-1 == ret) {
        perror("connect");
        return -1;
    }

    printf("connected successfully , waiting for communicating.\n");

    char buf[MAXSIZE] = {0};
    // 3.开始通信
    while (1) {
        // 写
        bzero(buf, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);

        // 增加退出功能
        if (strcmp(buf, "quit\n") == 0 || strcmp(buf, "QUIT\n") == 0)
            goto END;

        write(connect_fd, buf, strlen(buf));
        printf("send : %s", buf);

        // 读
        bzero(buf, sizeof(buf));
        int len = read(connect_fd, buf, sizeof(buf) - 1);
        if (-1 == len) {
            perror("read");
            return -1;
        }
        if (len > 0)
            printf("recv : %s", buf);
        else if (0 == len) {  // 说明写端关闭,也就是服务端关闭
            printf("server has closed...\n");
            break;
        }
    }

END:
    // 4.关闭连接
    close(connect_fd);

    return 0;
}
代码分析

好,现在我们来分析一下这段代码

首先我们使用的是,NIO模型,就是不阻塞,而是轮询,所以我们需要使用while循环来实现这个机制,然后在select()基础上我们要确认需要检测的文件描述符的读的状态,所以我们定义 fd_set read_set ,由于监听的listen_fd当有客户端连接的时候也是算有数据进入,对应read_set[]的标志位会改变,所以将其添加进去

// 先初始化
FD_ZERO(&read_set);
// 添加需要检测的文件描述符
FD_SET(listen_fd, &read_set);

之后进入while循环我们检测是否有变化,有变化则说明有新客户端连接或者连接上的客户端有数据进入,这里我们设置阻塞等待变化,当然也可以设置一个等待的周期时间

注意返回值 ret 代表的是检测到变化的个数,-1表示错误,0表示没有,可以重开循环(但是我们这里不会,因为我们阻塞);>0则表示有变化,我们可以进行后续处理

ret = select(max_fd + 1, &tmp_set, nullptr, nullptr, nullptr);

可能是新客户端连接或者已连接的客户端发送数据,分别如下:

新客户端连接

// 表示有新的客户端连接进来了
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);

if (-1 == connect_fd) {
    perror("accept");
    return -1;
}

// 获取客户端的信息
char ip[MAX_IPV4_STRING] = {0};
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip));

in_port_t port = ntohs(client_addr.sin_port);

// 打印信息
printf("client ip : %s , port : %d has connected...\n", ip, port);

// 将客户端的信息保存到全局数组中
cli_infos[connect_fd] = Client_Info(ip, port);

// 将新的文件描述符加入到集合中,这样select()就可以监听客户端的数据了
FD_SET(connect_fd, &read_set);
// 更新max_fd
max_fd = bigger(connect_fd, max_fd);

我们不看上面打印信息的部分,看最后两句

  • 我们将新的connect_fd添加到read_set当中,这样就可以检测了
  • 我们更新的max_fd,这样可以提高效率

已经连接上的客户端收到数据

我们就从listen_fd开始遍历,因为listen_fd最开始创建,在普遍情况下是最小的,遍历到max_fd为止

// 看完监听的文件描述符,还要看其他的文件描述符标识位
for (int i = listen_fd + 1; i < max_fd + 1; ++i) {
    if (FD_ISSET(i, &tmp_set))
        // 表示有数据到来,进行通信,服务端只处理一次,然后又重新检测是否有数据,有数据则又走这段代码
        // 并且如果服务端里面处理用循环处理,那么这个客户端一直抢占者服务端,其他服务端没办法发送数据
        Communicate(i);
}

接下来我们看通信函数

我们注意到一个细节,就是没有使用while循环,这是为什么呢?

因为如果服务端里面处理用循环处理,那么这个客户端一直抢占者服务端,其他服务端没办法发送数据;

并且我不用循环处理我把数据读了就结束函数,然后又重新开始检测,代码里移除标志位并且关闭文件描述符是在写端关闭的时候,这时候也是合情合理的

void Communicate(const int& _connect_fd) {
    char* _client_ip = cli_infos[_connect_fd].client_ip;
    in_port_t& _client_port = cli_infos[_connect_fd].client_port;

    char buf[MAXSIZE] = {0};
    // 读
    bzero(buf, sizeof(buf));
    int len = read(_connect_fd, buf, sizeof(buf) - 1);
    if (-1 == len) {
        perror("read");
        exit(-1);
    }
    if (len > 0)
        printf("recv client (ip : %s , port : %d) : %s", _client_ip, _client_port, buf);
    else if (0 == len) {  // 客户端关闭
        printf("client ip : %s , port : %d has closed...\n", _client_ip, _client_port);
        // 这里关闭之后需要移除文件描述符集合中的标志位表示我不需要监听这个了
        FD_CLR(_connect_fd, &read_set);
        // 关闭文件描述符
        close(_connect_fd);
        return;
    }
    // 写
    write(_connect_fd, buf, strlen(buf));
}

我们的代码中还有一个细节

就是在这里为什么要用tmp_set,有的地方是read_set,有的地方是tmp_set

这个地方我不能把read_set集合拿进去让内核进行拷贝修改然后覆盖我的这个;

我们设想这样一种情况,AB都检测,A发数据,B的被修改为0,但是下一次我肯定还要检测B的啊,这就出现问题了

所以我们想到的解决方案就是使用临时变量,但是像新客户端连接,写端关闭的时候删除文件描述符的检测这些还是要操作read_set,也很好理解

第四章:Linux中I/O多路复用的select技术_第2张图片

你可能感兴趣的:(牛客Linux,linux,c++)