Linux网络编程:多路转接--select

news2024/9/19 19:42:20

1. 初识select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
  • select只负责等待,可以等待多个fd,select本身无数据拷贝的能力,拷贝需要read、write来完成

2. select接口

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tomeval *timeout);

参数:nfds:select要监视多个fd中值最大的fd+1

           其余的都是输入输出型参数

           timeout:nullptr:阻塞时等待,直到有一个fd有数据

                            struct timeval timeout={0,0},非阻塞式等待

                                                                    {5,0},5s内阻塞式,超过5s非阻塞返回一次,5s内有任意fd就绪,则会直接返回timeout返回的为剩余的时间

返回值:>0:返回几就是有几个fd就绪了

              =0:超时返回

              <0:select调用失败 

select关心的事件值有三类:读、写、异常 -- 对于任意的fd都是如此

fd_set:位图,表示文件描述符的集合

readfds:

        输入:表示用户告诉内核,你要帮我关心集合readfds中的所有的fd的读事件----哪些fd上的读事件内核你要关心。比特位的位置目标是fd的数值;比特位的内容,表示是否关心。

        输出:return>0时,内核告诉用户,你所关心的用户fd中,有哪些fd已经就绪了---比特位的位置表示fd的数值;比特位的内容表示哪些fd上面对应的事件已经就绪了。

通过上面的方式,让用户和内核沟通,知晓对方所关心的fd。

其余两个参数也是同样的道理。

位图操作:

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的全部位
 

3. 细节

  • listensocksock首先交给select,listensock的链接就绪事件==读事件就绪(因为listensock是通过三次握手成功后得到的,也就是表明客户端要想服务端发消息)
  • accept在没有链接时,会等待(阻塞式的),所以需要将listensock交给select,否则就跟阻塞式IO相同了
  • 编写代码时需要自己维护一个存储所有可用fd的数组(_fdArray),可以同容器、数组、动态数组等实现
  • fd是有上限的:sizeof(fd_set)*8 = 1024
  • 每次select前,都要找到最大的fd(遍历fd数组),并将合法的fd设置大rfds中
  • accept后,产生新的sock,再将其加入(加入到fd数组中未被设置过的位置,若数组满就关闭当前的sock)到fd数组中,让select来处理。

4. demo代码实现 

下面的代码只考虑了写事件。

makefile

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

err.hpp

#pragma once

#include <iostream>

enum                                                                                                                                                  
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

log.hpp

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>

using namespace std;

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3  // 出错可运行
#define FATAL   4  // 致命错误

const char* to_levelStr(int level)
{
    switch ((level))
    {
    case DEBUG: return "DEBUG";
    case NORMAL: return "NORMAL";
    case WARNING: return "WARNING";
    case ERROR: return "ERROR";
    case FATAL: return "FATAL";
    default: return nullptr;
    }
}

void logMessage(int level, const char* format, ...) // ... 可变参数列表 
{
#define NUM 1024
    char logPreFix[NUM];
    snprintf(logPreFix, sizeof(logPreFix), "[%s][%ld][pid: %d]", to_levelStr(level), (long int)time(nullptr), getpid());

    char logContent[NUM];
    va_list arg;
    va_start(arg, format);

    vsnprintf(logContent, sizeof(logContent), format, arg);
    
    cout << logPreFix << logContent << endl;
    
   
}

sock.hpp

#pragma once

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

#include "log.hpp"
#include "err.hpp"


class Sock
{
    const static int gbacklog = 32;
public:
    static int Socket()
    {
        // 1.创建socket文件套接字对象    
        int sock = socket(AF_INET, SOCK_STREAM, 0); // 第二个参数与UDP不同    
        if (sock < 0)    
        {    
            // 创建套接字失败    
            logMessage(FATAL, "created socket error!");    
            exit(SOCKET_ERR);    
        }    
        logMessage(NORMAL, "created socket success: %d!", sock);    

        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, int port)
    {
        // 2.bind绑定自己的网络信息
        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 socket error!");
            exit(BIND_ERR);
        }
        logMessage(NORMAL, "bind socket success!");
    }

    static void Listen(int sock)
    {
        // 3.设置socket 为监听状态
        if (listen(sock, gbacklog) < 0) // 第二个参数backlog后面会讲 5的倍数
        {
            logMessage(FATAL, "listen socket error!");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL, "listen socket success!");
    }

    static int Accept(int listensock, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock = accept(listensock, (struct sockaddr *)&peer, &len); // sock 和client进行通信
        if (sock < 0) 
            logMessage(ERROR, "accept error, next!");
        else
        { 
            logMessage(NORMAL, "accept a new link success, get new sock: %d!", sock); // ?
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }
        return sock;
    }
};

selectServer.hpp

#pragma once

#include <string>
#include <iostream>
#include <functional>
#include "sock.hpp"
#include "log.hpp"
#include "err.hpp"

namespace select_ns
{
    static const int defaultport = 8080;
    static const int fd_num = sizeof(fd_set) * 8;
    static const int defaultfd = -1;

    using func_t = std::function<std::string (const std::string&)>;

    class SelectServer
    {
    public:
        SelectServer(func_t func, int port = defaultport)
            : _port(port), _listenSock(-1), _fdArray(nullptr), _func(func)
        {
        }

        void Print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < fd_num; i++)
            {
                if (_fdArray[i] != defaultfd)
                    std::cout << _fdArray[i] << " ";
            }
            std::cout << std::endl;
        }

        void Accepter(int listenSock)
        {
            // 走到这里accept不会阻塞 listensock套接字已经就绪了
            string clientIp;
            uint16_t clientPort = 0;
            int sock = Sock::Accept(listenSock, &clientIp, &clientPort);
            if (sock < 0)
                return;
            logMessage(NORMAL, "accept success [%s:%d]", clientIp.c_str(), clientPort);

            // sock 我们能直接recv/read吗?--不能 整个代码 只有select有资格检测事件是否就绪
            // 将新的sock交给select
            // 将新的sock托管给select的本质,将sock添加到_fdArray数组中
            int i;
            for (i = 0; i < fd_num; i++)
            {
                if (_fdArray[i] != defaultfd)
                    continue;
                else
                    break;
            }
            if (i == fd_num)
            {
                logMessage(WARNING, "server is full, please wait!");
                close(sock);
            }
            else
            {
                _fdArray[i] = sock;
            }
            Print();
        }

        void Recver(int sock, int pos)
        {
            // 1.读取
            // 这样读取有问题!不能保证是否读取到一个完整的报文
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行recv时,不会被阻塞,因为走到这里时文件描述符已经就绪了
            if (s > 0)
            {
                buffer[s] = 0;
                logMessage(NORMAL, "client# %s", buffer);
            }
            else if (s == 0)
            {
                close(sock);
                _fdArray[pos] = defaultfd;
                logMessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(sock);
                _fdArray[pos] = defaultfd;
                logMessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            // 2.处理request
            std::string response = _func(buffer);
            
            // 3.返回response
            // write
            write(sock, response.c_str(), response.size());
        }

        // handler event 中 不仅仅是有一个fd就绪,可能有多个
        // 我们的select只处理了read
        void HandlerReadEvent(fd_set &rfds)
        {
            for (int i = 0; i < fd_num; i++)
            {
                // 过滤掉非法的fd
                if (_fdArray[i] == defaultfd)
                    continue;

                // 下面的为正常的fd
                // 正常的fd不一定就绪
                // 目前一定是listen套接字
                if (FD_ISSET(_fdArray[i], &rfds) && _fdArray[i] == _listenSock)
                    Accepter(_listenSock);
                else
                    Recver(_fdArray[i], i);
            }
        }

        void initServer()
        {
            _listenSock = Sock::Socket();
            Sock::Bind(_listenSock, _port);
            Sock::Listen(_listenSock);
            // logMessage(NORMAL, "creat socket..");
            _fdArray = new int[fd_num];
            for (int i = 0; i < fd_num; i++)
                _fdArray[i] = defaultfd;
            _fdArray[0] = _listenSock; // 不变了
        }

        void start()
        {
            for (;;)
            {
                fd_set rfds;
                FD_ZERO(&rfds);
                int maxfd = _fdArray[0];
                for (int i = 0; i < fd_num; i++)
                {
                    if (defaultfd == _fdArray[fd_num])
                        continue;
                    FD_SET(_fdArray[i], &rfds); // 将合法fd全部添加到读文件描述符集中
                    if (maxfd < _fdArray[i])
                        maxfd = _fdArray[i];
                }

                // struct timeval timeout = {1, 0};
                // int n = select(_listenSock+1, &rfds, nullptr, nullptr, &timeout);
                // 一般而言 要使用select 需要程序员维护一个保存所有合法fd 的数组!
                int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));
                    break;
                default:
                    // 说明有时间就绪了,目前只有一个监听事件就绪
                    logMessage(NORMAL, "get a new link...");
                    HandlerReadEvent(rfds);
                    break;
                }
                sleep(1);

                // 下面为阻塞式写法
                // std::string clientIp;
                // uint16_t clientPort = 0;
                // int sock = Sock::Accept(_listenSock, &clientIp, &clientPort);
                // if(sock < 0) continue;
                // // 开始进行服务器处理逻辑
            }
        }

        ~SelectServer()
        {
            if (_listenSock < 0)
                close(_listenSock);
            if (_fdArray)
                delete[] _fdArray;
        }

    private:
        int _port;
        int _listenSock;
        int *_fdArray;
        func_t _func;
    };
}

main.cc

#include "selectServer.hpp"
#include "err.hpp"
#include <memory>

using namespace std;
using namespace select_ns;

static void usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " prot" << std::endl; 
}

std::string transaction(const string& request)
{
    return request;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    unique_ptr<SelectServer> svr(new SelectServer(transaction, atoi(argv[1])));

    svr->initServer();
    svr->start();

    return 0;
}

实验结果如下 :

 使用telnet作为客户端向服务器发消息时,收到了服务端的响应。

5. select优缺点

  • select能同时等待的文件fd是有上限的,除非修改内核,否则无法解决
  • select必须借助第三方数组来维护合法的fd
  • select的大部分参数是输入输出型的,调用select前要重新设置所有的fd,调用之后还要检查更新所有的fd,遍历是有成本的。
  • select为什么第一个参数是最大fd+1? 确定遍历范围--内核层面
  • select采用位图,用户和内核之间通信,要来回进行数据拷贝,会有拷贝成本

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

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

相关文章

内容创作者福音,4款文章改写神器轻松提升文章质量

在信息爆炸的时代&#xff0c;内容创作成为了连接世界的重要桥梁。作为一名专业创作者&#xff0c;我深知保持内容原创性和高质量的重要性。然而&#xff0c;灵感有时会枯竭&#xff0c;改写文章成为一项耗时且艰巨的任务。幸运的是&#xff0c;市面上有一些文章改写神器&#…

Flask+LayUI开发手记(四):弹出层实现增删改查功能

在上一节用dataTable实现数据列表时&#xff0c;已经加了表头工具栏和表内工具栏&#xff0c;栏内的按钮功能都是用来完成数据的增删改查了&#xff0c;这又分成两类功能&#xff0c;一类是删除或设置&#xff0c;这类功能简单&#xff0c;只需要选定记录&#xff0c;然后提交到…

Flutter 自动化测试 - 集成测试篇

Flutter集成测试 Flutter官方对Flutter应用测试类型做了三个阶段划分&#xff0c;分别为Unit&#xff08;单元&#xff09;测试、Widget&#xff08;组件&#xff09;测试、Integration&#xff08;集成&#xff09;测试。按照维护成本来看的话从左到右依次增高&#xff0c;按照…

预测癌症免疫治疗反应-TIDE数据库学习及知识整理

TIDE&#xff08;Tumor Immune Dysfunction and Exclusion&#xff09; 是一个用于预测癌症患者对免疫检查点抑制剂&#xff08;如PD-1/PD-L1抑制剂&#xff09;反应的算法。研究者通过检测肿瘤建模队列中每个基因的表达与效应性毒性T淋巴细胞(CTL)浸润水平的相互关系及对生存情…

Open3D 近似点体素滤波(36)

Open3D 近似点体素滤波(36) 一、算法介绍二、算法实现1.代码2.效果一、算法介绍 这个算法也是体素滤波, 它保留的点是近似点,也就是新的点,原始点云中对应位置是不存在这些点的。其他的看着类似,下面是代码,滤波抽稀结果 二、算法实现 1.代码 代码如下(示例): …

学习文件IO,让你从操作系统内核的角度去理解输入和输出(Java实践篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

【在Linux世界中追寻伟大的One Piece】应用层协议HTTP

目录 1 -> HTTP协议 2 -> 认识URL 2.1 -> urlencode和urldecode 3 -> HTTP协议请求与响应格式 3.1 -> HTTP请求 3.2 -> HTTP响应 4 -> HTTP的方法 4.1 -> HTTP常见方法 5 -> HTTP的状态码 6 -> HTTP常见Header 7 -> 最简单的HTTP服…

Linux系统报错“version ‘GLIBC_2.34‘ not found”解决方法

注意&#xff0c;此文章慎用&#xff0c;glibc不可随意升级&#xff0c;可能导致系统崩溃 一、查看版本 ldd --version 二、添加高版本源 sudo vi /etc/apt/sources.list.d/my.list 进入编辑页面 "i"键进入插入模式 输入源 deb http://th.archive.ubuntu.com/…

【信创】推荐一款超级好用的文件同步备份工具 _ 统信 _ 麒麟 _ 方德

往期好文&#xff1a;【信创】统信UOS打包工具介绍与使用教程 Hello&#xff0c;大家好啊&#xff01;今天给大家推荐一款在Linux系统上超级好用的文件同步和备份工具——FreeFileSync。无论是在日常工作还是数据管理中&#xff0c;文件同步和备份都是至关重要的任务。FreeFile…

【自动驾驶】控制算法(五)连续方程离散化与离散LQR原理

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

QT6 setCentralWidget 和 takeCentralWidget

qt6 中&#xff0c;初始化界面完成之后&#xff0c;可以使用setCentralWidget 设置当前的widget为中心页面 如果你存在多个widget想要多个切换 如果存在widget1 和 widget2 在初始化的时候 setCentralWidget(widget1)触发操作切换到 widget2 如果没有先takeCentralWidget 直…

13.深入解析ThreadPoolExecutor线程池

ThreadPoolExecutor线程池 线程池简介线程池的使用创建线程池ThreadPoolExecutor——推荐使用线程池的核心参数 Executors——不推荐使用 提交任务如何执行批量任务如何执行定时、延时任务如何执行周期、重复性任务 关闭线程池线程池的参数设计分析核心线程数(corePoolSize)最大…

EEMD-MPE-KPCA-BiLSTM、EEMD-MPE-BiLSTM、EEMD-PE-BiLSTM故障识别、诊断(Matlab)

EEMD-MPE-KPCA-BiLSTM(集合经验分解-多尺度排列熵-核主元分析-双向长短期网络)故障识别、诊断&#xff08;Matlab) 目录 EEMD-MPE-KPCA-BiLSTM(集合经验分解-多尺度排列熵-核主元分析-双向长短期网络)故障识别、诊断&#xff08;Matlab)效果一览基本介绍程序设计参考资料 效果一…

RK3588人工智能开发----【1】初识NPU

NPU 的诞生&#xff01; 随着人工智能和大数据时代的到来&#xff0c;传统嵌入式处理器中的CPU和GPU逐渐无法满足日益增长的深度学习需求。为了应对这一挑战&#xff0c;在一些高端处理器中&#xff0c;NPU&#xff08;神经网络处理单元&#xff09;也被集成到了处理器里。NPU的…

【GNSS射频前端】MA2769初识

MAX2769 芯片概述&#xff1a; MAX2769是一款单芯片多系统GNSS接收器&#xff0c;采用Maxim的低功耗SiGe BiCMOS工艺技术。集成了包括双输入低噪声放大器&#xff08;LNA&#xff09;、混频器、图像拒绝滤波器、可编程增益放大器&#xff08;PGA&#xff09;、压控振荡器&#…

note38:tdsql数据库迁移

数据迁移过程中遇到的具体问题&#xff1a; ①提供给系统团队的表结构与生产不一致&#xff0c;导致脚本报错。因为历史遗留问题&#xff0c;存在部分直接在生产环境更改字段长度或添加索引的情况&#xff0c;导致测试环境和生产环境的表结构不同步。 今后所有生产的变动&…

Vulkan 学习(5)---- Vulkan 内存分配

目录 Overview枚举内存信息分配内存内存映射 Overview Vulkan 将内存管理的工作交给了开发者自己负责&#xff0c;如何分配内存&#xff0c;如何指定内存策略都是由开发者自己决定的&#xff0c;当然处理问题也是由开发者自己负责的 Vulkan 将内存划分为两大类&#xff1a;主…

Android自定义简单仿QQ运动步数进展圆环

实现效果主要效果分为三个部分&#xff1a; 1.固定蓝色的大圆弧 color borderWidth 2.可以变化的小圆弧(红色) color borderWidth 3.中间的步数文字 color textSize drawArc方法 startAngle 确定角度的起始位置 sweepAngle 确定扫过的角度 useCenter 是否使用中心&#xff1a…

MyBatis XML配置文件(下)

MyBatis的开发有两种方式&#xff1a;1、注解 2、XML。使用MyBatis的注解方式&#xff0c;主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能&#xff0c;建议使用XML来配置映射语句&#xff0c;也就是将SQL语句写在XML配置文件中。 MyBatis XML开发的方式需要以…

UE5学习笔记17-让人物的视线和鼠标移动时的方向一致,并且不让人物模型旋转,只改变视线方向

一、创建标准动画帧 1.我想让人物在装备武器后根据鼠标的移动方向改变人物的视线方向&#xff0c;并且人物模型不会改变朝向 2.我的动画中存在一个四个方向瞄准的动画&#xff0c;将左下&#xff0c;坐上&#xff0c;左转&#xff0c;右上&#xff0c;右下&#xff0c;右转&…