协议,序列化,反序列化,Json

文章目录

  • 协议
  • 序列化和反序列化
  • 网络计算器
    • protocol.hpp
    • Server.hpp
    • Server.cc
    • Client.hpp
    • Client.cc
    • log.txt
    • 通过结果再次理解通信过程
  • Json
    • 效果

协议

协议究竟是什么呢?首先得知道主机之间的网络通信交互的是什么数据,像平时使用聊天APP聊天可以清楚,用户看到的不仅仅是聊天的文字,还能够看到用户的头像昵称等其他属性。也就可以证明网络通信不仅仅是交互字符串那么简单。事实上网络通信还可能会通过一个结构化的数据去交互,例如聊天软件里,一台主机向另一台发送消息,这个消息里面就包含了头像等其他的数据。

一台主机发送数据会把所有的数据整合成一个结构化数据统一发送,而收到数据的主机再将这个结构化数据分解成原始的每个独立的数据。而为了确保主机之间收到数据后能够成功的分解,整合和分解两个过程必须是按照统一的约定来执行,而这个约定就是协议

序列化和反序列化

上述的将所有需要发送的数据整合到一起的过程就称为序列化过程而分解的过程就称为反序列化过程

网络的通信就可以理解为:

image-20230806212144823

本篇文章就利用编写一个最简单的网络计算器来感受这个通信的过程

网络计算器

protocol.hpp

这个头文件用来编写协议及序列化反序列化的过程。

  1. 首先因为是一个计算器所以肯定需要两个数和一个计算符号,但是作为服务端不能要求客户怎么样去输入这个计算的格式,可能客户会输入 1+1 也可能会输入 1 + 1 。因此作为服务端要将客户的输入识别成自己的规定。这里就规定数 计算符号 数。所以可以先规定好分隔符,利用宏定义方便修改。
  2. 因为TCP是面向字节流的,所以要明确通信的数据的边界,不能读多也不能读少。因为对于TCP而言,它是全双工的,所以就会出现接收方来不及读,导致整个缓冲区里有大量的数据,因此就要规定好边界保证读到的是一个完整的数据
  3. 对于计算器而言,首先是要获得到需要计算的数据,然后处理得到计算结果。因此可以定义两个结构体,一个结构体负责发送请求也就是发送计算的数据,另一个结构体负责响应请求也就是处理计算结果。
  4. 在发送请求的结构体里保存了两个数和计算符号,但是由于需要网络通信,所以要在结构体里定义好序列化过程的方法。同时服务端拿到数据后要想处理数据就必须要先反序列化,所以结构体里也定义好反序列化过程的方法
  5. 在响应请求的结构体里,同样的服务端需要将计算好的数据发回给客户端也需要定义好序列化过程的方法,而客户端要获取数据也需要反序列化过程的方法
  6. 因为要确保读到的数据是完整的一个数据,因此可以定义一个函数,将序列化好的数据加上一个报头,也就是这个数据的长度,用来标识这个数据的长度,读的时候就可以根据长度的依据去读取。
  7. 当然因为这个报头并不是需要真正要传输的数据,所以需要再定义一个函数用来去掉这个报头
  8. 当两端读取到数据时就需要判断读到的是否是一个完整的数据。如果还没读到完整的数据就继续读。可以定义一个函数用来读取数据并且判断是否读到完整数据,这个依据就是读到的数据的长度是否和原数据的长度相等,这个原数据长度为报头加上正文加上分隔符的长度
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>using namespace std;// 定义好分隔符
#define SEP " "                       // 一条数据里每个元素的分隔符
#define SEP_LEN strlen(SEP)           // 分隔符的大小
#define LINE_SEP "\r\n"               // 数据与数据的分隔符
#define LINE_SEP_LEN strlen(LINE_SEP) // 分隔符大小// 为通信的数据加上数据的长度和分割
// 确保每条数据都能精确的读取到,不读多也不读少
// "text.size()"\r\n"text"\r\n
string enlength(const string &text)
{string res = to_string(text.size());res += LINE_SEP;res += text;res += LINE_SEP;return res;
}// 将全部的一条数据去掉前面的数据长度
// 提取出原始数据
bool delength(const string &package, string *text)
{// 找到第一个元素分隔符,弃掉前面的长度auto pos = package.find(LINE_SEP);if (pos == string::npos)return false;// 确认正文的长度int len = stoi(package.substr(0, pos));// 从第一个分割符往后开始到记录的字符串长度就是原始的数据*text = package.substr(pos + LINE_SEP_LEN, len);return true;
}// 请求
class Request
{
public:int _x;int _y;char _op;Request(): _x(0), _y(0), _op('0'){}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 序列化过程// 因为通信的数据一开始分为了好几个独立的元素// 所以将这些独立的元素合并成一个数据bool serialize(string *out){*out = "";*out += to_string(_x);*out += SEP;*out += _op;*out += SEP;*out += to_string(_y);return true;}// 反序列化过程// 将合并的一整个数据分解回原始的几个独立数据bool unserialize(const string &in){auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == string::npos || right == string::npos || left == right)return false;// 因为对于计算器而言,计算符号只有1位if (right - left - SEP_LEN != 1)return false;_x = stoi(in.substr(0, left));_y = stoi(in.substr(right + SEP_LEN));_op = in[left + SEP_LEN];return true;}
};// 响应请求
class Response
{
public:int _exitcode; // 返回码int _result;   // 返回结果Response(): _exitcode(0), _result(0){}Response(int exitcode, int result): _exitcode(exitcode), _result(result){}bool serialize(string *out){*out = "";*out += to_string(_exitcode);*out += SEP;*out += to_string(_result);return true;}bool unserialize(const string &in){auto pos = in.find(SEP);if (pos == string::npos)return false;_exitcode = stoi(in.substr(0, pos));_result = stoi(in.substr(pos + SEP_LEN));return true;}
};// 读取数据并且判断是否是个完整的数据的方法
bool recvPackage(int sock, string &buff, string *text)
{char buffer[1024];while (1){ssize_t n = recv(sock, buffer, sizeof(buffer), 0);if (n > 0){// 找到报头和正文之间的分隔符buffer[n] = 0;buff += buffer;auto pos = buff.find(LINE_SEP);if (pos == string::npos)continue;// 拿到正文的长度int len = stoi(buff.substr(0, pos));// 判断inbuff的长度是否等于整个数据的长度// 如果相等说明读到了完成的数据int max_len = len + 2 * LINE_SEP_LEN + buff.substr(0, pos).size(); // 整个数据的长度if (buff.size() < max_len)continue;cout << "目前拿到的所有报文:\n" << buff << endl;// 到这一步说明至少有一个完整的数据// 将整个完整的数据传回指针*text = buff.substr(0, max_len);cout << "完整的报文:\n" << *text << endl;buff.erase(0, max_len);return true;}elsereturn false;}return true;
}

Server.hpp

服务端就定义一个函数将整个的读取、反序列化、计算、序列化、发送的过程全部编写好,然后服务端启动加上一个函数参数,也就是计算过程的函数。

#pragma once#include "log.hpp"
#include "Protocol.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <sys/types.h>
#include <sys/wait.h>typedef function<bool(const Request &req, Response &res)> func_t;void HandlerEntery(int sock, func_t func)
{string buff;while (1){// 读取// 需要保证读到的是一个完整的请求string req_text;if (!recvPackage(sock, buff, &req_text))return;cout << "带报头的请求: \n" << req_text << endl;// 将读到的数据的头部去掉,也就是数据长度string req_str;if (!delength(req_text, &req_str))return;cout << "原始数据: " << req_str << endl;// 对读到的原始数据进行反序列化Request req;if (!req.unserialize(req_str))return;// 将反序列化后的结果计算出来后// 将结果放到响应类对象里// 通过响应类对象提取到结果Response res;func(req, res);string res_str;// 得到响应类对象序列化结果res.serialize(&res_str);cout << "计算完成,结果序列化:" << res_str << endl;// 再将得到的结果加上报头// 也就是数据长度确保数据的精确读取// 得到最终的序列化数据res_str = enlength(res_str);cout << "构建完整序列化数据完成:\n" << res_str << endl;// 将最终的数据发送回去send(sock, res_str.c_str(), res_str.size(), 0);cout << "服务端发送完成" << endl;}
}class Server
{
public:Server(const uint16_t &port = 8000): _port(port){}void Init(){// 创建负责监听的套接字 面向字节流_listenSock = socket(AF_INET, SOCK_STREAM, 0);if (_listenSock < 0){LogMessage(FATAL, "create socket error!");exit(1);}LogMessage(NORMAL, "create socket %d success!", _listenSock);// 绑定网络信息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(3);}LogMessage(NORMAL, "bind socket success!");// 设置socket为监听状态if (listen(_listenSock, 5) < 0){LogMessage(FATAL, "listen socket error!");exit(4);}LogMessage(NORMAL, "listen socket success!");}void start(func_t func){while (1){// server获取建立新连接struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);// 创建通信的套接字// accept的返回值才是真正用于通信的套接字_sock = accept(_listenSock, (struct sockaddr *)&peer, &len);if (_sock < 0){// 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已LogMessage(ERROR, "accept socket error, next");continue;}LogMessage(NORMAL, "accept socket %d success", _sock);cout << "sock: " << _sock << endl;// 利用多进程实现pid_t id = fork();if (id == 0) // child{close(_listenSock);// 调用方法包括读取、反序列化、计算、序列化、发送HandlerEntery(_sock, func);close(_sock);exit(0);}close(_sock);// fatherpid_t ret = waitpid(id, nullptr, 0);if (ret > 0){LogMessage(NORMAL, "wait child success"); // ?}}}private:int _listenSock; // 负责监听的套接字int _sock;       // 通信的套接字uint16_t _port;  // 端口号
};

Server.cc

这个服务端的计算函数就通过结构体的对象去作为参数完成,因为需要的数据都在结构体里

#include "Server.hpp"
#include <memory>// 输出命令错误函数
void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}// 计算方式
bool cal(const Request &req, Response &res)
{res._exitcode = 0;res._result = 0;switch (req._op){case '+':res._result = req._x + req._y;break;case '-':res._result = req._x - req._y;break;case '*':res._result = req._x * req._y;break;case '/':{if (req._y == 0)res._exitcode = 1;elseres._result = req._x / req._y;}break;default:res._exitcode = 2;break;}return true;
}int main(int argc, char *argv[])
{// 启动服务端不需要指定IPif (argc != 2){Usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);unique_ptr<Server> server(new Server(port));// 服务端初始化server->Init();//服务端启动server->start(cal);return 0;
}

Client.hpp

客户端和服务端一样也需要接收发送,不过客户端是先发送再接收。并且上述提过因为客户的输入方式无法控制,所以要定义一个函数将客户输入的数据提取到两个数和计算符号才能够构造请求的结构体对象

    #pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"
#include "Protocol.hpp"using namespace std;class Client
{
public:Client(const string &serverip, const uint16_t &port): _serverip(serverip), _port(port), _sock(-1){}void Init(){// 创建套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){LogMessage(FATAL, "create socket error");exit(1);}// TCP的客户端也不需要显示绑定端口,让操作系统随机绑定// TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept// TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept}void start(){// 向服务端发起链接请求struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_serverip.c_str());if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)LogMessage(ERROR, "connect socket error");// 和服务端通信else{string line;string buffer;while (1){cout << "Please cin: " << endl;getline(cin, line);Request req = ParseLine(line);string text;req.serialize(&text);cout << "序列化后的数据:" << text << endl;string send_str = enlength(text);cout << "添加报头后的数据: \n" << send_str << endl;send(_sock, send_str.c_str(), send_str.size(), 0);// read// 拿到完整报文string package;if (!recvPackage(_sock, buffer, &package))continue;cout << "拿到的完整报文: \n" << package << endl;// 拿到正文string end_text;if (!delength(package, &end_text))continue;cout << "拿到的正文:" << end_text << endl;// 反序列化Response res;res.unserialize(end_text);cout << "exitCode: " << res._exitcode << " result: " << res._result << endl;}}}~Client(){if (_sock >= 0)close(_sock);}// 将客户输入的数据提取构造请求结构体对象Request ParseLine(const string &line){auto it = line.begin();// 提取左边的数字string left;while (it != line.end() && *it >= '0' && *it <= '9'){left += *it;++it;}int leftnum = atoi(left.c_str());// 提取符号while (it != line.end() && *it != '+' && *it != '-' && *it != '+' && *it != '/')++it;char op = *it;// 提取右边数字while (it != line.end() && (*it < '0' || *it > '9'))++it;string right;while (it != line.end() && *it >= '0' && *it <= '9'){right += *it;++it;}int rightnum = atoi(right.c_str());return Request(leftnum, rightnum, op);}private:int _sock;string _serverip;uint16_t _port;
};

Client.cc

#include "Client.hpp"
#include <memory>// 输出命令错误函数
void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}int main(int argc, char *argv[])
{// 再运行客户端时,输入的指令需要包括主机ip和端口号if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t port = atoi(argv[2]);unique_ptr<Client> client(new Client(serverip, port));client->Init();client->start();return 0;
}

log.txt

这里还加了一个记录日志的方法,可加可不加

#pragma once#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>using namespace std;#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *to_levelstr(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, ...)
{
#define NUM 1024char logpre[NUM];snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());char line[NUM];// 可变参数va_list arg;va_start(arg, format);vsnprintf(line, sizeof(line), format, arg);// 保存至文件FILE* log = fopen("log.txt", "a");FILE* err = fopen("log.error", "a");if(log && err){FILE *curr = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;if(level == ERROR || level == FATAL) curr = err;if(curr) fprintf(curr, "%s%s\n", logpre, line);fclose(log);fclose(err);}
}

通过结果再次理解通信过程

image-20230806220726845

所以最终的流程可以分解为几个步骤:

image-20230806221206524

Json

上面的序列化和反序列化过程呢都是自己定义的,所以看起来并不好看,而且可读性也不美观。

其实也会第三方库是帮我们做好了序列化和反序列化工作的,例如 Json,protobuf。因为Json的使用比较简单,所以这里就使用Json

首先需要安装第三方的 Jsoncpp的库

yum install jsoncpp-devel

安装好之后就可以使用第三方库了,需要注意因为是第三方库和线程库一样,编译的时候需要加上 -lJsoncpp的选项

肯定下面的代码注释就可以了解到Json的使用了,注:为了不修改上述的一些代码,下面使用条件编译,只看Json部分即可

// 请求
class Request
{
public:int _x;int _y;char _op;Request(): _x(0), _y(0), _op('0'){}Request(int x, int y, char op): _x(x), _y(y), _op(op){}// 序列化过程// 因为通信的数据一开始分为了好几个独立的元素// 所以将这些独立的元素合并成一个数据bool serialize(string *out){
#ifdef MYSELF*out = "";*out += to_string(_x);*out += SEP;*out += _op;*out += SEP;*out += to_string(_y);
#else// Value是万能类型// 需要先定义出对象Json::Value root;// Json是kv结构存储的,所以需要定义k值标识v值root["first"] = _x;root["second"] = _y;root["op"] = _op;// Json要写入值给别的变量也需要先定义对象// 写的对象类型可以有几种,这里采用FastWriterJson::FastWriter w;// 调用write方法就可以写入*out = w.write(root);
#endifreturn true;}// 反序列化过程// 将合并的一整个数据分解回原始的几个独立数据bool unserialize(const string &in){
#ifdef MYSELFauto left = in.find(SEP);auto right = in.rfind(SEP);if (left == string::npos || right == string::npos || left == right)return false;// 因为对于计算器而言,计算符号只有1位if (right - left - SEP_LEN != 1)return false;_x = stoi(in.substr(0, left));_y = stoi(in.substr(right + SEP_LEN));_op = in[left + SEP_LEN];
#else// 同样的需要先定义对象// 读的对象也需要定义Json::Value root;Json::Reader reader;// 调用读方法,将root的值读到in中reader.parse(in, root);// asInt表示切换为整形类型// 通过k值就可以得到v值_x = root["first"].asInt();_y = root["second"].asInt();_op = root["op"].asInt();
#endifreturn true;}
};// 响应请求
class Response
{
public:int _exitcode; // 返回码int _result;   // 返回结果Response(): _exitcode(0), _result(0){}Response(int exitcode, int result): _exitcode(exitcode), _result(result){}bool serialize(string *out){
#ifdef MYSELF*out = "";*out += to_string(_exitcode);*out += SEP;*out += to_string(_result);
#elseJson::Value root;root["exitcode"] = _exitcode;root["result"] = _result;Json::FastWriter w;*out = w.write(root);
#endifreturn true;}bool unserialize(const string &in){
#ifdef MYSELFauto pos = in.find(SEP);if (pos == string::npos)return false;_exitcode = stoi(in.substr(0, pos));_result = stoi(in.substr(pos + SEP_LEN));
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);_exitcode = root["exitcode"].asInt();_result = root["result"].asInt();
#endifreturn true;}
};

只需要更改序列化反序列化过程即可,外面的协定不需要改变

效果

image-20230806222713722

使用 Json序列化的就很美观

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

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

相关文章

选择排序(指针法)

描述 用选择法对10个整数排序。 输入 输入包含10个整数&#xff0c;用空格分隔。 输出 输出排序后的结果&#xff0c;用空格分隔。 输入样例 1 3 1 4 1 5 9 2 6 5 3 输出样例 1 1 1 2 3 3 4 5 5 6 9 输入样例 2 2 4 6 8 10 12 14 16 18 20 输出样例 2 2 4 6 8 1…

迭代器模式(Iterator)

迭代器模式是一种行为设计模式&#xff0c;可以在不暴露底层实现(列表、栈或树等)的情况下&#xff0c;遍历一个聚合对象中所有的元素。 Iterator is a behavior design pattern that can traverse all elements of an aggregate object without exposing the internal imple…

【二分查找】74. 搜索二维矩阵

74. 搜索二维矩阵 解题思路 方法1 将二维数组转换为一维数组使用二分查找 class Solution {public boolean searchMatrix(int[][] matrix, int target) {// 使用二分查找// 将矩阵写入一个数组中 然后使用二分查找算法int[] a new int[matrix.length * matrix[0].length];i…

机器学习常用Python库安装

机器学习常用Python库安装 作者日期版本说明Dog Tao2022.06.16V1.0开始建立文档 文章目录 机器学习常用Python库安装Anaconda简介使用镜像源配置 Pip简介镜像源配置 CUDAPytorch安装旧版本 TensorFlowGPU支持说明 DGL简介安装DGLLife RDKitscikit-multilearn Anaconda 简介 …

Python 程序设计入门(004)—— 赋值运算符与常用函数

Python 程序设计入门&#xff08;004&#xff09;—— 赋值运算符与常用函数 目录 Python 程序设计入门&#xff08;004&#xff09;—— 赋值运算符与常用函数一、赋值运算符二、常用的数学函数1、round() 函数2、pow() 函数3、divmod() 函数 三、字符串与 str() 函数1、字符串…

UEditorPlus v3.3.0 图片上传压缩重构,UI优化,升级基础组件

UEditor是由百度开发的所见即所得的开源富文本编辑器&#xff0c;基于MIT开源协议&#xff0c;该富文本编辑器帮助不少网站开发者解决富文本编辑器的难点。 UEditorPlus 是有 ModStart 团队基于 UEditor 二次开发的富文本编辑器&#xff0c;主要做了样式的定制&#xff0c;更符…

C++经典排序算法详解

目录 一、选择排序 二、冒泡排序 三、插入排序 一、选择排序 选择排序 选择排序&#xff08;Selection sort&#xff09;是一种简单直观的排序算法。它的工作原理是&#xff1a;第一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存…

解决vite+vue3项目npm装包失败

报错如下&#xff1a; Failed to remove some directories [ npm WARN cleanup [ npm WARN cleanup D:\\V3Work\\v3project\\node_modules\\vue, npm WARN cleanup [Error: EPERM: operation not permitted, rmdir D:\V3Work\v3project\node_modules\vue\reactivity\…

题解 | #B.Distance# 2023牛客暑期多校6

B.Distance 贪心(?) 题目大意 对于两个大小相同的多重集 A , B \mathbb{A},\mathbb{B} A,B &#xff0c;可以选择其中任一元素 x x x 执行操作 x x 1 xx1 xx1 任意次数&#xff0c;最少的使得 A , B \mathbb{A},\mathbb{B} A,B 相同的操作次数记为 C ( A , B ) C(\m…

嵌入式开发学习(STC51-13-温度传感器)

内容 通过DS18B20温度传感器&#xff0c;在数码管显示检测到的温度值&#xff1b; DS18B20介绍 简介 DS18B20是由DALLAS半导体公司推出的一种的“一线总线&#xff08;单总线&#xff09;”接口的温度传感器&#xff1b; 与传统的热敏电阻等测温元件相比&#xff0c;它是一…

虚函数表(vtable)

虚函数表&#xff08;通常简称为 vtable&#xff09;是 C 用于实现多态行为的一种机制。当一个类定义了虚函数或者继承了虚函数&#xff0c;编译器会为该类生成一个虚函数表。下面详细介绍虚函数表及其工作原理&#xff1a; 1. 什么是虚函数表&#xff1f; 虚函数表是一个存放…

关于Express 5

目录 1、概述 2、Express 5的变化 2.1 弃用或删除内容的列表&#xff1a; app.param&#xff08;name&#xff0c;fn&#xff09;名称中的前导冒号&#xff08;&#xff1a;&#xff09; app.del() app.param&#xff08;fn&#xff09; 复数方法名 res.json&#xff0…

Codeforces Round 890 (Div. 2) D. More Wrong(交互题 贪心/启发式 补写法)

题目 t(t<100)组样例&#xff0c;长为n(n<2000)的序列 交互题&#xff0c;每次你可以询问一个区间[l,r]的逆序对数&#xff0c;代价是 要在的代价内问出最大元素的位置&#xff0c;输出其位置 思路来源 neal Codeforces Round 890 (Div. 2) supported by Constructo…

springboot工程测试临时数据修改技巧

目录 properties临时属性测试注入 args临时参数测试注入 bean配置类属性注入&#xff08;Import&#xff09; SpringBootTest是一个注解&#xff0c;用于测试Spring Boot应用程序。它可用于指示Spring Boot测试应用程序的启动点&#xff0c;并为测试提供一个可用的Spring应用…

Godot 4 源码分析 - Path2D与PathFollow2D

学习演示项目dodge_the_creeps&#xff0c;发现里面多了一个Path2D与PathFollow2D 研究GDScript代码发现&#xff0c;它主要用于随机生成Mob var mob_spawn_location get_node(^"MobPath/MobSpawnLocation")mob_spawn_location.progress randi()# Set the mobs dir…

【C语言】初阶完结练习题

&#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 ✨收录专栏&#xff1a;C语言初阶 ✨其他专栏&#xff1a;代码小游戏 &#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论…

Misc取证学习

文章目录 Misc取证学习磁盘取证工具veracryto挂载fat文件DiskGenius 磁盘取证例题[RCTF2019]disk 磁盘[](https://ciphersaw.me/ctf-wiki/misc/disk-memory/introduction/#_2)内存取证工具volatility 内存取证例题数字取证赛题0x01.从内存中获取到用户admin的密码并且破解密码 …

ffmpeg视频音频命令

视频音频合并 视频音频合并&#xff0c;以视频时间为主&#xff0c;音频短了循环 方法1&#xff1a;混音&#xff0c;视频权重0&#xff0c;volume调节音量&#xff0c;aloop无限循环&#xff0c;duration:first为第一个素材的长度 ffmpeg -i video.mp4 -i audio.mp3 -filter_…

P3754. [NOIP2002 提高组] 字串变换

本题思路 纯bfs&#xff0c;注意一个字符中有多个相同的可变字符即可。 代码 void solve() {string a,b; cin>>a>>b; // 读入起始串和结束串vector<pair<string,string>> mss; // 可能有一个字符串的多个变换规则&#xff0c;不能用map&#xff0c;…

如何搭建一个成功的家具小程序

家具行业近年来发展迅猛&#xff0c;越来越多的消费者开始选择在小程序商城上购买家具。因此&#xff0c;制作一款家具小程序商城成为了许多家具商家的必然选择。那么&#xff0c;如何制作一款个性化、功能齐全的家具小程序商城呢&#xff1f;下面将为大家介绍一种简单且高效的…