[计算机网络]---序列化和反序列化

前言

作者:小蜗牛向前冲

名言:我可以接受失败,但我不能接受放弃

  如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 

目录

 一、再谈协议

二、序列化和反序化

1、网络版本计算器的场景搭建

2、服务器构建 

 3、客户端构建

4、序列化和反序列化 

4.1自定义序列化和反序列化

4.2json实现序列化和反序列化

 5、测试


本期学习:重点理解序化和反序列化

 一、再谈协议

        在前面博客谈网络的时候,我们认为协议就是一种约定,在用socket api接口的时候,都是按照“字符串”的方式来进行发送和接受的,但是如果我们要传输一些结构化的数据,又该怎么办?

这就要我们将数据序列化进行传输或者接收。

在网络通信中,传输结构化数据的常见方法有以下几种:

  1. 序列化: 将结构化数据转换为字符串或字节流进行传输。在发送端,可以使用一种序列化格式将数据转换为字符串或字节流;在接收端,再将接收到的字符串或字节流反序列化为相应的数据结构。常见的序列化格式包括 JSON(JavaScript Object Notation)、XML(eXtensible Markup Language)、Protocol Buffers(protobuf)、MessagePack 等。选择序列化格式时,需要考虑数据的大小、可读性、解析效率等因素。

  2. 数据格式协议: 使用一种规定的数据格式协议进行数据传输。这种方法通常需要双方预先约定好数据格式协议,并按照协议的规定进行数据的编码和解码。常见的数据格式协议包括 HTTP、FTP、SMTP 等。在 HTTP 协议中,可以使用 Content-Type 来指定数据的格式,如 application/json 表示 JSON 格式数据,application/xml 表示 XML 格式数据。

  3. 自定义协议: 自定义通信协议,定义数据的传输格式和规则。在自定义协议中,可以根据实际需求灵活地定义数据的结构和编码方式,以及通信过程中的规则和约定。自定义协议通常需要双方进行协商和实现,但可以更好地满足特定场景下的需求。

二、序列化和反序化

为了更好的理解,下面我们将通过自己定制tcp协议,将数据进行序列化和反序列化。为了方便叙述,我们简单的制定网络版本的计算 ,来理解协议中序列化和反序列化

1、网络版本计算器的场景搭建

我们的构想是,客户端通过将计算请求数据序列化,发送到网络中,服务端接收后将数据进行反序列化进行计算处理

处理过程大致如上图:

  • c->s调用函数发送数据的本质就是将一个缓冲区的拷贝到另外一个缓冲区。
  • s->c服务器回显数据的本质也是将s中缓冲区的数据拷贝到c中的缓冲区
  • 所以所tcp是全双工的

2、服务器构建 

这里构建的服务器和上篇在套接字中的是没有本质区别的

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "log.hpp"
#include "protocol.hpp"
using namespace std;namespace server
{enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};static const uint16_t gport = 8080;static const int gbacklog = 5;// const Request &req: 输入型// Response &resp: 输出型typedef std::function<bool(const Request &req, Response &resp)> func_t;// 保证解耦void handlerEntery(int sock, func_t func){std::string inbuffer;while (true){// 1. 读取:"content_len"\r\n"x op y"\r\n// 1.1 你怎么保证你读到的消息是 【一个】完整的请求std::string req_text, req_str;// 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\nif (!recPackage(sock, inbuffer, &req_text))return;std::cout << "带报头的请求:\n"<< req_text << std::endl;if (!deLength(req_text, &req_str))return;std::cout << "去掉报头的正文:\n"<< req_str << std::endl;// 2. 对请求Request,反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(req_str))return;// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑// 3.1 得到一个结构化的响应Response resp;func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是!// 4.对响应Response,进行序列化// 4.1 得到了一个"字符串"std::string resp_str;resp.serialize(&resp_str);std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;// 5. 然后我们在发送响应// 5.1 构建成为一个完整的报文std::string send_string = enLength(resp_str);send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说}}class CalServer{public:CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port){}void initServer(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", _listensock);// 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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3. 设置socket 为监听状态if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}void start(func_t func){for (;;){// 4. server 获取新链接// sock, 和client进行通信的fdstruct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){logMessage(ERROR, "accept error, next");continue;}logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?// version 2 多进程版(2)pid_t id = fork();if (id == 0) // child{close(_listensock);// if(fork()>0) exit(0);//  serviceIO(sock);handlerEntery(sock, func);close(sock);exit(0);}close(sock);// fatherpid_t ret = waitpid(id, nullptr, 0);if (ret > 0){logMessage(NORMAL, "wait child success"); // ?}}}~CalServer() {}private:int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!uint16_t _port;};
}

对于 handlerEntery这个操作我们要分析,在这函数中首先我们要获取到由客户端发送过来的报文,而客户端发送过来的报文肯定是经过序列化的,所以我们要进行反序列化,在通过func回调函数得到一个结构化的响应,在将响应通过处理发送给客户端。

 // 保证解耦void handlerEntery(int sock, func_t func){std::string inbuffer;while (true){// 1. 读取:"content_len"\r\n"x op y"\r\n// 1.1 你怎么保证你读到的消息是 【一个】完整的请求std::string req_text, req_str;// 1.2 我们保证,我们req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\nif (!recPackage(sock, inbuffer, &req_text))return;std::cout << "带报头的请求:\n"<< req_text << std::endl;if (!deLength(req_text, &req_str))return;std::cout << "去掉报头的正文:\n"<< req_str << std::endl;// 2. 对请求Request,反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(req_str))return;// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑// 3.1 得到一个结构化的响应Response resp;func(req, resp); // req的处理结果,全部放入到了resp, 回调是不是不回来了?不是!// 4.对响应Response,进行序列化// 4.1 得到了一个"字符串"std::string resp_str;resp.serialize(&resp_str);std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;// 5. 然后我们在发送响应// 5.1 构建成为一个完整的报文std::string send_string = enLength(resp_str);send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说}}

对于calServer.cc的主程序,就是简单进行cal计算业务 

#include "calServer.hpp"
#include <memory>using namespace server;
using namespace std;static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// req: 里面一定是我们的处理好的一个完整的请求对象
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{// req已经有结构化完成的数据啦,你可以直接使用resp._exitcode = OK;resp._result = OK;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._exitcode = DIV_ZERO;elseresp._result = req._x / req._y;}break;case '%':{if (req._y == 0)resp._exitcode = MOD_ZERO;elseresp._result = req._x % req._y;}break;default:resp._exitcode = OP_ERROR;break;}return true;
}// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);unique_ptr<CalServer> tsvr(new CalServer(port));tsvr->initServer();tsvr->start(cal);return 0;
}

 3、客户端构建

对于客户端calClinet.hpp完成的主要逻辑是进行连网,输入计算,对服务器进行计算请求,在进行计算信息的构建。 

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "protocol.hpp"#define NUM 1024
using namespace std;
class CalClient
{
public:CalClient(const std::string &serverip, const uint16_t &serverport): _sock(-1), _serverip(serverip), _serverport(serverport){}void initClient(){// 1. 创建socket_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){std::cerr << "socket create error" << std::endl;exit(2);}// 2. tcp的客户端要不要bind?要的! 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!// 3. 要不要listen?不要!// 4. 要不要accept? 不要!// 5. 要什么呢??要发起链接!}void start(){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "socket connect error" << std::endl;}else{std::string line;std::string inbuffer;while (true){std::cout << "mycal>>> ";// 输入计算getline(cin, line);// 进行请求Request req = ParseLine(line);string content; // 存放计算信息req.serialize(&content);string send_string = enLength(content); // 添加报头send(_sock, send_string.c_str(), send_string.size(), 0);string package, text;if (!recPackage(_sock, inbuffer, &package))continue;if (!deLength(package, &text))continue;// 响应Response resp;resp.deserialize(text);std::cout << "exitCode: " << resp._exitcode << std::endl;std::cout << "result: " << resp._result << std::endl;}}}// 从文本中提取计算格式信息,然后用这些信息构建请求Request ParseLine(const std::string &line){//"1+1" "123*456" "12/0"int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后int i = 0;int cnt = line.size();string left, right;char op;while (i < cnt){switch (status){case 0:{if (!isdigit(line[i])) // isdigit检查字符是否是十进制{op = line[i];status = 1;}elseleft.push_back(line[i++]);}break;case 1:{i++;status = 2;}break;case 2:{right.push_back(line[i++]);}break;}}std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;return Request(std::stoi(left), std::stoi(right), op);}~CalClient(){if (_sock >= 0)close(_sock);}private:int _sock;std::string _serverip;uint16_t _serverport;
};

calClient.cc 

#include "calClient.hpp"
#include <memory>using namespace std;static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = atoi(argv[2]);unique_ptr<CalClient> tcli(new CalClient(serverip, serverport));tcli->initClient();tcli->start();return 0;
}

4、序列化和反序列化 

4.1自定义序列化和反序列化

前面说了一大堆要对数据进行序列化和反序列化 ,那到底什么是序列化和反序列,其实本质就是将一堆字符串,整和成一个字符串。为了完成我们网络计算器,这里我们写了二个类,一个是 Request对数据进行请求,另外一个是Response,每个类中都要对序化和反序列进行设计

class Request
{
public:Request() : _x(0), _y(0), _op(0){}Request(int x, int y, char op) : _x(x), _y(y), _op(op){}// 1. 自己写// 2. 用现成的bool serialize(std::string *out){}// "x op yyyy";bool deserialize(const std::string &in){}
public:// "x op y"int _x;int _y;char _op;
};
// 响应
class Response
{
public:Response() : _exitcode(0), _result(0){}Response(int exitcode, int result) : _exitcode(exitcode), _result(result){}bool serialize(std::string *out){}bool deserialize(const std::string &in){}public:int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准int _result;   // 计算结果
};

其中序列化和反序列化可以自己进行编写,也可以通过库进行完成。

在大部分场景中我们都是用json进行序列结构的,对于自定义序列化,每个实现方式可能不同,所以不重点分析了,下面会有完整代码,大家可以参考实现

4.2json实现序列化和反序列化

在Linux上使用json我们要进行库的安装

sudo yum install -y jsoncpp-devel

序列化 

使用了 JsonCpp 库来创建一个 JSON 对象 root,然后将一些值 _x_y_op 分别存储到 JSON 对象的键 "first""second""oper" 中。接下来,使用 Json::FastWriter 对象 writer 来将 JSON 对象 root 转换为字符串,并将结果写入到 out 指向的位置。

 Json::Value root;root["first"] = _x;root["second"] = _y;root["oper"] = _op;Json::FastWriter writer;// Json::StyledWriter writer;*out = writer.write(root);

反序列化 

 使用了 JsonCpp 库来解析输入的 JSON 字符串 in,并将解析得到的值存储到 Json::Value 类型的对象 root 中。然后,通过访问 root 对象的键 "first""second""oper" 来获取相应的值,并将这些值转换为整数类型,并分别存储到 _x_y_op 变量中。

 Json::Value root;Json::Reader reader;reader.parse(in, root);_x = root["first"].asInt();_y = root["second"].asInt();_op = root["oper"].asInt();

 注意:asInt() 函数用于将 JSON 值转换为整数类型

protocol.hpp 完整代码实现:添加报文,去报头,请求,响应获取数据包

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "/r/n" // /r回车   /n换行   /r/n表示换行的同时将光标移动到行首
#define LINE_SEP_LEN strlen(LINE_SEP)
using namespace std;// 错误枚举
enum
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};
// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n// 添加报头
string enLength(string &text)
{string send_string = to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}
// 去报头
//  "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)
{auto pos = package.find(LINE_SEP);if (pos == string::npos)return false;string text_len_string = package.substr(0, pos);int text_len = stoi(text_len_string);*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}// 请求
class Request
{
public:Request() : _x(0), _y(0), _op(0){}Request(int x, int y, char op) : _x(x), _y(y), _op(op){}// 1. 自己写// 2. 用现成的bool serialize(std::string *out){
#ifdef MYSELF*out = "";// 结构化 -> "x op y";std::string x_string = std::to_string(_x);std::string y_string = std::to_string(_y);*out = x_string;*out += SEP;*out += _op;*out += SEP;*out += y_string;
#elseJson::Value root;root["first"] = _x;root["second"] = _y;root["oper"] = _op;Json::FastWriter writer;// Json::StyledWriter writer;*out = writer.write(root);
#endifreturn true;}// "x op yyyy";bool deserialize(const std::string &in){
#ifdef MYSELF// "x op y" -> 结构化auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == std::string::npos || right == std::string::npos)return false;if (left == right)return false;if (right - (left + SEP_LEN) != 1)return false;std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - startstd::string y_string = in.substr(right + SEP_LEN);if (x_string.empty())return false;if (y_string.empty())return false;_x = std::stoi(x_string);_y = std::stoi(y_string);_op = in[left + SEP_LEN];
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);_x = root["first"].asInt();_y = root["second"].asInt();_op = root["oper"].asInt();
#endifreturn true;}public:// "x op y"int _x;int _y;char _op;
};// 响应
class Response
{
public:Response() : _exitcode(0), _result(0){}Response(int exitcode, int result) : _exitcode(exitcode), _result(result){}bool serialize(std::string *out){
#ifdef MYSELF*out = "";std::string ec_string = std::to_string(_exitcode);std::string res_string = std::to_string(_result);*out = ec_string;*out += SEP;*out += res_string;
#elseJson::Value root;root["exitcode"] = _exitcode;root["result"] = _result;Json::FastWriter writer;*out = writer.write(root);
#endifreturn true;}bool deserialize(const std::string &in){
#ifdef MYSELF// "exitcode result"auto mid = in.find(SEP);if (mid == std::string::npos)return false;std::string ec_string = in.substr(0, mid);std::string res_string = in.substr(mid + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;_exitcode = std::stoi(ec_string);_result = std::stoi(res_string);
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);_exitcode = root["exitcode"].asInt();_result = root["result"].asInt();
#endifreturn true;}public:int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准int _result;   // 计算结果
};// 获取数据包
//  "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
bool recPackage(int sock, string &inbuffer, string *text)
{char buffer[1024];while (true){// 从网络中接受资源ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;inbuffer += buffer;// 分析auto pos = inbuffer.find(LINE_SEP);if (pos == string::npos)continue;string text_len_string = inbuffer.substr(0, pos);int text_len = stoi(text_len_string);// 报文的总长度int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size();std::cout << "处理前#inbuffer: \n"<< inbuffer << std::endl;if (inbuffer.size() < total_len){std::cout << "你输入的消息,没有严格遵守我们的协议,正在等待后续的内容, continue" << std::endl;continue;}// 最少一个完整报文*text = inbuffer.substr(0, total_len);inbuffer.erase(0, total_len);std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;break;}elsereturn false;}return true;
}

 5、测试

下面我们进行几组简单的测试:

 左边是服务器,又边上是客户端

Linux小命令

killall 是一个在 Unix 和类 Unix 操作系统上用于终止进程的命令。与 kill 命令不同,killall 不是根据进程 ID(PID)来终止进程,而是根据进程的名称来匹配并终止相应的进程。

killall [选项] 进程名

其中,选项可以用来指定不同的操作行为,而进程名则是要终止的进程的名称。

一些常用的选项包括:

  • -e:显示详细的错误信息。
  • -i:交互模式,在终止进程之前询问用户。
  • -q:安静模式,不显示任何输出。
  • -u:指定用户,仅终止指定用户的进程。

注意:killall 命令会终止所有匹配名称的进程,因此需要谨慎使用,以免意外终止系统中重要的进程

 

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

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

相关文章

MySQL数据库基础(四):图形化开发工具DataGrip

文章目录 图形化开发工具DataGrip 一、DataGrip介绍 二、DataGrip安装 三、创建工程 四、连接数据库 五、选择要使用的数据库 六、DataGrip软件设置 1、设置字体大小 2、设置关键字大写 3、自动排版 图形化开发工具DataGrip 一、DataGrip介绍 DataGrip是JetBrains公…

Packet Tracer - Configuring ASA Basic Settings and Firewall Using CLI

Packet Tracer - 使用CLI配置ASA基本设置和防火墙 IP地址表 目标 验证连接并探索ASA设备使用CLI配置ASA的基本设置和接口安全级别使用CLI配置路由、地址转换和检查策略配置DHCP、AAA和SSH服务配置DMZ区域、静态NAT和访问控制列表&#xff08;ACL&#xff09; 场景 您的公司…

NBlog个人博客部署过程记录 -- 后端springboot + 前端vue

项目是fork的Naccl大佬NBlog项目&#xff0c;页面做的相当漂亮&#xff0c;所以选择了这个。可以参考2.3的效果图 惭愧&#xff0c;工作两年了也每个自己的博客系统&#xff0c;趁着过年时间&#xff0c;开始搭建一下. NBlog原项目的github链接&#xff1a;Naccl/NBlog: &#…

java8新特性——StreamAPI

说明&#xff1a; java8中有两大最为重要的改变。第一个是Lambda表达式&#xff1b;另外一个则是Stream API。 Stream API&#xff08;java.util.stream&#xff09;把真正的函数式编程风格引入java。这是目前为止对java类库最好的补充&#xff0c;因为Stream API可以极大提供j…

【C++】类和对象(五)友元、内部类、匿名对象

前言&#xff1a;前面我们说到类和对象是一个十分漫长的荆棘地&#xff0c;今天我们将走到终点&#xff0c;也就是说我们对于&#xff23;算是正式的入门了。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&#xff23;学习 &…

MATLAB计算极限和微积分

一.函数与极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0c;x分别趋于0和1&#xff0c;代码如下&#xff1a; syms x; limit(3*x*x/(2*x1),x,0) limit(3*x*x/(2*x1),x,1) 结果分别为0和1&#xff1a; 1.计算双侧极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0…

CPU和GPU有什么区别,玩游戏哪个更重要?

大家好&#xff01;今天我们要聊的话题是CPU和GPU&#xff0c;它们在电脑中扮演着重要的角色&#xff0c;虽然看起来只是两个简单的缩写&#xff0c;但它们的功能和影响是截然不同的&#xff01; 那么&#xff0c;究竟CPU和GPU有什么区别呢&#xff1f;在玩游戏时&#xff0c;…

JavaScript中的变量监听:实时捕捉变化的利器

JavaScript中的变量监听&#xff1a;实时捕捉变化的利器 在JavaScript开发中&#xff0c;经常需要监听变量的改变&#xff0c;并在变量值发生变化时执行相应的操作。这种实时捕捉变化的功能对于构建交互性强的应用程序至关重要。本文将介绍如何在JavaScript中监听变量的改变&am…

车载诊断协议DoIP系列 —— 车辆以太网节点需求汇总

车载诊断协议DoIP系列 —— 车辆以太网节点需求汇总 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,…

Web项目利用MybatisPlus进行分页查询

之前在写博客系统前台页面的时候&#xff0c;遇到了利用mp进行分页查询的情况&#xff0c;由于涉及到的知识点相对较为重要&#xff0c;固写一篇博客以此巩固。 一、功能需求 在首页和分类页面都需要查询文章列表。 首页&#xff1a;查询所有的文章分类页面&#xff1a;查询…

幻兽帕鲁云服务器搭建零基础教程,新手小白一看就会

以下教程基于阿里云服务器ECS 来搭建幻兽帕鲁游戏服务器&#xff0c;通过一键部署的方式&#xff0c;最快1分钟即可完成部署。 阿里云一键部署幻兽帕鲁的活动地址&#xff1a;1分钟畅玩&#xff01;一键部署幻兽帕鲁联机服务器 首先&#xff0c;打开阿里云的这个游戏服务器活…

Github项目推荐-Tiny-Rdm

项目地址 GitHub - tiny-craft/tiny-rdm: A Modern Redis GUI Client 项目简述 一个开源的Redis管理工具&#xff0c;有漂亮的界面和丰富的功能。使用的编程语言如下 项目截图