网络编程套接字(3)——协议定制 | 序列化与反序列化

文章目录

  • 一.认识“协议”
    • 1.协议的概念
    • 2.结构化数据的传输
    • 3.序列化和反序列化
  • 二. 网络版计算器
    • 1.服务端
    • 2.协议定制
      • (1) 网络发送和读取的正确理解
      • (2) 协议定制的问题
    • 3.客户端
    • 4.代码
  • 三.Json实现序列化反序列化
    • 1.简单介绍
    • 2.使用

一.认识“协议”

1.协议的概念

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,比如怎么建立连接、怎么互相识别等。

为了使数据在网络上能够从源到达目的,网络通信的参与方必须遵循相同的规则,我们将这套规则称为协议(protocol),而协议最终都需要通过计算机语言的方式表示出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。

2.结构化数据的传输

通信双方在进行网络通信时:

  • 如果需要传输的数据是一个字符串,那么直接将这一个字符串发送到网络当中,此时对端也能从网络当中获取到这个字符串。
  • 但如果需要传输的是一些结构化的数据,此时就不能将这些数据一个个发送到网络当中。

比如现在要实现一个网络版的计算器,那么客户端每次给服务端发送的请求数据当中,就需要包括左操作数、右操作数以及对应需要进行的操作,此时客户端要发送的就不是一个简单的字符串,而是一组结构化的数据。

如果客户端将这些结构化的数据单独一个个的发送到网络当中,那么服务端从网络当中获取这些数据时也只能一个个获取,此时服务端还需要纠结如何将接收到的数据进行组合。因此客户端最好把这些结构化的数据打包后统一发送到网络当中,此时服务端每次从网络当中获取到的就是一个完整的请求数据,客户端常见的“打包”方式有以下两种。

将结构化的数据组合成一个字符串

约定方案一:

  • 客户端发送一个形如“1+1”的字符串。
  • 这个字符串中有两个操作数,都是整型。
  • 两个数字之间会有一个字符是运算符。
  • 数字和运算符之间没有空格。

客户端可以按某种方式将这些结构化的数据组合成一个字符串,然后将这个字符串发送到网络当中,此时服务端每次从网络当中获取到的就是这样一个字符串,然后服务端再以相同的方式对这个字符串进行解析,此时服务端就能够从这个字符串当中提取出这些结构化的数据。

定制结构体+序列化和反序列化

约定方案二:

  • 定制结构体来表示需要交互的信息。
  • 发送数据时将这个结构体按照一个规则转换成网络标准数据格式,接收数据时再按照相同的规则把接收到的数据转化为结构体。
  • 这个过程叫做“序列化”和“反序列化”。

客户端可以定制一个结构体,将需要交互的信息定义到这个结构体当中。客户端发送数据时先对数据进行序列化,服务端接收到数据后再对其进行反序列化,此时服务端就能得到客户端发送过来的结构体,进而从该结构体当中提取出对应的信息。

3.序列化和反序列化

序列化和反序列化:

  • 序列化是将对象的状态信息转换为可以存储或传输的形式(字节序列)的过程。
  • 反序列化是把字节序列恢复为对象的过程。

OSI七层模型中表示层的作用就是,实现设备固有数据格式和网络标准数据格式的转换。其中设备固有的数据格式指的是数据在应用层上的格式,而网络标准数据格式则指的是序列化之后可以进行网络传输的数据格式。

序列化和反序列化的目的

  • 在网络传输时,序列化目的是为了方便网络数据的发送和接收,无论是何种类型的数据,经过序列化后都变成了二进制序列,此时底层在进行网络数据传输时看到的统一都是二进制序列。
  • 序列化后的二进制序列只有在网络传输时能够被底层识别,上层应用是无法识别序列化后的二进制序列的,因此需要将从网络中获取到的数据进行反序列化,将二进制序列的数据转换成应用层能够识别的数据格式。

我们可以认为网络通信和业务处理处于不同的层级,在进行网络通信时底层看到的都是二进制序列的数据,而在进行业务处理时看得到则是可被上层识别的数据。如果数据需要在业务处理和网络通信之间进行转换,则需要对数据进行对应的序列化或反序列化操作。

在这里插入图片描述

二. 网络版计算器

我们需要实现一个服务器版的计算器,客户端把要计算的两个数和计算类型发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。

1.服务端

服务端创建步骤:

  • 调用socket,创建套接字
  • 调用bind,绑定端口
  • 调用listen,将套接字状态设置为监听
  • 调用accept,获取新连接
  • 处理读取与写入的问题(重点)

2.协议定制

(1) 网络发送和读取的正确理解

在这里插入图片描述

客户端和服务器通信时,会调用read和write函数,它们是把数据直接发送到对端吗?不是

  • TCP协议有自己的发送缓冲区和接收缓冲区
  • 调用write本质:把用户所对应的数据拷贝到TCP的发送缓冲区
  • 调用read本质:把数据从接收缓冲区拷贝到用户层
  • 所以read和write的本质是拷贝函数
  • 把数据拷贝到TCP发送缓冲区后,剩下的数据怎么发,是由TCP决定的,所以TCP又叫做传输控制协议
  • 因为发送和接收是成对的,可以同时进行的,所以TCP协议是全双工的

综上:

  • TCP通信的本质是把自己发送缓冲区的数据经过网络拷贝到对方的接收缓冲区中
  • 网络通信的本质也是拷贝

(2) 协议定制的问题

在定制协议之前先解决一个问题,之前在使用TCP协议时我们只是简单的读取,没有考虑TCP是面向字节流的,读取数据不完整的问题。这里同样存在相同的问题,如果一下子对方发送了很多报文,这些报文都堆积在TCP的接收缓冲区中,你怎么保证自己读到的是一个完整的报文呢?

我们采用这样的方式:

  • 对报文定长
  • 使用特殊符号(在报文与报文之间增加特殊符号)
  • 自描述方式(自己设计协议)

协议设计格式:

在这里插入图片描述

Protocol.hpp

#include<string>
#include<iostream>
#include<vector>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include"Util.hpp"
using namespace std;// 给网络版本计算器定制协议
namespace Protocol_ns
{#define SEP " "#define SEP_LEN strlen(SEP)   // 绝对不能写成sizeof#define HEADER_SEP "\r\n"#define HEADER_SEP_LEN strlen("\r\n")// "长度"\r\n" "_x op _y"\r\n// "10 + 20" => "7"r\n""10 + 20"\r\n => 报头 + 有效载荷// 请求/响应 = 报头\r\n有效载荷\r\n// 请求 = 报头\r\n有效载荷\r\n报头\r\n有效载荷\r\n报头\r\n有效载荷\r\n// "10 + 20" => "7"r\n""10 + 20"\r\nstring AddHeader(string&str){cout<<"AddHeader 之前:\n"<<str<<endl;string s=to_string(str.size());s+=HEADER_SEP;s+=str;s+=HEADER_SEP;cout<<"AddHeader 之后:\n"<<s<<endl;return s;}// "7"r\n""10 + 20"\r\n => "10 + 20" string RemoveHeader(const string&str,int len){cout<<"RemoveHeader 之前:\n"<<str<<endl;// 从后面开始截取string res=str.substr(str.size()-HEADER_SEP_LEN-len,len); cout<<"RemoveHeader 之后:\n"<<res<<endl;   return res;}int Readpackage(int sock,string&inbuffer,string*package){cout<<"ReadPackage inbuffer 之前:\n"<<inbuffer<<endl;// 边读取char buffer[1024];ssize_t s=recv(sock,&buffer,sizeof(buffer)-1,0);if(s<=0)return -1;buffer[s]=0;inbuffer+=buffer;cout<<"ReadPackage inbuffer 之中:\n"<<inbuffer<<endl;// 边分析,  "7"r\n""10 + 20"\r\nauto pos=inbuffer.find(HEADER_SEP);if(pos==string::npos)return 0;string lenStr=inbuffer.substr(0,pos);    // 获取头部字符串, 没有动inbufferint len=Util::toInt(lenStr);             // 得到有效载荷的长度 => "123" -> 123int targetPackageLen=len+2*HEADER_SEP_LEN+lenStr.size();   // 得到整个报文长度if(inbuffer.size()<targetPackageLen)     // 不是一个完整的报文return 0;*package=inbuffer.substr(0,targetPackageLen);  // 提取到了报文有效载荷, 没有动inbufferinbuffer.erase(0,targetPackageLen);      // 从inbuffer中直接移除整个报文cout<<"ReadPackage inbuffer 之后:\n"<<inbuffer<<endl;return len;}// Request && Response都要提供序列化和反序列化功能// 1. 自己手写// 2. 用别人的 --- json, xml, protobufclass Request{public:Request(){}Request(int x,int y,char op):_x(x),_y(y),_op(op){}// 序列化: struct->stringbool Serialize(string* outStr)     {*outStr=""; string x_string=to_string(_x);string y_string=to_string(_y);// 手动序列化*outStr=x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outStr << std::endl;return true;}// 反序列化: string->structbool Deserialize(const string&inStr)   {// inStr:  10 + 20 => [0]=>10, [1]=>+, [2]=>20vector<string> result;Util::StringSplit(inStr,SEP,&result);if(result.size()!=3)return false;if(result[1].size()!=1)return false;_x=Util::toInt(result[0]);_y=Util::toInt(result[2]);_op=result[1][0];return true;}~Request(){}public:// _x op _y ==> 10 * 9 ? ==> 10 / 0 ?int _x;int _y;char _op;};class Response{public:Response(){}Response(int result,int code):_result(result),_code(code){}// 序列化: struct->stringbool Serialize(string* outStr)     {// _result _code*outStr=""; string res_string = to_string(_result);string code_string = to_string(_code);// 手动序列化*outStr=res_string + SEP + code_string;return true;}// 反序列化: string->structbool Deserialize(const string&inStr)   {// 10 0vector<string> result;Util::StringSplit(inStr,SEP,&result);if(result.size()!=2)return false;_result=Util::toInt(result[0]);_code=Util::toInt(result[1]);return true;}~Response(){}public:int _result;int _code;   // 0 success; 1,2,3,4代表不同错误码};}

Util.hpp

#pragma once#include<iostream>
#include<string>
#include<vector>
using namespace std;class Util
{
public:// 输入: const &// 输出: *// 输入输出: *static bool StringSplit(const string &str, const string &sep, vector<string> *result){// 10 + 20size_t start = 0;while (start < str.size()){auto pos = str.find(sep, start);if (pos == string::npos)break;result->push_back(str.substr(start, pos - start));// 更新位置start = pos + sep.size();}// 处理最后的字符串if(start<str.size())result->push_back(str.substr(start));return true;}static int toInt(const string&s)  // 字符串转整数{return atoi(s.c_str());}
};

3.客户端

客户端创建步骤:

  • 调用socket,创建套接字
  • 客户端不用自己bind端口
  • 调用connect,连接服务器
  • 处理读取与写入的问题

4.代码

源码地址

三.Json实现序列化反序列化

1.简单介绍

上面是自己定制协议实现序列化和反序列化,下面我们使用一些现成的方案来实现序列化和反序列化。C++常用的:protobuf 和 json,这里使用简单的 json。

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。Json数据由键值对组成,大括号表示对象,方括号表示数组。

在这里插入图片描述

2.使用

  • 安装json库
yum install -y jsoncpp-devel
  • 使用json包含的头文件:
#include <jsoncpp/json/json.h>

注意makefile文件要包含Json库的名称

在这里插入图片描述

我们在使用的时候直接创建Json对象来进行序列化和反序列化

#include<string>
#include<iostream>
#include<vector>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include"Util.hpp"
#include<jsoncpp/json/json.h>
using namespace std;// #define MYSELF 1// 给网络版本计算器定制协议namespace Protocol_ns
{#define SEP " "#define SEP_LEN strlen(SEP)   // 绝对不能写成sizeof#define HEADER_SEP "\r\n"#define HEADER_SEP_LEN strlen("\r\n")// "长度"\r\n" "_x op _y"\r\n// "10 + 20" => "7"r\n""10 + 20"\r\n => 报头 + 有效载荷// 请求/响应 = 报头\r\n有效载荷\r\n// 请求 = 报头\r\n有效载荷\r\n报头\r\n有效载荷\r\n报头\r\n有效载荷\r\n// 未来: "长度"\r\n"协议号\r\n""_x op _y"\r\n// "10 + 20" => "7"r\n""10 + 20"\r\nstring AddHeader(string&str){cout<<"AddHeader 之前:\n"<<str<<endl;string s=to_string(str.size());s+=HEADER_SEP;s+=str;s+=HEADER_SEP;cout<<"AddHeader 之后:\n"<<s<<endl;return s;}// "7"r\n""10 + 20"\r\n => "10 + 20" string RemoveHeader(const string&str,int len){cout<<"RemoveHeader 之前:\n"<<str<<endl;// 从后面开始截取string res=str.substr(str.size()-HEADER_SEP_LEN-len,len); cout<<"RemoveHeader 之后:\n"<<res<<endl;   return res;}int Readpackage(int sock,string&inbuffer,string*package){cout<<"ReadPackage inbuffer 之前:\n"<<inbuffer<<endl;// 边读取char buffer[1024];ssize_t s=recv(sock,&buffer,sizeof(buffer)-1,0);if(s<=0)return -1;buffer[s]=0;inbuffer+=buffer;cout<<"ReadPackage inbuffer 之中:\n"<<inbuffer<<endl;// 边分析,  "7"r\n""10 + 20"\r\nauto pos=inbuffer.find(HEADER_SEP);if(pos==string::npos)return 0;string lenStr=inbuffer.substr(0,pos);    // 获取头部字符串, 没有动inbufferint len=Util::toInt(lenStr);             // 得到有效载荷的长度 => "123" -> 123int targetPackageLen=len+2*HEADER_SEP_LEN+lenStr.size();   // 得到整个报文长度if(inbuffer.size()<targetPackageLen)     // 不是一个完整的报文return 0;*package=inbuffer.substr(0,targetPackageLen);  // 提取到了报文有效载荷, 没有动inbufferinbuffer.erase(0,targetPackageLen);      // 从inbuffer中直接移除整个报文cout<<"ReadPackage inbuffer 之后:\n"<<inbuffer<<endl;return len;}// Request && Response都要提供序列化和反序列化功能// 1. 自己手写// 2. 用别人的class Request{public:Request(){}Request(int x,int y,char op):_x(x),_y(y),_op(op){}// 序列化: struct->stringbool Serialize(string* outStr)     {*outStr=""; 
#ifdef  MYSELFstring x_string=to_string(_x);string y_string=to_string(_y);// 手动序列化*outStr=x_string + SEP + _op + SEP + y_string;std::cout << "Request Serialize:\n"<< *outStr << std::endl;
#elseJson::Value root;   // Value: 一种万能对象, 接受任意的kv类型root["x"]=_x;root["y"]=_y;root["op"]=_op;// Json::FastWriter writer;  // writer: 是用来进行序列化的 struct -> stringJson::StyledWriter writer;*outStr=writer.write(root);
#endifreturn true;}// 反序列化: string->structbool Deserialize(const string&inStr)   {
#ifdef  MYSELF// inStr:  10 + 20 => [0]=>10, [1]=>+, [2]=>20vector<string> result;Util::StringSplit(inStr,SEP,&result);if(result.size()!=3)return false;if(result[1].size()!=1)return false;_x=Util::toInt(result[0]);_y=Util::toInt(result[2]);_op=result[1][0];#elseJson::Value root;   Json::Reader reader;  // Reader: 是用来反序列化的reader.parse(inStr,root);_x=root["x"].asUInt();_y=root["y"].asUInt();_op=root["op"].asUInt();#endifPrint();return true;}void Print(){std::cout << "_x: " << _x << std::endl;std::cout << "_y: " << _y << std::endl;std::cout << "_z: " << _op << std::endl;}~Request(){}public:// _x op _y ==> 10 * 9 ? ==> 10 / 0 ?int _x;int _y;char _op;};class Response{public:Response(){}Response(int result,int code):_result(result),_code(code){}// 序列化: struct->stringbool Serialize(string* outStr)     {// _result _code*outStr=""; 
#ifdef  MYSELFstring res_string = to_string(_result);string code_string = to_string(_code);// 手动序列化*outStr=res_string + SEP + code_string;#elseJson::Value root;   root["result"]=_result;root["code"]=_code;// Json::FastWriter writer;Json::StyledWriter writer;*outStr=writer.write(root);
#endifreturn true;}// 反序列化: string->structbool Deserialize(const string&inStr)   {
#ifdef  MYSELF// 10 0vector<string> result;Util::StringSplit(inStr,SEP,&result);if(result.size()!=2)return false;_result=Util::toInt(result[0]);_code=Util::toInt(result[1]);#elseJson::Value root;Json::Reader reader; reader.parse(inStr, root);_result = root["result"].asUInt();_code = root["code"].asUInt();
#endifPrint();return true;}void Print(){std::cout << "_result: " << _result << std::endl;std::cout << "_code: " << _code << std::endl;}~Response(){}public:int _result;int _code;   // 0 success; 1,2,3,4代表不同错误码};
}

本文到此结束,码文不易,还请多多支持哦!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp小程序才到第五层就报错navigateto:fail webview count limit exceed

错误截图 原因 小程序官方描述是说可以跳转10层&#xff0c;但是使用uniapp开发的程序在小程序中才运行到第五层就报错了&#xff0c;原因是因为没有设置appId。如果设置了就正常了。

工业自动化与5G技术的融合:开启工业4.0时代的新篇章

工业自动化与5G技术的融合&#xff1a;开启工业4.0时代的新篇章 随着全球数字化进程的加速推进&#xff0c;工业自动化作为现代制造业的核心驱动力&#xff0c;正经历着前所未有的变革。而在这一变革中&#xff0c;5G技术的崛起为工业自动化带来了全新的可能性和机遇。本文将探…

计算机网络第4章-IPv6和寻址

IP地址的分配 为了获取一块IP地址用于一个组织的子网内&#xff0c;于是我们向ISP联系&#xff0c;ISP则会从已分给我们的更大 地址块中提供一些地址。 例如&#xff0c;ISP也许已经分配了地址块200.23.16.0/20。 该ISP可以依次将该地址块分成8个长度相等的连续地址块&…

Linux开发工具之编辑器vim

文章目录 1.vim是啥?1.1问问度娘1.2自己总结 2.vim的初步了解2.1进入和退出2.2vim的模式1.介绍2.使用 3.vim的配置3.1自己配置3.2下载插件3.3安装大佬配置好的文件 4.程序的翻译 1.vim是啥? 1.1问问度娘 1.2自己总结 vi/vim都是多模式编辑器&#xff0c;vim是vi的升级版本&a…

VR全景技术,为养老院宣传推广带来全新变革

现如今&#xff0c;人口老龄化的现象加剧&#xff0c;养老服务行业也如雨后春笋般不断冒头&#xff0c;但是市面上各式的养老院被包装的五花八门&#xff0c;用户实际参访后却差强人意&#xff0c;如何更好的给父母挑选更为舒心的养老环境呢&#xff1f;可以利用720度VR全景技术…

泄露35TB数据,医疗巨头Henry Schein遭受黑猫勒索组织攻击

近日&#xff0c;据Bleeping Computer 网站消息&#xff0c;BlackCat&#xff08;黑猫&#xff09;勒索软件团伙将医疗保健巨头Henry Schein 添加到了其暗网泄露网站&#xff0c;并声称其破坏了该公司的网络&#xff0c;窃取了35 TB的敏感文件&#xff0c;这些文件包括了Henry …

边缘计算如何改变数据存储?

边缘计算在整个价值链中提供多种优势——从降低成本到提高效率再到安全数据传输。该技术允许在源头收集和分析相关数据&#xff0c;这有助于减少延迟和带宽成本&#xff0c;同时显著提高计算过程的冗余系数和效率。 通过降低数据传输成本和损失&#xff0c;边缘计算帮助企业实现…

ssm+vue的疫情防控管理系统设计与实现(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的疫情防控管理系统设计与实现&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网…

Yolov5 + 界面PyQt5 +.exe文件部署运行

介绍 Yolov5是一种基于深度学习的目标检测算法&#xff0c;PyQt5是一个Python编写的GUI框架&#xff0c;用于创建交互式界面。在部署和运行Yolov5模型时&#xff0c;结合PyQt5可以方便地创建一个用户友好的界面&#xff0c;并将代码打包为.exe文件以供其他人使用。 下面是一个…

docker部署es+kibana

es 暴露的端口特别多 &#xff0c;十分耗内存&#xff0c;数据一般要放置到安全目录&#xff0c;挂载 官网推荐的命令&#xff1a;docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" elasticsearch…

汽车工业生产线数字孪生可视化管理平台,赋予工厂车间数字化智慧化管理

在工业4.0 的时代背景下&#xff0c;随着企业数字化进程的推进&#xff0c;数字孪生可视化技术逐渐在汽车行业得到广泛应用&#xff0c;数字孪生智慧工厂的建设也成为了汽车行业数字化转型的趋势之一。汽车制造业属于典型的离散制造行业&#xff0c;汽车生产包含冲压、焊接、涂…

Bytebase 2.11.0 - 支持 OceanBase Oracle 模式

&#x1f680; 新功能 支持 OceanBase Oracle 模式。支持设置 MySQL 在线变更参数。新增项目数据库查看者的角色。 &#x1f384; 改进 支持在项目中直接选择所有用户并为之添加角色。 调整了项目页面的布局。在 SQL 编辑器中通过悬浮面板展示表和列的详情。 &#x1faa6; …

WebGL智慧城市软件项目

WebGL开发智慧城市项目时&#xff0c;需要考虑多个方面&#xff0c;包括技术、隐私、安全和可持续性。以下是一些需要注意的关键问题&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.隐私和数据安全…

【Android】画面卡顿优化列表流畅度一

卡顿渲染耗时如图&#xff1a; 卡顿表现有如下几个方面&#xff1a; 网络图片渲染耗时大上下滑动反应慢&#xff0c;甚至画面不动新增一页数据加载渲染时耗时比较大&#xff0c;上下滑动几乎没有反应&#xff0c;画面停止没有交互响应 背景 实际上这套数据加载逻辑已经运行…

【PG】PostgreSQL13主从流复制部署(详细可用)

目录 版本 部署主从注意点 1 主库上创建复制用户 2 主库上修改pg_hba.conf文件 3 修改文件后重新加载配置使其生效 4 主库上修改配置文件 5 重启主库pg使参数生效 6 部署从库 7 备份主库数据至从库 停止从库 备份从库的数据库目录 新建数据库数据目录data 创建和…

Docker - 常用命令

Docker - 常用命令 帮助命令 docker version # 查看docker版本信息 docker info # 显示docker的系统信息&#xff0c;包括镜像和容器的数量 docker 命令 --help # 帮助命令官网帮助文档&#xff1a;https://docs.docker.com/engine/reference/commandline/cli/ 镜像…

Unity Mirror学习(一) SyncVars特性使用

官网中所说的网络对象&#xff0c;指的是挂了 NetworkIdentity组件的对象 官网中所说的玩家对象&#xff0c;指的是NetworkManager脚本上的PlayerPrefab预制体 这个概念对阅读官网文档很重要&#xff0c;我刚开始并不理解&#xff0c;走了歪路 SyncVars&#xff08;同步变量&a…

北斗卫星为油气行业发展注入新动力

北斗卫星为油气行业发展注入新动力 北斗卫星是中国自主研发的卫星导航系统&#xff0c;在全球范围内具有广泛应用。随着科技的进步和社会的发展&#xff0c;北斗卫星的智慧应用也逐渐在各行各业中崭露头角。特别是在油气行业&#xff0c;北斗卫星的智慧应用发挥了非常重要的作用…

华为云,阿里云,腾讯云 安全组配置规则

1.安全组常用端口 端口服务说明21FTPFTP服务所开放的端口&#xff0c;用于上传、下载文件。22SSHSSH端口&#xff0c;用于通过命令行模式或远程连接软件&#xff08;例如PuTTY、Xshell、SecureCRT等&#xff09;连接Linux实例。23TelnetTelnet端口&#xff0c;用于Telnet远程登…

ps 让图片附着在文字上

按住alt在文字与图片图片中间&#xff0c;文字在图片下面&#xff09;