【计算机网络】应用层自定义协议

自定义协议

  • 一、为什么需要自定义协议?
  • 二、网络版计算器
    • 1. 基本要求
    • 2. 序列化和反序列化
    • 3. 代码实现
      • (1)封装 socket
      • (2)定制协议和序列化反序列化
      • (3)客户端
      • (4)计算器服务端
      • (5)TCP服务端
      • (6)启动服务器
    • 4. 使用 JSON 进行序列化和反序列化
      • (1)安装 JSON 库
      • (2)测试 JSON
      • (3)在网络计算器中使用 JSON

一、为什么需要自定义协议?

我们上个知识点编写的TCP中,TCP是面向字节流的,我们怎么保证读取上来的数据是一个完整的报文呢?其实我们写的代码中不能保证这个问题,所以代码是有BUG的。TCP 叫做传输控制协议,也就是什么时候发送给对方,发多少,出错了怎么办,完全是由发送方的 TCP 协议来定!当我们使用 write() 函数向 sockfd 中写入数据时,数据不一定已经发给对方了,它的作用其实就是用户到内核的拷贝!这跟我们以前学的向文件中写入是一样的,我们将数据通过 fd 写入到内核的缓冲区,通过操作系统向磁盘中刷新缓冲区的内容。所以真正决定网路收发的协议是由 TCP 决定的!

而对于接收缓冲区来说,我们使用 read() 读取上来的数据就完全不确定了。所以我们在应用层就需要把协议定好,把协议定好才能更好的进行读上来的数据的分析!

所以回到最开始的问题,我们在进行读取的时候,怎么保证读取上来的数据是一个完整的报文呢?对于发送方,是将数据拷贝到它的 TCP 的发送缓冲区了,我们想怎么发完全不由应用层决定,是由 TCP 决定的,所以对方在它的接收缓冲区读上来的数据有可能是我们发送的一部分!所以对方在读的时候,怎么保证读到的是完整的呢?这就需要协议来进行定制了!

在这里插入图片描述

所以我们可以规定好通信双方只能使用固定大小的报文,即我们自己使用自定义协议。

二、网络版计算器

1. 基本要求

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

在应用层定协议,我们通常需要一个比较关键的字段。首先,协议本身就是一种“约定”,假设我们以实现网络版计算器为例,那么我们需要定义的第一个协议就是 request,代表需要相加的两个数和一个操作符,如下:

				struct request{int x;int y;char op;};

另外还需要定义另一个协议为 response,代表运算结果和正确性,如下:

				struct response{int result;int code;};

所以每一个结构体的每一个字段,每一个字段里的每一种值,我们都是要让客户端和服务器双方约定好的,约定好之后,我们使用结构化的方式,把约定表达出来,这就叫做我们定义出来的协议。

2. 序列化和反序列化

当我们向对方发信息时, 不仅仅只包含我们所发的信息,还有对应的头像,昵称和时间等等,实际上这些都是一个个的字符串,所以对方会收到四个字符串,但是肯定不能一个个发,是要把它们看作一个整体发给对方;而对方在收到这个整体的字符串后,就要将这个整体的字符串反向的转化成四个字符串,解析成信息内容、头像、昵称和时间。

那么怎么将这些信息看作一个整体呢?我们可以把需要发送的一个信息看作是一个结构体,其中这个结构体中有四个字段,分别代表上面的四个字符串;然后我们再把这个结构化的数据转化成为一个字符串,紧接着将这个字符串整体通过网络发送给对方主机,当对方主机收到这个字符串后,需要将这个字符串解析成为相同类型的结构化数据!在这个消息转化的过程,也是规定出来客户端和服务器双方约定出来的一种通用型的结构体,这就叫做双方定义出来的聊天协议。而在网络通信的时候,整个结构化的数据,把它多个字符串转化成一个字符串整体,这个过程我们称为序列化!而对方把一个字符串整体打散称为多个字符串这个过程称为反序列化

在这里插入图片描述

而以上的过程我们可以看作两层,一层是协议的定制,另一层是序列化和反序列化,如下图:

在这里插入图片描述

那么为什么需要进行序列和反序列化呢?主要是为了方便网络进行收发!

所以根据我们自定义的协议和序列化反序列化,我们的网络版计算机的简略流程如下:

在这里插入图片描述

下面我们根据上图的流程图简易实现一个网络版的计算器。

3. 代码实现

(1)封装 socket

每次提供网络通信都要重新编写 socket 套接字的代码,所以我们现在这里对 socket 进行一下简单的封装,代码如下:

				#pragma once#include <iostream>#include <string>#include <cstring>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netinet/in.h>#include "log.hpp"enum{SocketErr = 2,BindErr, ListenErr,};const int backlog = 10;class Sock {public:Sock(){}~Sock(){}public:void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if(_sockfd < 0){lg(Fatal, "socket error, %s: %d", strerror(errno), errno);exit(SocketErr);}}void Bind(uint16_t port){sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_addr.s_addr = INADDR_ANY;local.sin_family = AF_INET;local.sin_port = htons(port);if(bind(_sockfd, (const sockaddr*)&local, sizeof(local)) < 0){lg(Fatal, "bind error, %s: %d", strerror(errno), errno);exit(BindErr);}}void Listen(){if(listen(_sockfd, backlog) < 0){lg(Fatal, "listen error, %s: %d", strerror(errno), errno);exit(ListenErr);}}int Accept(std::string* client_ip, uint16_t* client_port){sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(_sockfd, (sockaddr*)&peer, &len);if(newfd < 0){lg(Warning, "accept error, %s: %d", strerror(errno), errno);return -1;}char buffer[64];inet_ntop(AF_INET, &peer.sin_addr, buffer, sizeof(buffer));*client_ip = buffer;*client_port = ntohs(peer.sin_port);return newfd;}void Close(){close(_sockfd);}bool Connect(std::string serverip, uint16_t serverport){sockaddr_in peer;memset(&peer, 0, sizeof(peer));inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));peer.sin_family = AF_INET;peer.sin_port = htons(serverport);int n = connect(_sockfd, (const sockaddr*)&peer, sizeof(peer));if(n < 0){lg(Fatal, "connect error, %s: %d", strerror(errno), errno);return false;}return true;}int GetFd(){return _sockfd;}private:int _sockfd;};

(2)定制协议和序列化反序列化

在进行定制协议的时候,为了保证对方接受时是一个完整的报文,也就是当对方进行读取时,对方怎么知道是一个报文多大多长呢?所以我们需要使用分隔符将报文和报文之间分隔开来,比如可以使用 \n,也就是使用 \n 对报文之间进行分隔。 但是我们在实现的时候,在报文前再加上一个字段,就是代表有效报文的长度,长度和报文之间也是使用 \n 进行分隔。那么在进行读取的时候,在遇到第一个 \n 之前,就是该报文的长度,然后根据长度去读取报文,就能保证读取到一个完整的报文,当遇到第二个 \n 就代表本次读取完毕,进行下一次读取。

Request

				const std::string blank_space_sep = " ";const std::string protocol_sep = "\n";// 定制协议class Request{public:Request(int x, int y, char op): _x(x), _y(y), _op(op){}Request(){}public:// 序列化bool Serialize(std::string *out){// 构建报文的有效载荷// struct => string// "len"\n"x op y"\nstd::string s = std::to_string(_x);s += blank_space_sep;s += _op;s += blank_space_sep;s += std::to_string(_y);*out = s;return true;}// 反序列化bool Deserialize(const std::string &in) // "x op y"{size_t left = in.find(blank_space_sep);if(left == std::string::npos) return false;std::string part_x = in.substr(0, left);size_t right = in.rfind(blank_space_sep);if(right == std::string::npos) return false;std::string part_y = in.substr(right + 1);if(left + 1 != right - 1) return false;_op = in[left + 1];_x = std::stoi(part_x);_y = std::stoi(part_y);return true;}void DebugPrint(){std::cout << "新请求构建完成: " << _x << _op << _y << "=?" << std::endl; }public:int _x;int _y;char _op;};

Response

				class Response{public:Response(int result, int code): _result(result), _code(code){}Response(){}public:// 序列化bool Serialize(std::string *out){// "len"\n"result code"\nstd::string s = std::to_string(_result);s += blank_space_sep;s += std::to_string(_code);*out = s;return true;}// 反序列化bool Deserialize(const std::string &in)     // "result code"{size_t pos = in.find(blank_space_sep);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;}void DebugPrint(){std::cout << "结果响应完成, result: " << _result << ", code: "<< _code << std::endl; }public:int _result;int _code; // 表示结果的准确性};

下面对封装报头和提取报文也进行简单封装:

				// 封装报头   "x op y" =>  "len"\n"x op y"\nstd::string Encode(std::string &content){std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;}// 提取报文   "len"\n"x op y"\n => "x op y"bool Decode(std::string& package, std::string* content){size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0, pos);size_t len = stoi(len_str);// package = len_str + content_str + 2(\n)// size_t total_len = len_str.size() + len + 2*protocol_sep.size();size_t total_len = len_str.size() + len + 2;if(package.size() < total_len) return false;*content = package.substr(pos + 1, len);// 如果已经得到一个完整的报文,需要移除这个报文package.erase(0, total_len);return true;}

(3)客户端

客户端首先创建需求,然后将需求序列化,并添加报头后通过网络进行发送,当服务端把计算结果返回响应时,客户端进行读取,将数据提取报文,并反序列化得到结果。

				#include <iostream>#include <string>#include <ctime>#include <unistd.h>#include "Socket.hpp"#include "Protocol.hpp"using namespace std;void Usage(const string& str){cout << "\nUsage: " << str << " serverip serverport\n\n" << endl;}int main(int argc, char* argv[]){if(argc != 3){Usage(argv[0]);exit(0);}uint16_t server_port = stoi(argv[2]);string server_ip = argv[1];Sock _sock;_sock.Socket();bool ret = _sock.Connect(server_ip, server_port); if(!ret){cerr << "client connect error" << endl;return 1;}srand(time(nullptr));int cnt = 1;string operas = "+-*/%=^&";string inbuffer_stream;while(cnt <= 10){cout << "====================第" << cnt << "次测试......" << endl;int x = rand() % 100 + 1;usleep(1000);int y = rand() % 100;usleep(1000);char op = operas[rand() % operas.size()];Request req(x, y, op);req.DebugPrint();string package;req.Serialize(&package);package = Encode(package);std::cout << "最新请求: \n" << package;write(_sock.GetFd(), package.c_str(), package.size());char buffer[128];size_t n = read(_sock.GetFd(), buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;inbuffer_stream += buffer;  // "len"\n"result code"\nstd::cout << inbuffer_stream << std::endl;string content;bool ret = Decode(inbuffer_stream, &content);   // "result code"if(!ret){cerr << "Decode err" << endl;return 2;}Response resp;ret = resp.Deserialize(content);if(!ret){cerr << "Deserialize err" << endl;return 3;}resp.DebugPrint();}cnt++;cout << "=================================" << endl;sleep(1);}_sock.Close();return 0;}

(4)计算器服务端

计算器服务端的 Calculator 方法对 package 进行提取报文,获取到需要计算的数据,然后进行反序列化进行计算后,再根据 Response 进行序列化,最后添加报头后返回。

				#pragma once#include <string>#include <iostream>#include "Protocol.hpp"enum{DIV_ERR = 1,MOD_ERR = 2,OP_ERR = 3};class ServerCal{public:ServerCal(){}Response CalculatorHelper(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 = MOD_ERR;elseresp._result = req._x % req._y;}break;case '/':{if (req._y == 0)resp._code = DIV_ERR;elseresp._result = req._x / req._y;}break;default:resp._code = OP_ERR;break;}return resp;}// "len"\n"10 + 20"\nstd::string Calculator(std::string &package){std::string content;bool ret = Decode(package, &content); // content = "10 + 20"if (!ret)return "";Request req;ret = req.Deserialize(content); // x = 10, y = 20, op = '+'if (!ret)return "";content = "";Response resp = CalculatorHelper(req); // result = 30, code = 0resp.Serialize(&content);              // content = "30 0"content = Encode(content);             // content = "len"\n"30 0\n"return content;}~ServerCal(){}};

(5)TCP服务端

TCP服务端获取到新连接后,根据返回的sockfd就可以进行网络通信,也就是获取到客户端的连接请求,紧接着我们创建子进程为其提供服务,首先进行数据读取,将读取到的数据每次添加到 inbuffer_stream 中,每次获取到数据都进行调用计算器服务端的 Calculator 方法,尝试对获取到的数据进行处理,如果处理成功,会在 Decode 方法中将已经提取的报文移除,所以不影响下次读取。当成功调用 Calculator 方法,就将计算结果发送回去。

				#pragma once#include <signal.h>#include <functional>#include "Socket.hpp"#include "log.hpp"using func_t = std::function<std::string(std::string &)>;class TcpServer{public:TcpServer(uint16_t port, func_t callback): _port(port), _callback(callback){}~TcpServer(){}bool InitServer(){_listen_sock.Socket();_listen_sock.Bind(_port);_listen_sock.Listen();lg(Info, "init server done");return true;}void Start(){signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);while (true){// 获取连接std::string client_ip;uint16_t client_port;int sockfd = _listen_sock.Accept(&client_ip, &client_port);if (sockfd < 0)continue;lg(Info, "accept a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip.c_str(), client_port);// 提供服务if (fork() == 0){_listen_sock.Close();std::string inbuffer_stream;// 数据计算while (true){char buffer[1280];ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;inbuffer_stream += buffer;lg(Debug, "debug: \n%s", inbuffer_stream.c_str());while (true){// 如果解析失败,会返回空串std::string info = _callback(inbuffer_stream);if (info.empty())break;write(sockfd, info.c_str(), info.size());}}else if (n == 0)break;elsebreak;}exit(0);}close(sockfd);}}private:uint16_t _port;Sock _listen_sock;func_t _callback;};

(6)启动服务器

				#include "TcpServer.hpp"#include "Protocol.hpp"#include "ServerCal.hpp"using namespace std;void Usage(const string& str){cout << "\nUsage: " << str << " port\n\n" << endl;}int main(int argc, char* argv[]){if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);ServerCal cal;TcpServer* tsvp = new TcpServer(port, bind(&ServerCal::Calculator, &cal, std::placeholders::_1));tsvp->InitServer();tsvp->Start();return 0;}

4. 使用 JSON 进行序列化和反序列化

JSON 其实是一种帮我们进行序列化和反序列化的工具,上面的序列化和反序列化都是我们自己写的,而现在我们可以直接使用 JSON 帮我们完成序列化和反序列化。

(1)安装 JSON 库

我们在 C++ 中想要使用 JSON,首先需要安装 jsoncpp 第三方库,在我们的云服务器上执行指令 sudo yum install jsoncpp-devel -y 即可。

安装成功后,我们可以通过 ls /usr/include/jsoncpp/json/ 查看到我们需要的头文件,下面我们使用到的是 json.h,但是系统默认的搜索路径是 /usr/include/,所以我们可以在包头文件的时候带上路径,也可以在编译选项中添加。

在这里插入图片描述

我们也可以在 /lib64/libjsoncpp.so 路径下找到 JSON 的第三方库,如下:

在这里插入图片描述

(2)测试 JSON

下面我们简单使用一下 JSON,我们先使用一下序列化的功能:

				int main(){Json::Value root;root["x"] = 10;root["y"] = 20;root["op"] = '*';Json::FastWriter w;string res = w.write(root);cout << res << endl;return 0;}

如上代码,我们创建一个 Value 万能对象,然后建立 k-v 映射关系,接下来创建一个 FastWriter 的对象,调用对象中的 write() 方法即可进行序列化,结果如下:

在这里插入图片描述

另外,在序列化的时候,我们还可以创建 StyledWriter 的对象,这种是按照特定风格形成的字符串,如下:

在这里插入图片描述

接下来我们进行反序列化,代码如下:

				int main(){Json::Value root;root["x"] = 10;root["y"] = 20;root["op"] = '*';Json::FastWriter w;// Json::StyledWriter w;string res = w.write(root);cout << res << endl;Json::Value v;Json::Reader r;r.parse(res, v);int x = v["x"].asInt();int y = v["y"].asInt();char op = v["op"].asInt();cout << "x = " << x << ", y = " << y << ", op = " << op << endl;return 0;}

如上代码,在反序列化中我们需要创建一个 Reader 对象,并调用对象中的 parse() 方法,该方法的第一个参数就是需要进行反序列化的字符串,第二个参数就是将反序列化后的字段需要写入到哪个对象中,结果如下:

在这里插入图片描述

(3)在网络计算器中使用 JSON

下面我们对网络版计算器的序列化和反序列化的部分进行修改,我们在该部分添加 JSON 代码,但是我们使用的是条件编译,可以让我们在自己的序列化和反序列化与 JSON 之间进行平滑的切换,代码如下:

				#pragma once#include <iostream>#include <string>#include <jsoncpp/json/json.h>// #define USE_MYSELF 1const std::string blank_space_sep = " ";const std::string protocol_sep = "\n";// 封装报头   "x op y" =>  "len"\n"x op y"\nstd::string Encode(std::string &content){std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;}// 提取报文   "len"\n"x op y"\n => "x op y"bool Decode(std::string& package, std::string* content){size_t pos = package.find(protocol_sep);if(pos == std::string::npos) return false;std::string len_str = package.substr(0, pos);size_t len = stoi(len_str);// package = len_str + content_str + 2(\n)// size_t total_len = len_str.size() + len + 2*protocol_sep.size();size_t total_len = len_str.size() + len + 2;if(package.size() < total_len) return false;*content = package.substr(pos + 1, len);// 如果已经得到一个完整的报文,需要移除这个报文package.erase(0, total_len);return true;}// 定制协议class Request{public:Request(int x, int y, char op): _x(x), _y(y), _op(op){}Request(){}public:// 序列化bool Serialize(std::string *out){#ifdef USE_MYSELF// 构建报文的有效载荷// struct => string// "len"\n"x op y"\nstd::string s = std::to_string(_x);s += blank_space_sep;s += _op;s += blank_space_sep;s += std::to_string(_y);*out = s;return true;#elseJson::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::FastWriter w;*out = w.write(root);return true;#endif}// 反序列化bool Deserialize(const std::string &in) // "x op y"{#ifdef USE_MYSELFsize_t left = in.find(blank_space_sep);if(left == std::string::npos) return false;std::string part_x = in.substr(0, left);size_t right = in.rfind(blank_space_sep);if(right == std::string::npos) return false;std::string part_y = in.substr(right + 1);if(left + 1 != right - 1) return false;_op = in[left + 1];_x = std::stoi(part_x);_y = std::stoi(part_y);return true;#elseJson::Value root;Json::Reader r;r.parse(in, root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();return true;#endif}void DebugPrint(){std::cout << "新请求构建完成: " << _x << _op << _y << "=?" << std::endl; }public:int _x;int _y;char _op;};class Response{public:Response(int result, int code): _result(result), _code(code){}Response(){}public:// 序列化bool Serialize(std::string *out){#ifdef USE_MYSELF// "len"\n"result code"\nstd::string s = std::to_string(_result);s += blank_space_sep;s += std::to_string(_code);*out = s;return true;#else Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter w;*out = w.write(root);return true;#endif}// 反序列化bool Deserialize(const std::string &in)     // "result code"{#ifdef USE_MYSELFsize_t pos = in.find(blank_space_sep);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;#elseJson::Value root;Json::Reader r;r.parse(in, root);_result = root["result"].asInt();_code = root["code"].asInt();return true;#endif}void DebugPrint(){std::cout << "结果响应完成, result: " << _result << ", code: "<< _code << std::endl; }public:int _result;int _code; // 表示结果的准确性};

我们可以通过在编译选项中加上宏定义的选项,使我们更方便地选择哪种序列化和反序列化的方式,例如 makefile 文件中:

				.PHONY:allall:servercal clientcalFlag=-DUSE_MYSELF=1Lib=-ljsoncppservercal:ServerCal.ccg++ -o $@ $^ -std=c++11 $(Lib) $(Flag)clientcal:ClientCal.cc g++ -o $@ $^ -std=c++11 $(Lib) $(Flag).PHONY:clean clean:rm -f servercal clientcal

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1can36hco3ehk

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

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

相关文章

Javaweb之SpringBootWeb案例之自动配置以及常见方案的详细解析

3.2 自动配置 我们讲解了SpringBoot当中起步依赖的原理&#xff0c;就是Maven的依赖传递。接下来我们解析下自动配置的原理&#xff0c;我们要分析自动配置的原理&#xff0c;首先要知道什么是自动配置。 3.2.1 概述 SpringBoot的自动配置就是当Spring容器启动后&#xff0c…

【论文笔记】An Effective Adversarial Attack on Person Re-Identification ...

原文标题&#xff08;文章标题处有字数限制&#xff09;&#xff1a; 《An Effective Adversarial Attack on Person Re-Identification in Video Surveillance via Dispersion Reduction》 Abstract 通过减少神经网络内部特征图的分散性攻击reid模型。 erbloo/Dispersion_r…

Vue3中组件通讯的方式

Vue3中组件通讯的方式 1 &#x1f916;GPT&#x1f916;: (答案有点问题混淆了vue2的内容) 父组件向子组件传递数据 props 子组件通过 props 属性从父组件接收数据。emit事件子组件通过emit 事件 子组件通过 emit事件子组件通过emit 发射事件向父组件发送消息。provide / in…

Chrome插件 | WEB 网页数据采集和爬虫程序

无边无形的互联网遍地是数据&#xff0c;品类丰富、格式繁多&#xff0c;包罗万象。数据采集&#xff0c;或说抓取&#xff0c;就是把分散各处的内容&#xff0c;通过各种方式汇聚一堂&#xff0c;是个有讲究要思考的体力活。君子爱数&#xff0c;取之有道&#xff0c;得注意遵…

mobile app 安全扫描工具MobSF了解下

可以干啥&#xff1a; static 静态分析 dynamic 动态分析 可以用来渗透了 如何docker安装 docker image 下载地址https://hub.docker.com/r/opensecurity/mobile-security-framework-mobsf/ setup 两行即可 1 docker pull opensecurity/mobile-security-framework-mobsf…

年轻人怎么搞钱?

年轻人想要搞钱&#xff0c;可以考虑以下几个方面&#xff1a; 1. 创业&#xff1a;年轻人可以通过自己的创意&#xff0c;找到一个市场的空缺&#xff0c;开创自己的业务。可以从比较小的项目开始&#xff0c;逐渐扩大范围&#xff0c;积累经验和财富。 2. 投资&#xff1a;…

Hadoop之HDFS——【模块二】数据管理

一、Namespace的概述 1.1.集群与命名空间的关系 类似于大集群与小集群之间的关系,彼此之间独立又相互依存。每个namespace彼此独立,Namespace工作时只负责维护本区域的数据,同时所有的namespace维护的文件都可以共用DataNode节点,为了区分数据属于哪些Namespace,DataNode…

强大而灵活的python装饰器

装饰器&#xff08;Decorators&#xff09; 一、概述 在Python中&#xff0c;装饰器是一种特殊类型的函数&#xff0c;它允许我们修改或增强其他函数的功能&#xff0c;而无需修改其源代码。装饰器在函数定义之后立即调用&#xff0c;并以函数对象作为参数。装饰器返回一个新…

CrossOver 24下载-CrossOver 24 for Mac下载 v24.0.0中文永久版

CrossOver 24是一款可以让mac用户能够自由运行和游戏windows游戏软件的虚拟机类应用&#xff0c;虽然能够虚拟windows但是却并不是一款虚拟机&#xff0c;也不需要重启系统或者启动虚拟机&#xff0c;类似于一种能够让mac系统直接运行windows软件的插件。它以其出色的跨平台兼容…

NVMe开发——PCIe复位

简介 PCIe中有4种复位机制&#xff0c;早期的3种被称为传统复位(Conventional Reset)。传统复位中的前2种又称为基本复位(Fundamental Resets)&#xff0c;分别为冷复位(Cold Reset)&#xff0c;暖复位(Warm Reset)。第3种复位为热复位(Hot Reset)。第4种复位被称为功能级复位…

179基于matlab的2D-VMD处理图像

基于matlab的2D-VMD处理图像&#xff0c;将图片进行VMD分解&#xff0c;得到K个子模态图&#xff0c;将每个模态图进行重构&#xff0c;得到近似的原图。可以利用这点进行图像去噪。程序已调通&#xff0c;可直接运行。 179 2D-VMD 图像分解重构 图像处理 (xiaohongshu.com)

每日五道java面试题之spring篇(九)

目录&#xff1a; 第一题. 说一下Spring的事务传播行为第二题. 说一下 spring 的事务隔离&#xff1f;第三题. Spring AOP and AspectJ AOP 有什么区别&#xff1f;AOP 有哪些实现方式&#xff1f;第四题. JDK动态代理和CGLIB动态代理的区别第五题. 解释一下Spring AOP里面的几…

【白嫖8k买的机构vip教程】Appium自动化(3):Appium-Desktop界面介绍

Appium-Desktop主界面包含三个菜单Simple、Advanced、Presets Simple界面&#xff1a; Host设置Appium server的ip地址&#xff0c;本地调试可以将ip地址修改为127.0.0.1&#xff1b;Port设置端口号&#xff0c;默认是4723不用修改Start Server 启动 Appium serverEdit Confi…

重生奇迹MU玩家容易遇到的问题

1、玩家可以在画面左上角座标旁找到「奇迹助手」的小按钮&#xff0c;用它来开启介面。 2、打怪范围&#xff1a;自动寻找所设定范围内的怪物&#xff0c;勾选后角色搜索范围内若无可攻击的目标&#xff0c;将会随机移动位置直到有攻击目标为止&#xff0c;但移动范围不超出所…

搭建LNMP环境并搭建论坛和博客

目录 一、LNMP架构原理 二、编译安装Nginx 三、编译安装MySQL 四、编译安装PHP 五、配置Nginx支持PHP解析 六、安装论坛 七、安装博客 一、LNMP架构原理 LNMP架构&#xff0c;是指在Linux平台下&#xff0c;由运行Nginx的web服务器&#xff0c;运行PHP的动态页面解析程序…

Python:练习:编写一个程序,录入一个美元数量(int),然后显示出增加%5税率后的相应金额。

案例&#xff1a; 编写一个程序&#xff0c;录入一个美元数量&#xff08;int&#xff09;&#xff0c;然后显示出增加%5税率后的相应金额。格式如下所示&#xff1a; Enter an amount:100 With tax added:$105.0 思考&#xff1a; 1、录入一个美元数量&#xff0c;录入&am…

解决GitHub无法访问的问题:手动修改hosts文件与使用SwitchHosts工具

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

SpringBoot原理-配置优先级(黑马学习笔记)

配置优先级 在我们前面的课程当中&#xff0c;我们已经讲解了SpringBoot项目当中支持的三类配置文件&#xff1a; ● application.properties ● application.yml ● application.yaml 在SpringBoot项目当中&#xff0c;我们要想配置一个属性&#xff0c;可以通过这三种方…

Liunx前后端项目部署(小白也可安装)

文章目录 一、CentOS服务器的安装二、jdk安装三、Tomcat安装四、MySQL安装、五、nginX安装六、多个项目负载均衡&#xff0c;部署后端项目七、前端项目部署 一、CentOS服务器的安装 选择liunx&#xff0c;下面选择CentOS 7 ![在这里插入图片描述](https://img-blog.csdnimg.cn…

一文讲透:可视化大屏中3D元素的融入和使用方法

在可视化大屏中&#xff0c;3D元素融入的越来越多&#xff0c;贝格前端工场经常接到这类项目&#xff0c;很多老铁认为加个3D效果很easy&#xff0c;其实不然&#xff0c;工序非常复杂&#xff0c;总结如下。 一、什么是3D技术 三维展示&#xff08;3D展示&#xff09;是指使用…