【Linux Network】I/O多路转接之select

文章详细介绍了Linux系统中的select函数,包括其函数原型、执行过程、socket就绪条件、特点以及优缺点。select用于实现多路复用输入/输出模型,能监视多个文件描述符的状态变化,但存在文件描述符数量限制和效率问题。文中还展示了基于select的简单多人聊天程序的server源代码,通过示例帮助读者更好地理解和应用select。
摘要由CSDN通过智能技术生成

 

目录

1. 初识select

1.1 select函数原型

1.2 理解select执行过程

1.3 socket就绪条件

1.4 select的特点

1.5 select优缺点

2. 基于select的多人聊天程序

server源代码:

client的登录:

结果演示:



 Linux Network🌷

1. 初识select

系统提供 select 函数来实现多路复用输入 / 输出模型;
  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

总结:

select只负责等待,read、recv、send、write、accept负责自己的核心的业务功能(读、写);

在这里我们会想,read、recv、send、write、accept也有等待的功能啊,但是这些系统调用接口只能等待一个fd;

1.1 select函数原型

select 的函数原型如下:
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, 
            fd_set *exceptfds, struct timeval *timeout); 
参数解释:
  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset、wrset、exset分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合及异常文件描述符的集合;
  • 参数timeout的结构为timeval,用来设置select()的等待时间;
  • 其中后四个参数都是输入输出型参数;
参数 timeout 取值:
  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回;
关于 fd_set 结构

其实这个结构就是一个整数数组 , 更严格的说 , 是一个 " 位图 ". 使用位图中对应的位来表示要监视的
文件描述符;
提供了一组操作 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的全部位

fd_set的大小

fd_set是一个位图结构,它是有大小的,下面我们来查看一下Linux环境下的 fd_set 的大小:

#include <iostream>    
#include <sys/select.h>    
    
using namespace std;    
    
int main()    
{    
  cout << "sizeof(fd_set): " << sizeof(fd_set) << endl;    
  cout << "How many fd can opened by fd_set: " << sizeof(fd_set)*8 << endl;                                                                        
  return 0;    
}    

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

函数返回值:
  • 执行成功则返回文件描述词状态已改变的个数;
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
错误值可能为:
  • EBADF 文件描述词为无效的或该文件已关闭;
  • EINTR 此调用被信号所中断;
  • EINVAL 参数n 为负值;
  • ENOMEM 核心内存不足;

1.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 被清空;

select核心功能:以读为例,核心的两点:

1. 用户告知内核,你要帮我关心哪些fd上的读事件就绪;

2. 内核告知用户,你所关心的哪些fd上的读事件已经就绪;

1.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协议头中, 有一个紧急指针的字段),

1.4 select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=128,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=1024;
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断;
二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从
array取得 fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
备注 : fd_set 的大小可以调整,可能涉及到重新编译内核, 感兴趣的同学可以自己去收集相关资料;

1.5 select优缺点

select的优点:

  • 可以一次等待多个fd,在一定程度上提高IO的效率;

select的缺点:

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便;
  • select底层需要轮询式的检测那些fd上的事件是否就绪;
  • select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝问题;
  • select支持的文件描述符数量太小;

关于select缺点:打开的文件描述符数量大小限制:

一个进程可以打开的文件描述符数量是有限的;

在一般虚拟机上,可以打开的文件描述符是32个,但内核也是支持扩展文件描述符的;

在云服务器上,可以打开的文件描述符数量如下:

虽然进程本身可以打开的文件描述符数量是有限的,但经过查看在云服务器上一个进程可以打开的文件描述符是100001个,因此select可以打开的文件描述符数量有限确实是select的一个缺点;

2. 基于select的多人聊天程序

server源代码:

  • Sock.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, 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)
        {
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if (listen(sock, 5) < 0)
        {
            cerr << "listen error !" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr *)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};
  • server.cc
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"

#define NUM (sizeof(fd_set) * 8)

int fd_array[NUM]; //内容>=0,合法的fd,如果是-1,该位置没有fd

static void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

// ./select_server 8080
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = (uint16_t)atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);
    for (int i = 0; i < NUM; i++)
    {
        fd_array[i] = -1;
    }

    // accept: 不应该,accept的本质叫做通过listen_sock获取新链接
    //         前提是listen_sock上面有新链接,accept怎么知道有新链接呢??
    //         不知道!!!accept阻塞式等待
    //         站在多路转接的视角,我们认为,链接到来,对于listen_sock,就是读事件就绪!!!
    //         对于所有的服务器,最开始的时候,只有listen_sock

    //事件循环
    fd_set rfds;
    fd_array[0] = listen_sock;
    for (;;)
    {
        FD_ZERO(&rfds);
        int max_fd = fd_array[0];
        for (int i = 0; i < NUM; i++)
        {
            if (fd_array[i] == -1)
                continue;
            //下面的都是合法的fd
            FD_SET(fd_array[i], &rfds); //所有要关心读事件的fd,添加到rfds中
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i]; //更新最大fd
            }
        }

        struct timeval timeout = {0, 0}; // 5s
        // 我们的服务器上的所有的fd(包括listen_sock),都要交给select进行检测!!
        // recv,read,write,send,accept : 只负责自己最核心的工作:真正的读写(listen_sock:accept)
        int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //暂时阻塞
        switch (n)
        {
        case -1:
            std::cerr << "select error" << std::endl;
            break;
        case 0:
            std::cout << "select timeout" << std::endl;
            break;
        default:
            std::cout << "有fd对应的事件就绪啦!" << std::endl;
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == -1)
                    continue;
                //下面的fd都是合法的fd,合法的fd不一定是就绪的fd
                if (FD_ISSET(fd_array[i], &rfds))
                {
                    std::cout << "sock: " << fd_array[i] << " 上面有了读事件,可以读取了" << std::endl;
                    // 一定是读事件就绪了!!!
                    // 就绪的fd就在fd_array[i]保存!
                    // read, recv时,一定不会被阻塞!
                    // 读事件就绪,就一定是可以recv,read吗??不一定!!
                    if (fd_array[i] == listen_sock)
                    {
                        std::cout << "listen_sock: " << listen_sock << " 有了新的链接到来" << std::endl;
                        // accept
                        int sock = Sock::Accept(listen_sock);
                        if (sock >= 0)
                        {
                            std::cout << "listen_sock: " << listen_sock << " 获取新的链接成功" << std::endl;
                            // 获取成功
                            // recv,read了呢?绝对不能!
                            // 新链接到来,不意味着有数据到来!!什么时候数据到来呢?不知道
                            // 可是,谁可以最清楚的知道那些fd,上面可以读取了?select!
                            // 无法直接将fd设置进select,但是,好在我们有fd_array[]!
                            int pos = 1;
                            for (; pos < NUM; pos++)
                            {
                                if (fd_array[pos] == -1)
                                    break;
                            }
                            // 1. 找到了一个位置没有被使用
                            if (pos < NUM)
                            {
                                std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;
                                fd_array[pos] = sock;
                            }
                            else
                            {
                                // 2. 找完了所有的fd_array[],都没有找到没有被使用位置
                                // 说明服务器已经满载,没法处理新的请求了
                                std::cout << "服务器已经满载了,关闭新的链接" << std::endl;
                                close(sock);
                            }
                        }
                    }
                    else
                    {
                        // 普通的sock,读事件就绪啦!
                        // 可以进行读取啦,recv,read
                        // 可是,本次读取就一定能读完吗?读完,就一定没有所谓的数据包粘包问题吗?
                        // 但是,我们今天没法解决!我们今天没有场景!仅仅用来测试
                        std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
                        char recv_buffer[1024] = {0};
                        ssize_t s = recv(fd_array[i], recv_buffer, sizeof(recv_buffer) - 1, 0);
                        if (s > 0)
                        {
                            recv_buffer[s] = '\0';
                            std::cout << "client[ " << fd_array[i] << "]# " << recv_buffer << std::endl;
                        }
                        else if (s == 0)
                        {
                            std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
                            //对端关闭了链接
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                        else
                        {
                            //读取失败
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                    }
                }
            }
            break;
        }
    }

    return 0;
}

client的登录:

客户端使用 telnet 进行登录,如果没有的可以使用如下命令进行下载:

sudo yum install telnet

结果演示:

 

I/O多路转接之select终于告一段落了,下篇我们继续poll和epoll🎈

如果本篇博客对您有所帮助的话,还请点赞、收藏并关注我✨

才疏学浅,如果有所疏漏的话,还请评论指出!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瞳绣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值