多路转接(上)——select

news2024/12/23 1:38:00

目录

一、select接口

1.认识select系统调用

2.对各个参数的认识

二、编写select服务器

1.两个工具类

2.网络套接字封装

3.服务器类编写

4.源文件编写

5.运行


一、select接口

1.认识select系统调用

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

头文件:sys/time.h、sys/types.h、unistd.h

功能:select负责IO中多个描述符等的那部分,该函数会在描述符的读写异常事件就绪时,提醒进程进行处理。

参数:后面讲。

返回值:返回值大于0表示有相应个文件描述符就绪,返回值等于0表示没有文件描述符就绪,超时返回,返回值小于0表示select调用失败。

2.对各个参数的认识

(1)struct timeval* timeout

它是一个struct timeval类型的类指针,定义如下:

struct timeval
{
    time_t tv_sec;        /* Seconds. */
    suseconds_t tv_usec;  /* Microseconds. */
};

内部有两个成员,第一个是秒,第二个是微秒。

这个timeout如果传参nullptr,默认select阻塞等待,只有当一个或者多个文件描述符就绪时才会通知上层进程去读取数据。

参数如果传入struct timeval timeout = {0, 0},秒和微秒时间都设置成0,此时select就使用非阻塞等待,需要程序员编写轮询检测代码。

参数如果设置了具体值,如struct timeval timeout = {5, 0},时间设置成5秒,select就会在5秒内阻塞等待,如果在5秒内有文件描述符就绪,则通知上层;如果没有则超时返回。

(2)int nfds

表示要等待的所有文件描述符中的最大值加一。

假设要等待3个文件描述符,分别为3、4和7,则传参时就需要传7+1=8给nfds。

(3)fd_set

fd_set是一个等待读取就绪文件描述符的位图。

它的每一个比特位代表一个文件描述符,比特位的状0和1表示该比特位是否被select监听。

下面就是fd_set位图的示意图,表示偏移量为1、3、5的三个文件描述符需要被select监视。

fd_set类型的大小为128字节,每个字节有8个比特位,所以fd_set类型能包含1024个比特位,也表明select最多能监视1024个文件描述符。

虽然我们知道fd_set属于位图结构,但是我们并不清楚其内部实现。

所以在对位图数据进行增删 查改时一定要使用系统提供的增删查改接口。

  • FD_ZERO(fd_set *fdset);——将fd_set清零,集合中不含任何文件描述符,可用于初始化
  • FD_SET(int fd, fd_set *fdset);——将fd加入fd_set集合
  • FD_CLR(int fd, fd_set *fdset);——将fd从fd_set集合中移除
  • FD_ISSET(int fd, fd_set *fdset);——检测fd是否在fd_set集合中,不在则返回0

(4)fd_set* reads

fd_set* reads、fd_set* writefds、fd_set* exceptfds中间的这三个参数属于输出型参数。

在这里我以fd_set* reads为例进行讲解。

fd_set* reads表示读位图,传递的参数表示需要被监视的文件描述符,而且select只关心是这些文件描述符内否有数据需要被读取。

假如说,我们定义了一个fd_set变量使用FD_SET将文件描述符1、3、5填入变量,最后将该变量的指针传入函数。

在select正常返回或超时返回时,它会更改这个变量。

比方说,select调用完成后将位图改为下面的样式,表明文件描述符1、3准备好了,可以由系统调用去读取。由于两个文件描述符就绪,所以返回值为2。

在下次进行select调用时,我们还能再次修改该位图,增加或减少需要监听的文件描述符。

select再次返回时,该位图依旧会被修改,从而指示在这一次调用后哪些文件描述符已经准备就绪。

也就是说,传参时这个位图代表需要监听的描述符,调用返回时这个位图代表已就绪的文件描述符。

fd_set* reads与fd_set* writefds、fd_set* exceptfds在使用上是一样的,只不过fd_set* writefds只关心进程向文件描述符中写数据的操作,而fd_set* exceptfds只关心该文件描述符是否出现了错误。

它们也会以同样的方式修改自己对应的fd_set变量,从而达到通知进程的目的。

二、编写select服务器

1.两个工具类

代码需要使用两个工具类,err.hpp储存所有的错误码,原来打印日志的log.hpp也继续使用。

err.hpp

#pragma once

#include<iostream>

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

log.hpp

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>

//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define  ERROR_FILE "./error.txt"

//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

//将运行等级转换为字符串
const char* to_string(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, ...)
{
    //输出到屏幕
    char logprefix[1024];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
    
    char logcontent[1024];
    va_list arg;//可变参数列表
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout << logprefix << logcontent << std::endl;
    //输出到文件中
    //打开两个文件
    FILE* log = fopen(LOG_FILE, "a");
    FILE* err = fopen(ERROR_FILE, "a");
    if(log != nullptr && err != nullptr)
    {
        FILE* cur = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING)
            cur = log;
        if(level == ERROR || level == FATAL)
            cur = err;
        if(cur)
            fprintf(cur, "%s%s\n", logprefix, logcontent);
        fclose(log);
        fclose(err);
    }
}

2.网络套接字封装

将之前写的socket、bind、accept等函数封装到一个Sock类中。

#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"log.hpp"
#include"err.hpp"

class Sock
{
private:
    static const int backlog = 32;//队列长度为32
public:
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", listensock);//创建套接字成功,打印让用户观察到

        //打开端口复用保证程序退出后可以立即正常启动
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

        return listensock;
    }

    static void Bind(int listensock, int port)
    {
        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        
        if(bind(listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
    }

    static void Listen(int listensock)
    {
        //listen设置socket为监听模式
        if(listen(listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        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);
        
        if(sock < 0)
        {
            logmessage(ERROR, "accept fail");//接收新文件描述符失败
        }
        else
        {
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }
};

3.服务器类编写

服务器类的相关函数都定义在selectserver.hpp内,而且我们实现的select服务器只关心读事件。

我大致说一下运行流程:在构造对象后,initserver创建套接字并初始化成员变量,start函数循环调用select函数,然后我们筛选出有效的有读事件就绪的描述符放入_fdarray数组,然后使用handler_read函数处理事件。

最后,在handler_read函数内判断描述符是普通描述符还是监听描述符。

普通描述符读事件就绪表示需要读取数据,我们实现一个Receiver进行处理;监听描述符读事件就绪表示有链接需要接收,我们实现一个Accepter函数进行处理。

#pragma once
#include<iostream>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<functional>
#include"sock.hpp"

namespace select_func
{
    static const int default_port = 8080;//默认端口号为8080
    static const int fdnum = sizeof(fd_set) * 8;//最大端口号为1024
    static const int default_fd = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素

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

    class SelectServer
    {
    public:
        SelectServer(func_t func, int port = default_port)
            :_listensock(-1)
            ,_port(default_port)
            ,_fdarray(nullptr)
            ,_func(func)
        {}

        ~SelectServer()
        {
            if(_listensock > 0)
                close(_listensock);//关闭监听文件描述符
            if(_fdarray)
                delete []_fdarray;//释放存储文件描述符的数组
        }

        void initserver()
        {
            //创建listen套接字,绑定端口号,设为监听状态
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            //构建一个储存所有需要管理的文件描述符的数组,并把数组所有元素置为-1
            _fdarray = new int[fdnum];
            for(int i = 0; i<fdnum; ++i)
            {
                _fdarray[i] = default_fd;
            }
            //将listen套接字放在第一个,在程序运行的全过程中都不会被修改
            _fdarray[0] = _listensock;
        }

        void start()
        {
            while(1)
            {
                //填写位图
                fd_set fds;
                FD_ZERO(&fds);
                int maxfd = _fdarray[0];//最初,012三个描述符默认打开,3为监听描述符,所以描述符的最大值为3
                for(int i = 0; i<fdnum; ++i)
                {
                    if(_fdarray[i] != default_fd)//筛选出有效的文件描述符
                    {
                        FD_SET(_fdarray[i], &fds);//将该文件描述符加入位图
                        if(_fdarray[i] > maxfd)//fdarray储存有新增加的有效文件描述符
                            maxfd = _fdarray[i];//maxfd需要根据元素增大
                    }
                }
                //logmessage(NORMAL, "maxfd:%d", maxfd);

                //调用select
                //struct timeval timeout = {1, 0};
                int n = select(maxfd+1, &fds, nullptr, nullptr, nullptr);//非阻塞调用
                switch(n)
                {
                    case 0://没有描述符就绪
                        logmessage(NORMAL, "time out.");
                        break;
                    case -1://select出错了
                        logmessage(ERROR, "select error, error code:%d %s", errno, strerror(errno));
                        break;
                    default://有描述符就绪(获取链接就属于读就绪)
                        //logmessage(NORMAL, "server get new tasks.");
                        handler_read(fds);//处理数据
                        break;
                }
            }
        }

        void Accepter()
        {
            //走到这里说明等的过程select已经完成了
            std::string clientip;
            uint16_t clientport = 0;
            //select只负责等,接收链接还是需要accept,但是这次调用不会阻塞了
            int sock = Sock::Accept(_listensock, &clientip, &clientport);
            if (sock < 0)//接收出错不执行
                return;
            logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);

            //链接已经被建立,新的描述符通信产生
            //这个描述符我们也要再次插入数组
            int i = 0;
            for(i = 0; i<fdnum; ++i)
            {
                if(_fdarray[i] == default_fd) 
                    break;
            }
            if(i == fdnum)//数组满了
            {
                logmessage(WARNING, "server if full, please wait");
                close(sock);//关闭该链接
            }
            else
                _fdarray[i] = sock;//将数据插入数组

            print_list();//打印数组的内容
        }

        void Receiver(int sock, int pos)
        {
            //接收客户端发来的数据
            char buffer[1024];
            ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);
            if (n > 0)
            {
                buffer[n] = 0;//在末尾加上/0
                logmessage(NORMAL, "client# %s", buffer);
            }
            else if (n == 0)
            {
                close(sock);
                _fdarray[pos] = default_fd;
                logmessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(sock);
                _fdarray[pos] = default_fd;
                logmessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            //使用回调函数处理数据
            std::string response = _func(buffer);

            //发回响应
            write(sock, response.c_str(), response.size());
        }

        void handler_read(fd_set& fds)
        {
            //我们将读取数据的处理分为两种:
            //第一种是获取到了新链接
            //第二种是有数据需要被读取
            for(int i = 0; i<fdnum; ++i)
            {
                //筛选出有效的文件描述符
                if(_fdarray[i] != default_fd)
                {
                    //listensock就绪表示进程获取到了新链接
                    if(FD_ISSET(_fdarray[i], &fds) && _fdarray[i] == _listensock)
                        Accepter();//建立链接
                    //其他普通文件描述符就绪
                    else if(FD_ISSET(_fdarray[i], &fds))
                        Receiver(_fdarray[i], i);//接收数据
                }
            }
        }

        void print_list()
        {
            std::cout << "fd list:" << std::endl;
            for(int i = 0; i<fdnum; ++i)
            {
                if(_fdarray[i] != default_fd)
                    std::cout << _fdarray[i] << " ";
            }
            std::cout << std::endl;
        }

    private:
        int _listensock;
        int _port;
        int* _fdarray;
        func_t _func;
    };
}

4.源文件编写

还是用老方法,initserver初始化,start开始运行,unique_ptr管理对象。

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

using namespace std;
using namespace select_func;

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

string transaction(const string& str)
{
    return str;
}

int main(int argc, char *argv[])
{
    unique_ptr<SelectServer> p(new SelectServer(transaction));
    p->initserver();
    p->start();
    return 0;
}

5.运行

为了省事,我们直接使用telnet作为客户端即可。

下面我们对服务器进行连接,发送数据,查看其运行。

注意,telnet发送数据需要按Ctrl+],然后出现telnet>后,按Enter键后才能输入数据并按Enter发送。最后,如果向退出telnet,同样按Ctrl+],然后输入q或者quit,就能退出了。

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

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

相关文章

服务器部署 Nacos 获取不到配置浏览器可以访问

服务器部署 Nacos 获取不到配置浏览器可以访问 &#x1f4d4; 千寻简笔记介绍 千寻简笔记已开源&#xff0c;Gitee与GitHub搜索chihiro-notes&#xff0c;包含笔记源文件.md&#xff0c;以及PDF版本方便阅读&#xff0c;且是用了精美主题&#xff0c;阅读体验更佳&#xff0c…

Jmeter+ant+jenkins接口自动化测试

平台简介 一个完整的接口自动化测试平台需要支持接口的自动执行&#xff0c;自动生成测试报告&#xff0c;以及持续集成。Jmeter 支持接口的测试&#xff0c;Ant 支持自动构建&#xff0c;而 Jenkins 支持持续集成&#xff0c;所以三者组合在一起可以构成一个功能完善的接口自动…

第四章:人工智能深度学习教程-激活函数(第三节-Pytorch 中的激活函数)

在本文中&#xff0c;我们将了解 PyTorch 激活函数。 目录 什么是激活函数以及为什么使用它们&#xff1f; Pytorch 激活函数的类型 ReLU 激活函数&#xff1a; Python3 Leaky ReLU 激活函数&#xff1a; Python3 S 形激活函数&#xff1a; Python3 Tanh 激活函数&am…

计算机丢失mfc140.dll是什么意思?附送修复教程

mfc140.dll是Microsoft Foundation Classes&#xff08;MFC&#xff09;库的一部分&#xff0c;是一种动态链接库&#xff08;DLL&#xff09;文件。MFC库是Microsoft提供的一种C编程框架&#xff0c;它为开发者提供了许多方便的工具和类&#xff0c;以简化Windows应用程序的开…

兴达易控485Modbus转profinet网关应用于搅拌站的配置案例

在兴达易控485Modbus转profinet网关(XD-MDPN100)的帮助下&#xff0c;该搅拌站采用了双行星动力搅拌桨混合器与PLC和变频器进行通信&#xff0c;从而实现对变频器的精确控制&#xff0c;大大提高了搅拌过程的稳定性和效率。 该方案还具有高度的灵活性和可扩展性&#xff0c;使…

Oracle11g for centos7

准备工作 x86 centos7 oracle11G 环境搭建 配置好虚拟机&#xff0c;网络通畅&#xff0c;建议最少3G内存。 安装依赖 yum install binutils compat-libstdc-33 glibc* ksh gcc gcc-c libgcc libstdc* libaio libaio-devel libXext libX11 libXau libxcb libXi make sy…

计算机丢失mfc100.dll如何恢复,详细解析mfc100.dll文件丢失解决方法

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;比如“mfc100.dll丢失”。这是因为动态链接库&#xff08;DLL&#xff09;文件是Windows操作系统的重要组成部分&#xff0c;它们包含了许多程序运行所需的函数和数据。当这些DLL文件丢失或损坏时&#x…

2023年腾讯云双11活动入口在哪里?

2023年双11腾讯云推出了11.11大促优惠活动&#xff0c;下面给大家分享腾讯云双11活动入口、活动时间、活动详情&#xff0c;希望可以助力大家轻松上云&#xff01; 一、腾讯云双11活动入口 活动地址&#xff1a;点此直达 二、腾讯云双11活动时间 腾讯云双11活动时间跨度很长…

【C++干货铺】STL简述 | string类的使用指南

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 什么是STL STL的版本 STL的六大组件 STL的缺陷 string类 C语言中的字符串 标准库中的string类 string类常用的接口使用指南 string类中常见的构造 strin…

【Linux精讲系列】——vim详解

​作者主页 &#x1f4da;lovewold少个r博客主页 ⚠️本文重点&#xff1a;c入门第一个程序和基本知识讲解 &#x1f449;【C-C入门系列专栏】&#xff1a;博客文章专栏传送门 &#x1f604;每日一言&#xff1a;宁静是一片强大而治愈的神奇海洋&#xff01; 目录 目录 ​作者…

Docker 学习路线 13:部署容器

部署容器是使用Docker和容器化管理应用程序更高效、易于扩展和确保跨环境一致性性能的关键步骤。本主题将为您概述如何部署Docker容器以创建和运行应用程序。 概述 Docker容器是轻量级、可移植且自我包含的环境&#xff0c;可以运行应用程序及其依赖项。部署容器涉及启动、管…

vivado时序分析-2时序分析关键概念

时序分析关键概念 1、最大和最小延迟分析 时序分析属静态验证 &#xff0c; 旨在验证在硬件上加载并运行设计后 &#xff0c; 其时序行为的可预测性。它会将各种制造和环境变化因素组合到延迟模型中并按时序角及其变化量加以分组&#xff0c; 将所有这些要素一并纳入考量范围。…

XSS 跨站点脚本漏洞详解

文章目录 漏洞概述XSS漏洞原理xss漏洞危害xss漏洞验证XSS漏洞分类反射型存储型DOM型 固定会话攻击原理简单xss注入复现 XSS 攻防xss构造方法利用标签符号<>事件响应javascript伪协议其他标签 XSS 变形方式xss防御黑白名单策略输入过滤 案例XSS 盲打 漏洞概述 ​ 跨站点脚…

简单理解 Sentinel 滑动窗口实现原理

theme: serene-rose 1. 引言 Hi&#xff0c;你好&#xff0c;我是有清 对于刚经历过双 11 的电商人来说&#xff0c;限流这个词肯定在 10.24 的晚 20.00 点被提起过 限流作为保护我们系统不被流量冲垮的手段之一&#xff0c;建议每个电商人深入了解学习&#xff0c;什么&#x…

ansible-第二天

ansible 第二天 以上学习了ping、command、shell、script模块&#xff0c;但一般不建议使用以上三个&#xff0c;因为这三个模块没有幂等性。举例如下&#xff1a; [rootcontrol ansible]# ansible test -a "mkdir /tmp/1234"[WARNING]: Consider using the file …

GitHub上的开源工业软件

github上看到一个中国人做的流体力学开源介绍&#xff0c;太牛了&#xff01; https://github.com/clatterrr/FluidSimulationTutorialsUnity 先分析一下工业仿真软件赛道 工业仿真软件的赛道和产品主要功能如下&#xff1a; 1. 工艺仿真赛道&#xff1a; - 工厂布局优化&am…

人工智能模型转ONNX 连接摄像头使用ONNX格式的模型进行推理

部署之后模型的运算基本上能快5倍。本地部署之后&#xff0c;联网都不需要&#xff0c;数据和隐私不像在网上那样容易泄露了。 模型部署的通用流程 各大厂商都有自己的推理工具。 训练的归训练&#xff0c;部署的归部署&#xff0c;人工智能也分训练端和部署端&#xff0c;每一…

派金SDK接入文档

一、接入SDK 1、将sdk文件手动导入到目标项目中&#xff0c;如下图所示&#xff1a; 2、该SDK需接入其他三方广告&#xff0c;通过pod的方式接入&#xff0c;在Profile中加入如下代码&#xff1a; pod GDTMobSDK, ~> 4.14.40pod BaiduMobAdSDK, ~> 5.313pod KSAdSDK…

pytorch中常用的损失函数

1 损失函数的作用 损失函数是模型训练的基础&#xff0c;并且在大多数机器学习项目中&#xff0c;如果没有损失函数&#xff0c;就无法驱动模型做出正确的预测。 通俗地说&#xff0c;损失函数是一种数学函数或表达式&#xff0c;用于衡量模型在某些数据集上的表现。损失函数在…

数模之线性规划

线性规划 优化类问题&#xff1a;有限的资源&#xff0c;最大的收益 例子: 华强去水果摊找茬&#xff0c;水果摊上共3个瓜&#xff0c;华强总共有40点体力值,每劈一个瓜能带来40点挑衅值,每挑一个瓜问“你这瓜保熟吗”能带来30点挑衅值,劈瓜消耗20点体力值&#xff0c;问话消耗…