网络编程套接字(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\n
    string 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\n
        auto pos=inbuffer.find(HEADER_SEP);
        if(pos==string::npos)
            return 0;
        
        string lenStr=inbuffer.substr(0,pos);    // 获取头部字符串, 没有动inbuffer
        int len=Util::toInt(lenStr);             // 得到有效载荷的长度 => "123" -> 123
        int targetPackageLen=len+2*HEADER_SEP_LEN+lenStr.size();   // 得到整个报文长度
        if(inbuffer.size()<targetPackageLen)     // 不是一个完整的报文
            return 0;
        
        *package=inbuffer.substr(0,targetPackageLen);  // 提取到了报文有效载荷, 没有动inbuffer
        inbuffer.erase(0,targetPackageLen);      // 从inbuffer中直接移除整个报文

        cout<<"ReadPackage inbuffer 之后:\n"
            <<inbuffer<<endl;

        return len;
    }

    // Request && Response都要提供序列化和反序列化功能
    // 1. 自己手写
    // 2. 用别人的 --- json, xml, protobuf

    class Request
    {
    public:

        Request()
        {

        }

        Request(int x,int y,char op)
            :_x(x)
            ,_y(y)
            ,_op(op)
        {
        }

        // 序列化: struct->string
        bool 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->struct
        bool Deserialize(const string&inStr)   
        {
            // inStr:  10 + 20 => [0]=>10, [1]=>+, [2]=>20
            vector<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->string
        bool 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->struct
        bool Deserialize(const string&inStr)   
        {
            // 10 0
            vector<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 + 20
        size_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\n
    string 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\n
        auto pos=inbuffer.find(HEADER_SEP);
        if(pos==string::npos)
            return 0;
        
        string lenStr=inbuffer.substr(0,pos);    // 获取头部字符串, 没有动inbuffer
        int len=Util::toInt(lenStr);             // 得到有效载荷的长度 => "123" -> 123
        int targetPackageLen=len+2*HEADER_SEP_LEN+lenStr.size();   // 得到整个报文长度
        if(inbuffer.size()<targetPackageLen)     // 不是一个完整的报文
            return 0;
        
        *package=inbuffer.substr(0,targetPackageLen);  // 提取到了报文有效载荷, 没有动inbuffer
        inbuffer.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->string
        bool Serialize(string* outStr)     
        {
            *outStr=""; 
#ifdef  MYSELF
            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;
#else
            Json::Value root;   // Value: 一种万能对象, 接受任意的kv类型
            root["x"]=_x;
            root["y"]=_y;
            root["op"]=_op;

            // Json::FastWriter writer;  // writer: 是用来进行序列化的 struct -> string
            Json::StyledWriter writer;

            *outStr=writer.write(root);
#endif
            return true;
        }

        // 反序列化: string->struct
        bool Deserialize(const string&inStr)   
        {
#ifdef  MYSELF
            // inStr:  10 + 20 => [0]=>10, [1]=>+, [2]=>20
            vector<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];

#else
            Json::Value root;   
            Json::Reader reader;  // Reader: 是用来反序列化的
            reader.parse(inStr,root);

            _x=root["x"].asUInt();
            _y=root["y"].asUInt();
            _op=root["op"].asUInt();
    
#endif
            Print();

            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->string
        bool Serialize(string* outStr)     
        {
            // _result _code
            *outStr=""; 
#ifdef  MYSELF
            string res_string = to_string(_result);
            string code_string = to_string(_code);

            // 手动序列化
            *outStr=res_string + SEP + code_string;

#else
            Json::Value root;   
            root["result"]=_result;
            root["code"]=_code;
            // Json::FastWriter writer;
            Json::StyledWriter writer;

            *outStr=writer.write(root);
#endif
            return true;
        }

        // 反序列化: string->struct
        bool Deserialize(const string&inStr)   
        {
#ifdef  MYSELF
            // 10 0
            vector<string> result;
            Util::StringSplit(inStr,SEP,&result);

            if(result.size()!=2)
                return false;

            _result=Util::toInt(result[0]);
            _code=Util::toInt(result[1]);

#else
            Json::Value root;
            Json::Reader reader; 
            reader.parse(inStr, root);

            _result = root["result"].asUInt();
            _code = root["code"].asUInt();
#endif
            Print();
            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.mfbz.cn/a/124759.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Pytest插件

官方文档&#xff1a;API Reference — pytest documentation BaseReport 定义Case结果输出 >>> from _pytest.reports import TestReport >>> test TestReport(1,1,1,pass,,running) >>> print(dir(test)) [__annotations__, __class__, __delatt…

02-MySQL-基础-增删改查

一、DDL 定义数据库对象&#xff08;数据库&#xff0c;表&#xff0c;字段&#xff09; ①&#xff1a;数据库操作 01. 查询 1. 查询所有数据库 show databases;2. 查询当前使用的数据库 SELECT database();02. 使用 USE sys;03. 创建 CREATE DATABASE [IF NOT EXISTS]数据…

自动化测试(Java+eclipse)教程

webdriver环境配置 1.下载chromedriver到本地&#xff08;一定要选择和自己浏览器相对应的版本chromedriver下载地址&#xff09; 2.加入到环境变量path中 webdriver工作原理 创建web自动化测试脚本 1.Maven项目创建 File->New->project->(搜索maven)选择maven pr…

JavaScript客户端操作

BOM(browser object model)是浏览器对象模型的简称&#xff0c;被广泛应用于Web开发中&#xff0c;主要用于对客户端浏览器的管理。BOM的概念比较古老&#xff0c;但是一直没有被标准化&#xff0c;不过各主流浏览器均支持BOM&#xff0c;都遵守最基本的规则和用法&#xff0c;…

Dcoker学习笔记(一)

Dcoker学习笔记一 一、 初识Docker1.1 简介1.2 虚拟机和docker的区别1.3 Docker架构1.4 安装Docker&#xff08;Linux&#xff09; 二、 Dcoker基本操作2.1 镜像操作2.2 容器操作练习 2.3 数据卷volume&#xff08;容器数据管理&#xff09;简介数据卷语法数据卷挂载 2.4 自定义…

【Node.js入门】1.1Node.js 简介

Node.js入门之—1.1Node.js 简介 文章目录 Node.js入门之—1.1Node.js 简介什么是 Node.js错误说法 Node.js 的特点跨平台三方类库自带http服务器非阻塞I/O事件驱动单线程 Node.js 的应用场合适合用Node.js的场合不适合用Node.js的场合弥补Node.js不足的解决方案 什么是 Node.j…

前端缓存机制——强缓存、弱缓存、启发式缓存

强缓存和弱缓存的主要区别是主要区别在于缓存头携带的信息不同。 强缓存&#xff1a; 浏览器发起请求&#xff0c;查询浏览器的本地缓存&#xff0c;如果找到资源&#xff0c;则直接在浏览器中使用该资源。若是未找到&#xff0c;或者资源已过期&#xff0c;则浏览器缓存返回未…

Java进阶(JVM调优)——JVM调优参数 JDK自带工具使用 内存溢出和死锁问题案例 GC垃圾回收

前言 JVM作为Java进阶的知识&#xff0c;是需要Java程序员不断深度和理解的。 本篇博客介绍JVM调优的相关知识&#xff0c;给出了一个demo案例&#xff0c;介绍了JVM调优的主要参数&#xff1b;介绍了jdk自带的jvm分析工具的使用&#xff1b;给出了一个内存溢出的调优场景&am…

Unity中Shader再议ATTENUATION

文章目录 前言一、实现实时阴影的投射1、直接复制之前实现投射阴影的Pass 二、实现实时阴影的接受&#xff0c;同时实现光照衰减1、在 v2f 中使用这个2、在 顶点着色器 中使用这个3、在 片元着色器 中使用这个 前言 在之前文章中&#xff0c;实现了 Global Illumination 的直接…

leetcode经典面试150题---5.多数元素

目录 题目描述 前置知识 代码 方法一 排序法 思路 实现 复杂度 方法二 哈希表 思路 实现 题目描述 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给…

mybatis嵌套查询子集合只有一条数据

我们再用mybatis做嵌套查询时&#xff0c;有时会遇到子集合只有1条数据的情况&#xff0c;例如下这样&#xff1a; 数据库查询结果 xml <resultMap id"userMap" type"com.springboot.demo.test.entity.User"><id column"uid" property…

【容器化】Docker

文章目录 概述环境配置的难题虚拟机Linux 容器Docker 核心概念安装命令启动与停止命令镜像相关命令容器相关命令 部署MySQL 部署Tomcat 部署Nginx 部署Redis 部署 迁移与备份Dockerfile 制作镜像Docker 私有仓库将镜像上传到私有仓库从私有仓库拉取镜像 来源 概述 环境配置的难…

TIA博途中已经被调用的变量,为什么交叉引用时却没有显示调用信息?

TIA博途中已经被调用的变量&#xff0c;为什么交叉引用时却没有显示调用信息&#xff1f; 故障现象&#xff1a; 如下图所示&#xff0c;在HMI的画面中&#xff0c;已经连接了对应的变量&#xff0c; 如下图所示&#xff0c;这里为HMI变量表&#xff0c; 如下图所示&#xff…

野火霸天虎 STM32F407 学习笔记_5 按键输入;位带操作介绍

输入——按键点灯 开发板按键电路如下&#xff1a; 按键未按下接地&#xff0c;按下后为高电平。电容起到消抖作用&#xff0c;软件处理就不需要手动延时消抖了。 编程没啥难度&#xff0c;就是改了一下输入模式。使用 ReadInputDataBits 读取。 //bsp_button.c #include &q…

SpringCloudAlibaba系列之Nacos配置管理

目录 说明 认识配置中心 Nacos架构图 Nacos配置管理实现原理 核心源码分析-客户端 核心源码分析-服务端 配置修改的实时通知 主流配置中心对比 小小收获 说明 本篇文章主要目的是从头到尾比较粗粒度的分析Nacos配置中心的一些实现&#xff0c;很多细节没有涉及&#…

uniapp H5预览PDF支持手势缩放、分页、添加水印、懒加载、PDF下载

效果预览 项目说明 uniapp vue2 node&#xff1a;v14.18.3 npm&#xff1a; 6.14.15 安装pdfh5.js插件 pdfh5 - npm (npmjs.com)pdfh5.js 基于pdf.js和jQuery pdfh5 - npm (npmjs.com) npm install pdfh5 由于我安装最新的pdfh5.js后运行时报错 所以我选择降低版本,可能是node…

【Node.js入门】1.2 部署Node.js开发环境

1.2 部署Node.js开发环境 在 Windows 系统上安装 Node.js 两种文件格式的安装包 Windows安装包&#xff08;.msi&#xff09;Windows二进制文件&#xff08;.exe&#xff09;安装包 检查Node.js版本 node --version 在 Linux 系统上安装 Node.js Linux操作系统上安装Nod…

基础:JavaScript的怪癖之一:提升(Hoisting)

JavaScript&#xff0c;通常被称为“Web 语言”&#xff0c;是一种多功能且广泛使用的编程语言。它以其怪癖而闻名&#xff0c;其中之一就是 hoisting&#xff08;提升&#xff09;。无论你是经验丰富的开发人员还是刚刚开始你的编码之旅&#xff0c;理解提升对于编写干净和高效…

汽车标定技术(六)--基于模型开发如何生成完整的A2L文件(2)

目录 1. 自定义ASAP2文件 2. asap2userlib.tlc需要修改的部分 3. 标定量观测量地址替换 3.1 由elf文件替换 3.2 由map文件替换 3.3 正则表达式&#xff08;含asap2post.m修改方法&#xff09; 4.小结 书接上文汽车标定技术(五)--基于模型开发如何生成完整的A2L文件(1)-C…

时间序列预测中的数据分析->周期性、相关性、滞后性、趋势性、离群值等特性的分析方法

本文介绍 本篇文章给大家介绍的是&#xff0c;当我们在进行有关时间序列相关的工作或者实验时&#xff0c;需要对数据进行的一些数据分析操作(包括周期性、相关性、滞后性、趋势性、离群值等等分析)的方法。在本篇文章中会以实战的形式进行讲解&#xff0c;同时提供运行代码和…