Linux学习之自定义协议

news2024/11/11 13:22:25

前言:

首先对于Tcp的socket的通信,我们已经大概了解了,但是其中其实是由一个小问题,我们目前是不得而知得,在我们客户端与服务端连接成功后,服务端读取来自客户端得消息,我们使用的是函数read,通过来读取accept之后的文件,从而获取信息,可是我们怎么去知道每次读取都是一个完成的报文呢?

在用户端与服务端通信的时候,用户首先写信息到发送缓冲区当中,经过网络推送到接收缓冲区,之后服务端从接收缓冲区中读取数据。在这个过程中,其实都是有Tcp自主控制的,这也就是他为什么叫传输控制协议,tcp会处理在发送过程中所有遇到的问题:发什么,什么时候发,出错了怎么办?这里我们用到的接口,如write,read,accept等都是实现用户到内核的数据拷贝。

如何保证,数据的发送是准确无误的,这就取决于tcp,而tcp怎么保证?这就需要协议的定制。

目录

前言:

协议的定制

套接字文件

protocol.hpp

Calcultor.hpp

客户端:

服务端:

主运行函数:


协议的定制

协议是一种约定,在进行socket通信时,读写都是用字符串,那么如果要传输的数据是结构化数据呢?

我们以接受发消息为例:

我们平常发的消息,不仅仅只有我们的消息的内容,其实还有时间,名字,和消息。

一般,用户发出消息,系统会将该消息写到一个固定的结构体里,在将该结构化数据转化为字符串,再通过网络发送出去,接受的时候,还是需要将字符串先转化为结构数据从,之后再访问其中成员获取消息给用户。

数据的结构化的过程我们可以简单的理解为协议的封装。

现在我们就通过编写一个网络计算机为例,将所有的知识结合起来:

套接字文件

首先是套接字文件,里面包含了客户端与服务端需要直接调用的方法,例如创建套接字,绑定,监听,连接,接受等。

//网络接口
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include <string.h>
#include<strings.h>

const int backlog=10;
const int defaultfd=-1;
enum 
{
    CREATEERROR=1,
    BINDERROR=2,
    LISTENERROR=3,
    ACCEPTERROR=4
};
class Sock
{
      public:
            Sock(const int socket=defaultfd):_sockfd()
            {}
            void Createsockfd()
            {
                //1.创建套接字文件描述符
                _sockfd=socket(AF_INET,SOCK_STREAM,0);
                if(_sockfd<0)
                {
                    std::cout<<"创建失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
                    exit(CREATEERROR);
                }
                std::cout<<"创建成功"<<std::endl;
            }
            void Bind(uint16_t port)
            {
                //初始化sockaddr_in
                struct sockaddr_in local;
                bzero(&local, sizeof(local));
                local.sin_family=AF_INET;
                local.sin_port=htons(port);
                local.sin_addr.s_addr=INADDR_ANY;   
                //2.绑定端口号
                socklen_t len=sizeof(local);
                int n=bind(_sockfd,(const struct sockaddr*)&local,len);
                if(n<0)
                {
                    std::cout<<"绑定失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
                    exit(BINDERROR);
                }
                std::cout<<"端口号绑定成功"<<std::endl;

            }
            void Listen()
            {
                //3.将套接字设置为监听状态
                int l=listen(_sockfd,backlog);
                if(l<0)
                {
                    std::cout<<"监听失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
                    exit(LISTENERROR);
                }
                std::cout<<"设置监听成功"<<std::endl;

            }            
           void Connect(std::string &ip,uint16_t &port)
           {
                //我去连接哪一个服务端

                    //获取服务端信息
                struct sockaddr_in remote;
                bzero(&remote, sizeof(remote));
                remote.sin_family=AF_INET;
                remote.sin_port=htons(port);
                inet_pton(AF_INET, ip.c_str(),&(remote.sin_addr));
                //建立连接
                socklen_t len=sizeof(remote);
                int n=connect(_sockfd,(const struct sockaddr*)&remote,len);
                if(n<0)
                {
                    std::cout<<"连接失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
                    exit(ACCEPTERROR);
                }
                std::cout<<"连接成功"<<std::endl;
              
              

           }
           int Accept(std::string *clientip,uint16_t* clientport)
           {
                
                struct sockaddr_in client;
                socklen_t clientlen=sizeof(client);
                std::cout<<"测试1。。。。。。"<<std::endl;
                int newfd=accept(_sockfd,(struct sockaddr*)&client,&clientlen);
                
                if(newfd<0)
                {
                    std::cout<<"接收失败"<<",错误码:"<<errno<<",错误信息"<<strerror(errno)<<std::endl;
                    return -1;
                }
                std::cout<<"测试2。。。。。。"<<std::endl;
                //获取ip地址与端口号
                char _ptr[32];
                inet_ntop(AF_INET, &client.sin_addr,_ptr,sizeof(_ptr));
                *clientip=std::string(_ptr);
                *clientport=ntohs(client.sin_port);
                std::cout<<"接收成功"<<std::endl;
                return newfd;
           }
             
            int Fd()
            {
                return _sockfd;
            }
            ~Sock()
            {
            
            }
            void Close()
            {
                //孙子进程提供服务

                close(_sockfd);
            }

    private:

            int _sockfd;
};

我们来看看协议封装的头文件:

protocol.hpp

主要是序列化与反序列化

#pragma once
#include <iostream>
#include <string>
using namespace std;
const std::string blankspace = " ";
const string protocol_str = "/n";
//这里我们计算的字符串 10 * 10 会被Encode为 5/n10 * 10/n
std::string Encode(std::string &content)//编码格式为 len/nx op y/n
{
    std::string package = std::to_string(content.size());
    package += protocol_str;
    package += content;
    package += protocol_str;

    return package;
}
//反之去掉/n 与长度 变为 x op y格式
bool Decode(std::string &package, std::string *content)
{
    std::size_t pos = package.find(protocol_str);
    if(pos == std::string::npos) return false;
    std::string len_str = package.substr(0, pos);
    std::size_t len = std::stoi(len_str);
    // package = len_str + content_str + 2
    std::size_t total_len = len_str.size() + len + 2;
    if(package.size() < total_len) return false;

    *content = package.substr(pos+1, len);
    // earse 移除报文 package.erase(0, total_len);
    package.erase(0, total_len);

    return true;
}

// 定制协议,这里我们打算搞得是网络计算器
// 请求
class Request
{
public:
    Request(int data1, int data2,char c) : _x(data1), _y(data2),_op(c)
    {
    }
    // 报文的读格式为 len/n a op b
    bool Serialize(string *out) // 序列化
    {
        // 转化为字符串
        // 构建报文的有效载荷
        std::string s = std::to_string(_x);
        s += blankspace;
        s += _op;
        s += blankspace;
        s += std::to_string(_y);
        *out = s;
        return true;
        //序列化  将已知的x,op,y进行第一层封装为 x op y 格式的字符串
        
    }
    bool Deserialize(const string &package) // 反序列化   已经定义好了是 len\nx op y
    {
            size_t pos=package.find(blankspace);
            if(pos==std::string::npos)
            {
                return false;
            }
            std::string x=package.substr(0,pos);

            size_t pot=package.rfind(blankspace);
            if(pot==std::string::npos)
            {
                return false;
            }
            std::string y=package.substr(pot+1);

            if(pos+2!=pot)
            {
                return false;
            }
            _op=package[pos+1];
            _x=std::stoi(x.c_str());
            _y=std::stoi(y.c_str());
            //通过反序列化取得 字符 x,y,op 并转化相应类型

    }
    ~Request()
    {
    }

public:
    int _x;
    int _y;
    char _op; // + = * / %
};
// 应答
class Response
{
public:
        Response(int result, int code) : _result(result), _code(code)
        {
        }
        ~Response()
        {
        }
    // 序列化
        bool Serialize(string *out)
        {
            //"len"\n"result code"
            // 构建有效的报文载荷
            std::string s = std::to_string(_result);
            s += blankspace;
            s += std::to_string(_code);
            *out = s;
            return true;
        }
    // 反序列化
        bool Deserialize(const std::string &in) // len\n result code 
        {
            std::size_t pos = in.find(blankspace);
             if (pos == std::string::npos)
            return false;
            std::string part_left = in.substr(0, pos);
            std::string part_right = in.substr(pos+1);

            _result = std::stoi(part_left);
            _code = std::stoi(part_right);

            return true;
        }

public:
    int _result;
    int _code; // 错误码,非0具体实际表明错误原因
};

之后根据我们初始化构造传入的参数,进行编码,传入到定制对象Request中,进行序列化,之后完成序列化,取得定制对象并传入 函数Calculator中进行计算,再根据返回的结果在进行Response对象的定制,序列化,再编码,作为报文字符串可以进行发了,服务端之后接受到到再解码,反序列化,即可。

计算转化头文件

Calcultor.hpp

主要是将传入的字符串,编码,序列化,之后调用计算,再解码,反序列化。实现计算过程。

#pragma once
#include<iostream>
#include"protocol.hpp"

class ServerCal{

    public:
    ServerCal()
    {}
    Response CalculatorOperator(const Request &req)
    {
        Response resp(0, 0);
        switch (req.op)
        {
        case '+':
            resp.result = req.x + req.y;
            break;
        case '-':
            resp.result = req.x - req.y;
            break;
        case '*':
            resp.result = req.x * req.y;
            break;
        case '/':
        {
            if (req.y == 0)
                resp.code = Div_Zero;
            else
                resp.result = req.x / req.y;
        }
        break;
        case '%':
        {
            if (req.y == 0)
                resp.code = Mod_Zero;
            else
                resp.result = req.x % req.y;
        }
        break;
        default:
            resp.code = Other_Oper;
            break;
        }

        return resp;
    }
    std::string Calculator(std::string &package)
    {
        //首先把内容转成报文格式
        std::string content;
        bool can=Decode(package,&content);//把传进来的字符解码 len/n x op y
        if(!can) 
            return ;
        Request req;
        can=req.Deserialize(content);//反序列化 x op y
        if(!can)
        return ;

        content="";
        Response resp=CalculatorOperator(req);//进行计算req.x,req.y,req.op
        resp.Serialize(&content);//变为 result code
        content=Encode(content);//变为 len/n result code
        return content;
      
    }
    private:


};

协议的定制就是约定,无论客户端还是服务端都要以这种方式来接受发报文,不符合该要求的是不会接收的。

可以看到序列与反序列化基本上都是字符串操作,每次这样写有点麻烦,实际上,再开发过程中,有现成的序列与反序列化的方法供我们使用,一般就是json,protobuf.

//json
bool Serialize(std::string *out)
    {

        Json::Value root;
        root["x"] = x;
        root["y"] = y;
        root["op"] = op;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
    }
bool Deserialize(const std::string &in) // "x op y"
    {

       Json::Value root;
        Json::Reader r;
        r.parse(in, root);
        x = root["x"].asInt();
        y = root["y"].asInt();
        op = root["op"].asInt();
        return true;

    }
bool Serialize(std::string *out)
    {

        Json::Value root;
        root["result"] = result;
        root["code"] = code;
        // Json::FastWriter w;
        Json::StyledWriter w;
        *out = w.write(root);
        return true;
    }
    bool Deserialize(const std::string &in) // "result code"
    {

        Json::Value root;
        Json::Reader r;
        r.parse(in, root);

        result = root["result"].asInt();
        code = root["code"].asInt();
        return true;
    }

客户端:

#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<assert.h>
#include"Protocol.hpp"
#include"Socket.hpp"
void USage(char *s)
{
    std::cout<<"Usage:"<<s<<"<ip> <port>"<<std::endl;
}
int main(int argc,char*argv[])
{
    if(argc!=3)
    {
       USage(argv[0]);
       exit(0);
    }
    uint16_t port=stoi(argv[2]);
    std::string ip =argv[1];
    Sock sockfd;
    sockfd.Createsockfd();
    sockfd.Connect(ip, port);
    

    int cnt=0;
    srand(time(NULL)^getpid());
    const std::string opers = "+-*/%=-=&^";
    while(cnt <= 10)
    {
        std::cout << "===============第" << cnt << "次测试....., " << "===============" << std::endl;
        int x = rand() % 100 + 1;
        usleep(1234);
        int y = rand() % 100;
        usleep(4321);
        char oper = opers[rand()%opers.size()];
        Request req(x, y, oper);

        std::string package;
        req.Serialize(&package);

        package = Encode(package);

        write(sockfd.Fd(), package.c_str(), package.size());
        
        cout<<"发送字符串:"<<package<<endl;
        char buffer[128];
        std::string inbuffer_stream;
        ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer)); 
        if(n > 0)
        {
            buffer[n] = 0;
            inbuffer_stream += buffer; // "len"\n"result code"\n
            std::cout << inbuffer_stream << std::endl;
            std::string content;
            bool r = Decode(inbuffer_stream, &content); // "result code"
            assert(r);

            Response resp;
            r = resp.Deserialize(content);
            assert(r);

            cout<<"接受字符串:"<<content<<endl;
        }

        std::cout << "=================================================" << std::endl;
        sleep(1);

        cnt++;
    }
    sockfd.Close();
    return 0;

   
}

服务端:

#pragma once
#include<iostream>
#include<functional>
#include<strings.h>
#include <signal.h>
#include"Socket.hpp"
using namespace std;
const uint16_t defaultport=8080;
const string defaultip="127.0.0.1";
const int defaultsockfd=-1;
using func_t = std::function<std::string(std::string &package)>;
class Tcpserver
{

  public:
         Tcpserver(uint16_t port ,func_t callback) : _port(port), _callback(callback)
         {}

         void InitServer()
         {
            _listensock.Createsockfd();
            _listensock.Bind(_port);
            _listensock.Listen();
         }

         void Start()
         {
            
            signal(SIGCHLD,SIG_IGN); 
            signal(SIGPIPE,SIG_IGN); 
            while(true)
            {
                string clientip;
                uint16_t clientport;
                int sockfd=_listensock.Accept(&clientip,&clientport);//回调初始化地址与端口号
                if(sockfd<0)
                {
                    continue;
                }   
                //孙子进程提供服务,可并发访问。
                if(fork()==0)
                {
                        //关闭文件描述符不影响文件缓冲区,防止文件描述符不够用
                        _listensock.Close(); 
                        //service
                        std::string buffoutput;
                        while(true)
                        {
                            char buff[1024];
                            ssize_t n=read(sockfd, buff, sizeof(buff));
                            if(n==0)
                            {
                                std::cout<<"读取消息失败"<<"错误码"<<errno<<"错误原因"<<strerror(errno)<<std::endl;
                                break;
                            }else if(n>0)
                            {
                                std::cout<<"读取消息成功:"<<buff<<",进行报文解读.........."<<std::endl;
                                buff[n]=0;
                                buffoutput+=std::string(buff);
                                std::cout<<buffoutput<<endl;
                                //可能有一堆请求
                                while(true)
                                {
                                    std::string info=_callback(buffoutput);//回调给函数DoCalculator
                                    //为空就继续读
                                    if(info.empty()) 
                                    {
                                        break;
                                    }
                                    std::cout<< "debug, response:"<< info.c_str()<<std::endl;
                                    std::cout<<"debug:"<<buffoutput.c_str()<<std::endl;
                                    //向客户写回信息,向缓冲区里写
                                    write(sockfd,info.c_str(),info.size());
                                }
                                
                            }else 
                            {
                                break;
                            }
                            
                        }

                        exit(0);
                }
                close(sockfd);


            }

         }

         ~Tcpserver()
         {
 
         }

  private:
         Sock _listensock;
         uint16_t _port;
        func_t _callback;
};

主运行函数:

#include"Calculator.hpp"
#include"ServerCalculator.hpp"
#include <unistd.h>
void Hlper(char *s)
{
    std::cout<<"please enter correct command in '"<<s<<" port[1024+]'"<<std::endl;
}
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        Hlper(argv[0]);
        exit(1);
    }
    uint16_t port=stoi(argv[1]);
    ServerCal Calculator;
    Tcpserver *Cal=new Tcpserver(port,std::bind(&ServerCal::DoCalculator,&Calculator,std::placeholders::_1));//这里用的是包装器bind
    Cal->InitServer();
    Cal->Start();
                                     

    return 0;

}

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

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

相关文章

线程池相关详解

1.线程池的核心参数 线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数&#xff1a; corePoolSize核心线程数目 maximumPoolSize最大线程数目&#xff08;核心线程救急线程的最大数目&#xff09; keepAliveTime生存时间:救急线程的生存时间&#xff0c;生…

Pytorch入门实战 P3-天气识别

目录 一、前期准备 1、查看设备 2、导入本地数据 3、测试下获取到的天气数据 4、图像预处理 5、划分数据集 6、加载数据集 二、搭建简单的CNN网络&#xff08;特征提取分类&#xff09; 三、训练模型 1、设置超参数 2、编写训练函数 3、编写测试函数 4、正式训练 …

4、类加载器

2.4.1 什么是类加载器 类加载器&#xff08;ClassLoader&#xff09;是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术&#xff0c;类加载器只参与加载过程中的字节码获取并加载到内存这一部分。 类加载器会通过二进制流的方式获取到字节码文件的内容&#xff0c…

Visual Studio配置libtorch(cuda安装一步到位)

Visual Studio配置libtorch visual Studio安装cuDNN安装CUDAToolkit安装libtorch下载Visual Studio配置libtorch(cuda版本配置) visual Studio安装 visual Studio点击安装 具体的安装和配置过程这里就不进行细讲了&#xff0c;可以参考我这篇博客Visual Studio配置OpenCV(保姆…

【嵌入式学习】Qtday03.21

一、思维导图 二、练习 自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面。&#xff08;不要使用课堂上的图片和代码&#xff0c;自己发挥&#xff0c;有利于后面项目的完成&#xff09; 要求&#xff1a; 1. 需要使用Ui界面文件进行界面设计 2. ui界面上的组件…

vue.js制作学习计划表案例

通俗易懂&#xff0c;完成“学习计划表”用于对学习计划进行管理&#xff0c;包括对学习计划进行添加、删除、修改等操作。 一. 初始页面效果展示 二.添加学习计划页面效果展示 三.修改学习计划完成状态的页面效果展示 四.删除学习计划 当学习计划处于“已完成”状态时&…

栈——数据结构——day4

栈的定义 栈是限定仅在一段进行插入和删除操作的线性表。 我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表&#xff0c;简称LIFO结构。 栈的插入操作&#xff0c;叫作进栈&#…

开源项目ChatGPT-Next-Web的容器化部署(三)-- k8s deployment.yaml部署

一、说在前面的话 有了docker镜像&#xff0c;要把一个项目部署到K8S里&#xff0c;主要就是编写deployment.yaml。 你需要考虑的是&#xff1a; 环境变量服务的健康检测持久化启动命令程序使用的数据源程序使用的配置文件 因为本前端项目比较简单&#xff0c;这里只做一个…

重学SpringBoot3-Profiles介绍

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-Profiles介绍 Profiles简介如何在Spring Boot中使用Profiles定义Profiles激活ProfilesIDEA设置active profile使用Profile-specific配置文件 条件化Bean…

Python爬虫案例-爬取主题图片(可以选择自己喜欢的主题)

2024年了&#xff0c;你需要网络资源不能还自己再慢慢找吧&#xff1f; 跟着博主一块学习如何利用爬虫获取资源&#xff0c;从茫茫大海中寻找那个她到再妹子群中找妹子&#xff0c;闭着眼睛都可以找到合适的那种。文章有完整示例代码&#xff0c;拿过来就可以用&#xff0c;欢迎…

就业班 第二阶段 2401--3.18 day1 初识mysql

初识&#xff1a; 1、关系型数据库mysql、mariadb、sqlite 二维关系模型 2、非关系型数据库 redis、memcached sql 四个部分 DDL 数据库定义语言 创建数据库&#xff0c;创建用户&#xff0c;创建表 DML 数据库操作语言 增删改 DQL 数据库查询语言 查 DCL 数据库控制语言 授权 …

Pake一键打包,轻松构建桌面级应用!

Pake&#xff1a;顷刻之间&#xff0c;智能封装——WEB到桌面瞬间联通&#xff0c;让网站应用像搭积木般部署 - 精选真开源&#xff0c;释放新价值。 概览 Pake&#xff0c;作为一款新颖且极具创新性的桌面应用开发框架&#xff0c;凭借其独特的技术路径和高效的实现方式&…

时代教育期刊投稿发表

《时代教育》是由成都传媒集团主管主办&#xff0c;中华人民共和国新闻出版总署批准国内公开出版发行的专业教育类期刊&#xff0c;主要刊登各类高等院校、职业技术学校、中小学教师及研究生、教育科研工作者的教育实践研究成果&#xff1b;教育教学行业的最新动态&#xff1b;…

基于SSM+Jsp+Mysql的KTV点歌系统

基于SSMJspMysql的KTV点歌系统 基于SSMJspMysql的KTV点歌系统的设计与实现 开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工…

jvm提供的远程调试 简单使用

JVM自带远程调试功能 JVM远程调试&#xff0c;其实是两个虚拟机之间&#xff0c;通过socket通信&#xff0c;达到远程调试的目的&#xff1b; 前提 确保本地和远程的网络是开通的&#xff1b; 本地操作 远程操作 在启动命令参数中 把上面的内容复制进去

第 6 章 ROS-URDF练习(自学二刷笔记)

重要参考&#xff1a; 课程链接:https://www.bilibili.com/video/BV1Ci4y1L7ZZ 讲义链接:Introduction Autolabor-ROS机器人入门课程《ROS理论与实践》零基础教程 6.3.4 URDF练习 需求描述: 创建一个四轮圆柱状机器人模型&#xff0c;机器人参数如下&#xff0c;底盘为圆柱…

NIVision-相机图像采集

应用场景 上位机与工业相机通讯&#xff0c;控制相机抓取图像。 工业相机的通讯接口大多为USB口或网口。 USB口则直接将通讯线缆插入上位机USB端口&#xff0c;打开MAX中设备与接口一栏可以看到电脑给相机分配的资源名称&#xff1b;网口则需要将网线连接相机和上位机&#xf…

【数据库】SQL Server 2008 R2 安装过程

启动安装程序&#xff0c;点击setup&#xff0c;进入【SQLServer安装中心】 点击界面左侧的【安装】&#xff0c;然后点击右侧的【全新SQLServer独立安装或向现有安装添加功能】&#xff0c;进入【SQLServer2008R2安装程序】界面&#xff0c;如下图所示&#xff1a; 进入【安装…

浅谈Postman与Jmeter的区别、用法

前阶段做了一个小调查&#xff0c;发现软件测试行业做功能测试和接口测试的人相对比较多。在测试工作中&#xff0c;有高手&#xff0c;自然也会有小白&#xff0c;但有一点我们无法否认&#xff0c;就是每一个高手都是从小白开始的&#xff0c;所以今天我们就来谈谈一大部分人…

师徒互电,眼冒金星,采集系统变电刺激系统!

原文来自微信公众号&#xff1a;工程师看海&#xff0c;很高兴分享我的原创文章&#xff0c;喜欢和支持我的工程师&#xff0c;一定记得给我点赞、收藏、分享哟。 加微信[chunhou0820]与作者进群沟通交流 电的我眼冒金星&#xff0c;以为自己被三体召唤&#xff0c;整个世界为我…