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

news2024/11/22 9:11:55

 

目录

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🎈

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550259.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++初阶--C++入门之基础学习

0.前言 C是一门非常好的编程语言&#xff0c;但可能在学习C的过程中会遇到很多困难。人们常说 “一个人走得很快&#xff0c;一群人会走的更远”&#xff0c; 所以就让我们一起攻坚克难&#xff0c;一起征服C吧&#xff01;从本章开始&#xff0c;我们将开始C的基础学习&#x…

Linux简介及基础操作

1.Linux的作用&#xff1a; 商业服务器基本都是linux的、开源软件都先支持linux、大数据分析&#xff0c;机器学习首选linux、整个互联网地基基本由linux支撑起来。如&#xff1a; 生活中的手机是基于linux二次开发的&#xff0c;还有路由器也是基于linux开发的。 2.Linux是什…

acwing提高--多源BFS+最小步数模型+双端队列广搜

多源BFS 1.矩阵距离 题目https://www.acwing.com/problem/content/description/175/ #include<bits/stdc.h> using namespace std; #define x first #define y second typedef pair<int,int> PII; const int N1010; char g[N][N]; int dist[N][N]; PII q[N*N];…

【轻量化网络系列(2)】MobileNetV2论文超详细解读(翻译 +学习笔记+代码实现)

前言 上一篇我们介绍了MobileNetV1&#xff0c;主要是将普通Conv转换为dw和pw&#xff0c;但是在dw中训练出来可能会很多0&#xff0c;也就是depthwise部分得到卷积核会废掉&#xff0c;即卷积核参数大部分为0&#xff0c;因为权重数量可能过少&#xff0c;再加上Relu激活函数…

稳定币是个好生意

* * * 原创&#xff1a;刘教链 * * * 本月早些时候&#xff0c;市值第一的稳定币发行商Tether公布了其一季度的储备和盈利数据[1]。不能说是亮眼&#xff0c;只能说是非常亮眼。就看几个亮点吧&#xff1a; 1. 一季度净利润14.8亿美元&#xff0c;是2022年四季度的两倍多&…

关于Java中的抽象类注意事项

文章目录 &#x1f3c6;文章导读&#x1f342;抽象类的定义&#x1f342;抽象类的特性&#x1f342;总结&#xff1a;面试题普通类和抽象类有哪些区别&#xff1f;抽象类能使用final继承吗&#xff1f; &#x1f3c6;文章导读 在本篇文章中&#xff0c;对抽象类进行了一个详细的…

c++学习——c与c++const修饰的变量的区别

c语言下const修饰的变量 1、c语言下const修饰的变量都有空间 2. c语言的const修饰的全局变量具有外部链接属性 07 const修饰的变量.c #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <stdlib.h>const int a 10;//常…

1. Linux环境搭建及问题解决方案

本文介绍了Linux环境搭建的过程以及遇到的问题和解决方案&#xff0c;并且介绍了常用的Linux命令. 一、Linux环境搭建 整体所需的环节 安装VMware安装Linux &#xff08;这边我选的是Server版本&#xff09;安装配置Samba&#xff08;Samba是一种Linux和Windows之间进行文件共…

二层环路详解:交换机环路产生的过程和原因

前言&#xff1a; 在了解环路之前得先了解交换机的工作原理&#xff0c;当然交换机的基本工作原理其实非常简单&#xff0c;只有“单播转发与泛洪转发”、“交换机MAC地址表”这两个&#xff01;其他的如vlan&#xff0c;生成树等也是在此基础上增加的&#xff0c;弥补交换机基…

初始Linux的基本操作

上篇博客中&#xff0c;我介绍了关于Linux的相关概念&#xff0c;让我们初步的了解到Linux的重要性&#xff0c;在这篇博客中我会再讲一些Linux操作系统的理解。 一.操作系统 我们知道Linux是一个操作系统&#xff0c;而操作系统操作系统(英语&#xff1a;Operating System&…

[深度好文]10张图带你轻松理解关系型数据库系统的工作原理

[深度好文]10张图带你轻松理解关系型数据库系统的工作原理 原文(欢迎关注)&#xff1a;https://mp.weixin.qq.com/s/CNCfWRpv8QlICGvZkLG4Jw 尽管数据库在我们应用程序中扮演着储存几乎所有状态的关键角色&#xff0c;但人们对其运行原理的了解通常仅停留在较为浅显的层面&…

跟我一起使用 compose 做一个跨平台的黑白棋游戏(4)移植到compose-jb实现跨平台

前言 在上一篇文章中&#xff0c;我们已经实现了游戏的所有界面和逻辑代码&#xff0c;并且在 Android 上已经可以正常运行。 这篇文章我们将讲解如何将其从使用 jetpack compose 修改为使用 compose-jb 从而实现跨平台。 老规矩&#xff0c;先看效果图&#xff1a; 可以看到…

063:cesium设置带边界线材质(material-7)

第063个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中设置带边界折线材质,请参考源代码,了解PolylineOutlineMaterialProperty的应用。 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共89行)相关API参考…

Python-matplotlib中的pie(饼)图

Python-matplotlib中的pie&#xff08;饼&#xff09;图 %matplotlib inline import matplotlib.pyplot as pltm 51212 f 40742 m_perc m/(mf) f_perc f/(mf)colors [navy,lightcoral] labels ["Male","Female"]plt.figure(figsize(8,8)) paches,te…

为什么不胜任的人,反而获得晋升?

作者| Mr.K 编辑| Emma 来源| 技术领导力(ID&#xff1a;jishulingdaoli) 也许你有过这样的经历&#xff0c;自己勤勤恳恳地干活&#xff0c;每个月却只拿着微薄的薪水&#xff0c;有些人明明无法胜任工作&#xff0c;却像坐了火箭一样飞速晋升。这种现象在现实生活中无处不在…

3699元还配同价位最好屏幕!Redmi Book 14评测:几乎完美的“水桶”轻薄本

一、前言&#xff1a;4K价位最好屏幕 不久前&#xff0c;有网友让我推荐一台4000元价位的轻薄本&#xff0c;笔者直接选了一台搭载i5-13500H处理器且价格仅售4299元的某一线品牌产品。 但是&#xff0c;事后才发现不对&#xff0c;因为这款极具性价比的笔记本竟然用了45%NTSC色…

MIT6.824 lecture5上课笔记(涉及到Lab2A)- Go threads and raft

总结&#xff1a;本节课讲解了一些会在lab2中使用到的go的多线程技巧&#xff0c;会给一些简单的demo&#xff0c;lab2中可能会借鉴这些demo。 详细的Lab2 raft算法实现源码&#xff0c;请参考我的个人仓库&#xff08;记得点颗星星&#xff09;, 配合readme食用更佳。 MIT6.…

ChatGPT 使用 拓展资料:如何善用大语言模型的推理能力?

ChatGPT 使用 拓展资料:如何善用大语言模型的推理能力?

ChatGPT Plus 会员续费扣款失败如何处理

扣款失败 笔者由于开通 ChatGPT Plus 会员比较早&#xff0c;3月26日一个月就到期了&#xff0c;但是最近几天注意到&#xff0c;虚拟卡上也没有提醒我扣费&#xff0c;还是能继续使用 GPT-4.0&#xff0c;就很奇怪&#xff0c;于是就研究了一番。 PS: 如果有小伙伴还不会开通 …