【计网】自定义序列化反序列化(二) —— 实现网络版计算器【上】
🌎 实现网络版计算器【上】
文章目录:
实现网络版计算器【上】
自定义协议
制定自定义协议
Jsoncpp序列化反序列化
Json::Value类
Jsoncpp序列化
Jsoncpp反序列化
自定义协议序列化反序列化
Service 服务改写
服务器端完结
🚀自定义协议
✈️制定自定义协议
在上一篇我们说了,两台机器想要通过网络进行通信,那么通信双方所遵循的协议必须相同,应用层也是如此,大部分情况,双方首先约定好数据传输格式,那么一端计算机发送的数据,另外一端也就能相应的解析。
- 那为什么我们需要序列化和反序列化呢?
我们既然定义好了双方的通信协议,那么我们直接按照通信协议进行发送不就行了?这是因为,在很多时候,大型项目里一般底层代码都不会改变,但是上层就说不准了,双方的结构化数据很容易会因为业务需求的变动而变动,每一次变动都可能需要去处理跨平台问题。
比如Linux x64平台与Linux x32平台的内存对齐方式就不同,如果双方协议一直在改变,那么就必须要一同处理这种平台差异,是一种费时费力不讨好的表现。
但是我们在双发发数据之前进行了序列化,进行了字符串式的处理,相当于添加了一层软件层。这样上层无论怎么变,底层收到的代码都是字符串,把业务逻辑的改变抛给上层解决,这样就不用因为上层的变动而更改底层代码逻辑。
所以序列化与反序列化问题显得尤为重要,就上一篇所谈论的计算问题进行序列化与反序列化处理。处理两个模块,一个是客户端发来的请求,一个是服务器端处理请求后返回的响应。所以在这里设计两个类 Request 和 Response。
我们目的是实现网络版计算器,客户端发送Request,其中包含左操作数,右操作数,以及运算符。服务器端需要对Request进行处理,但是不排除客户端发送的数据是非法运算,所以Response类不仅记录结果,还需要记录运算错误方式。同时,双方都需要进行序列化和反序列化操作:
namespace protocol_ns
{class Request{public:Request(){}Request(int x, int y, char oper):_x(x), _y(y), _oper(oper){}bool Serialize(const std::string *message)// 序列化{}bool Deserialize(const std::string &in)// 反序列化{}private:int _x;int _y;char _oper;// + - * / %, _x _oper _y};class Response{public:Response(){}Response(int result, int code):_result(result), _code(code){}bool Serialize(const std::string *message)// 序列化{}bool Deserialize(const std::string &in)// 反序列化{}private:int _result;int _code;// 0: success, 1: 除0, 2: 非法操作};
} // namespace protocol_ns
上述的Request与Response就是双方约定的协议,那么具体的协议内容如何进行规定呢?我们知道,我们发送的数据很可能会积压在发送缓冲区,而Tcp一旦发送有可能一次发送的是多个序列化之后的字符串,那么服务器端在收到这些数据之后需要对每一条完整的数据进行区分。甚至发送过去的数据不完整,需要等待下一次发送,哪些能处理哪些需要延迟处理,都是我们需要考虑的。
既然是协议,我们就采用其他网络协议那样,定义为 报文 = 报头 + 有效载荷,我们进行如下定义:"有效载荷长度"\r\n"有效载荷"
如果你愿意,你也可以在报头部分加上类型等判定比如 "有效载荷长度""数据类型"\r\n"有效载荷"
,不过这里我们不搞这么麻烦了,就采用前面一种报文方式。
其中 \r\n 代表分隔符,将报头部分与 有效载荷进行分离。比如,一个客户端发来请求的格式就是:"有效载荷长度"\r\n"_x _oper _y"\r\n
,有效载荷长度不记录分隔符,只记录引号内有效载荷的长度。
那么,如果服务器端收到的字符串进行解析,报头部分显示有效载荷长度是100,但是现在只有50,所以我们就需要在等数据完整再进行处理。
🚀Jsoncpp序列化反序列化
Jsoncpp 是一个用于处理 JSON 数据的 C++ 库。它提供了将 JSON 数据序列化为字符串以及从字符串反序列化为 C++ 数据结构的功能。Jsoncpp 是开源的,广泛用于各种需要处理 JSON 数据的 C++ 项目中。
- Jsoncpp特性:
- 简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
- 高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
- 全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
- 错误处理:在解析 JSON 数据时,Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。
其中在Linux环境下安装Jsoncpp库的命令如下:
ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel
✈️Json::Value类
Json::Value
是 Jsoncpp 库中的一个重要类,用于表示和操作 JSON 数据结构。以下是一些常用的 Json::Value 操作列表:
构造函数:
- Json::Value():默认构造函数,创建一个空的 Json::Value 对象。
- Json::Value(ValueType type, bool allocated = false):根据给定的ValueType(如 nullValue, intValue, stringValue 等)创建一个 Json::Value 对象。
访问元素:
- Json::Value& operator[](const char* key):通过键(字符串)访问对象中的元素。如果键不存在,则创建一个新的元素。
- Json::Value& operator[](const std::string& key):同上,但使用std::string 类型的键。
- Json::Value& operator[](ArrayIndex index):通过索引访问数组中的元素。如果索引超出范围,则创建一个新的元素。
- Json::Value& at(const char* key):通过键访问对象中的元素,如果键不存在则抛出异常。
- Json::Value& at(const std::string& key):同上,但使用 std::string类型的键。
类型检查:
- bool isNull():检查值是否为 null。
- bool isBool():检查值是否为布尔类型。
- bool isInt():检查值是否为整数类型。
- bool isInt64():检查值是否为 64 位整数类型。
- bool isUInt():检查值是否为无符号整数类型。
- bool isUInt64():检查值是否为 64 位无符号整数类型。
- bool isIntegral():检查值是否为整数或可转换为整数的浮点数。
- bool isDouble():检查值是否为双精度浮点数。
- bool isNumeric():检查值是否为数字(整数或浮点数)。
- bool isString():检查值是否为字符串。
- bool isArray():检查值是否为数组。
- bool isObject():检查值是否为对象(即键值对的集合)。
赋值和类型转换:
- Json::Value& operator=(bool value):将布尔值赋给 Json::Value 对象。
- Json::Value& operator=(int value):将整数赋给 Json::Value 对象。
- Json::Value& operator=(unsigned int value):将无符号整数赋给Json::Value 对象。
- Json::Value& operator=(Int64 value):将 64 位整数赋给 Json::Value对象。
- Json::Value& operator=(UInt64 value):将 64 位无符号整数赋给Json::Value 对象。
- Json::Value& operator=(double value):将双精度浮点赋给Json::Value 对象。
- Json::Value& operator=(const char* value):将 C 字符串赋给Json::Value 对象。
- Json::Value& operator=(const std::string& value):将std::string赋给Json::Value 对象。
- bool asBool():将值转换为布尔类型(如果可能)。
- int asInt():将值转换为整数类型(如果可能)。
- Int64 asInt64():将值转换为 64 位整数类型(如果可能)。
- unsigned int asUInt():将值转换为无符号整数类型(如果可能)。
- UInt64 asUInt64():将值转换为 64 位无符号整数类型(如果可能)。
- double asDouble():将值转换为双精度浮点数类型(如果可能)。
- std::string asString():将值转换为字符串类型(如果可能)。
数组和对象操作:
- size_t size():返回数组或对象中的元素数量。
- bool empty():检查数组或对象是否为空。
- void resize(ArrayIndex newSize):调整数组的大小。
- void clear():删除数组或对象中的所有元素。
- void append(const Json::Value& value):在数组末尾添加一个新元素。
- Json::Value& operator[](const char* key, const Json::Value&defaultValue = Json::nullValue):在对象中插入或访问一个元素,如果键不存在则使用默认值。
- Json::Value& operator[](const std::string& key, const Json::Value& defaultValue = Json::nullValue):同上,但使用 std::string类型。
✈️Jsoncpp序列化
序列化指的是将数据结构或对象转换为一种格式,以便在网络上传输或存储到文件中。Jsoncpp 提供了多种方式进行序列化,这里不再做详细解释,直接使用最简单的两种展示给大家:
使用
Json::FastWriter
进行Json格式序列化:
首先,我们先定义结构化数据Stu,结构体内记录的是name,age,weight,首先我们需要使用 Json::Value 对象将结构化数据转化为字符串:
#include <iostream>
#include <jsoncpp/json/json.h>struct stu
{std::string name;int age;double weight;
};int main()
{// 结构化数据struct stu zs = {"阿熊", 20, 80};// 转换为字符串Json::Value root;root["name"] = zs.name;root["age"] = zs.age;root["weight"] = zs.weight;// to do ...return 0;
}
接着使用 Json::Writer 对象将root的各个分散字符串转化为一个字符串:
int main()
{// 结构化数据struct stu zs = {"阿熊", 20, 80};// 转换为字符串Json::Value root;root["name"] = zs.name;root["age"] = zs.age;root["weight"] = zs.weight;Json::FastWriter writer;std::string str = writer.write(root);std::cout << str;return 0;
}
这里还有一个需要注意的点,当我们在Linux下进行编译的时候,直接编译会报如下错误:
这是因为Jsoncpp库属于第三方库,要想使用Jsoncpp库就必须在编译时带上 -ljsoncpp
选项,表示链接到Jsoncpp库:
上面的数据实际上就是结构化数据进行序列化之后的字符串,其原本应该是:"{"age":20,"name":"阿熊","weight":80}"
。
使用
Json::StyleWriter
进行Json格式序列化:
代码还是上述的代码,只是把Json::FastWriter类型替换为 Json::StyleWriter 类型:
#include <iostream>
#include <jsoncpp/json/json.h>struct stu
{std::string name;int age;double weight;
};int main()
{// 结构化数据struct stu zs = {"阿熊", 20, 80};// 转换为字符串Json::Value root;root["name"] = zs.name;root["age"] = zs.age;root["weight"] = zs.weight;// Json::FastWriter writer;Json::StyledWriter writer;std::string str = writer.write(root);std::cout << str;return 0;
}
这两种序列化方式我们采用任何一种即可。
✈️Jsoncpp反序列化
反序列化指的是将序列化后的数据重新转换为原来的数据结构或对象。Jsoncpp 提供了以下方法进行反序列化:
首先,我们预先将Jsoncpp序列化后的字符串信息放在了一个txt文件当中,将来只需要从文件中读取信息并进行反序列化即可,向out.txt文件中读取信息:
#include <iostream>
#include <fstream>
#include <jsoncpp/json/json.h>struct stu
{std::string name;int age;double weight;public:void Debug(){std::cout << name << std::endl;std::cout << age << std::endl;std::cout << weight << std::endl;}
};int main()
{// 读取字符串信息std::ifstream in("out.txt");if(!in.is_open()) return 1;char buffer[1024];in.read(buffer, sizeof(buffer));in.close();return 0;
}
接下来就是进行反序列化的过程,我们使用 Json::Reader
对象调用 parse()
接口把序列化的字符串进行分割到 Json::Value 的对象当中,最后再将Stu对象的各个值拷贝。
#include <iostream>
#include <fstream>
#include <jsoncpp/json/json.h>struct stu
{std::string name;int age;double weight;public:void Debug(){std::cout << name << std::endl;std::cout << age << std::endl;std::cout << weight << std::endl;}
};int main()
{std::ifstream in("out.txt");if(!in.is_open()) return 1;char buffer[1024];in.read(buffer, sizeof(buffer));in.close();std::string json_str = buffer;Json::Value root;Json::Reader reader;bool ret = reader.parse(json_str, root);(void)ret;struct stu zs;zs.name = root["name"].asString();zs.age = root["age"].asInt();zs.weight = root["weight"].asDouble();zs.Debug();return 0;
}
🚀自定义协议序列化反序列化
经过上述的json序列化和反序列化的过程,我们可以将此应用到我们自定义协议 Request 和 Response类当中的序列化和反序列化:
#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <jsoncpp/json/json.h>namespace protocol_ns
{class Request{public:Request(){}Request(int x, int y, char oper) : _x(x), _y(y), _oper(oper){}bool Serialize(std::string *out){// 转换成为字符串Json::Value root;root["x"] = _x;root["y"] = _y;root["oper"] = _oper;Json::FastWriter writer;// Json::StyledWriter writer;*out = writer.write(root);return true;}bool Deserialize(const std::string &in) // 你怎么知道你读到的in 就是完整的一个请求呢?{Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_x = root["x"].asInt();_y = root["y"].asInt();_oper = root["oper"].asInt();return true;}public:int _x;int _y;char _oper; // "+-*/%" _x _oper _y}; // --- "字符串"class Response{public:Response(){}Response(int result, int code) : _result(result), _code(code){}bool Serialize(std::string *out){// 转换成为字符串Json::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter writer;// Json::StyledWriter writer;*out = writer.write(root);return true;}bool Deserialize(const std::string &in){Json::Value root;Json::Reader reader;bool res = reader.parse(in, root);if (!res)return false;_result = root["result"].asInt();_code = root["code"].asInt();return true;}public:int _result; // 结果int _code; // 0:success 1: 除0 2: 非法操作 3. 4. 5}; // --- "字符串"
}
序列化之后的字符串还不够,我们还需要给字符串添加报头以及分隔符,组成一个网络报文发送给对端,所以在制定协议当中我们需要添加Encode()接口对有效载荷进行封装:
const std::string SEP = "\r\n"; // 分隔符std::string Encode(const std::string &json_str)
{int json_str_len = json_str.size();std::string proto_str = std::to_string(json_str_len);proto_str += SEP;proto_str += json_str;proto_str += SEP;return proto_str;
}
那么既然有对有效载荷的封装,就一定存在对网络报文的解包,所以Decode()接口也是必须的接口,用来对我们自定义网络报文进行解包,首先我们需要寻找分隔符,如果连报头都找不到就说明这批数据并不是自己的数据,直接返回一个空串。那么接着就一定会带有\r\n。
除了完整的分隔符以外,我们还必须得收到报头部分,也就是有效载荷长度信息,如果没有找到报头部分,直接返回空串。这个时候往后执行就必定能拿到报头信息,接下来就是有效载荷部分,我们知道,有效载荷两边都有分隔符,如果想要Decode()接口确认一个完整的请求,应该至少有 初始分隔符长度 + 有效载荷的长度 + 两个分隔符的长度,这样才能保证,Decode的数据至少有一个完整报文:
td::string Decode(std::string &inbuffer)
{auto pos = inbuffer.find(SEP);if (pos == std::string::npos)// 未发现分隔符的位置,直接返回一个空串return std::string();std::string len_str = inbuffer.substr(0, pos);if (len_str.empty())return std::string();int packlen = std::stoi(len_str);int total = packlen + len_str.size() + 2 * SEP.size();// 一个完整报文长度if (inbuffer.size() < total)// 如果没有一个完整报文直接返回空串return std::string();std::string package = inbuffer.substr(pos + SEP.size(), packlen);// 有效载荷进行分离inbuffer.erase(0, total);// 报文已经处理完成,将处理完成后的报文删除return package;
}
这样,一个简单的序列化和反序列化过程我们就已经完成了。
✈️Service 服务改写
那么简单的协议框架我们就已经搭建完毕,接着将视角转回到服务器端,TcpServer 我们已经改写完毕,那么就需要再main函数中将Service 接口进行完善并编写启动服务器的demo。
#include <iostream>
#include <functional>
#include <memory>#include "Log.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "CalCulate.hpp"using namespace protocol_ns;void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " local_port\n"<< std::endl;
}void Service(socket_sptr sockptr, InetAddr client)
{LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";while(true){char inbuffer[1024];ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);if(n > 0){inbuffer[n] = 0;std::cout << clientaddr << inbuffer << std::endl;std::string echo_string = "[server echo]# ";echo_string += inbuffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){//client 退出 && 关闭链接LOG(INFO, "%s quit", clientaddr.c_str());break;}else{LOG(ERROR, "read error", clientaddr.c_str());break;}}::close(sockfd);
}// ./tcpserver port
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return 1;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, Service);tsvr->Loop();return 0;
}
我们将Service 接口中的close放在线程回调当中,具体的服务不用管是否需要关闭文件描述符,而在 HandlerSock 中,没有具体的sockfd,虽然 ThreadData类内有构造 Socket 的智能指针,但是我们并没有对应的Get函数将私有成员变量返回出来,所以在模版方法模式中我们应该添加一些常用的接口:
class Socket
{public:virtual void CreateSocketOrDie() = 0; // 创建套接字virtual void BindSocketOrDie(InetAddr &addr) = 0; // 绑定套接字virtual void ListenSocketOrDie() = 0; // 监听套接字virtual socket_sptr Accepter(InetAddr *addr) = 0;virtual bool Connector(InetAddr &addr) = 0;virtual int SockFd() = 0;// 返回sockfdvirtual int Recv(std::string *out) = 0;// 接收消息virtual int Send(std::string &in) = 0; // 发送消息// to do...
};
父类构建了抽象函数,那么子类 TcpSocket 必须对父类抽象函数进行重写:
int SockFd() override
{return _sockfd;
}int Recv(std::string *out) override
{char inbuffer[1024];ssize_t n = ::recv(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){inbuffer[n] = 0;*out += inbuffer;// 接收文件采用的是 += 表示在out中追加数据}return n;
}int Send(std::string &in) override
{int n = ::send(_sockfd, in.c_str(), in.size(), 0);return n;
}
注意在Recv()接口中我们将读取的数据追加到out中,这是因为我们每次读取的并不一定是完整的序列化字符串,所以我们需要对每一次来的数据进行追加,尽量组成完整的请求。那么线程的回调函数就可以通过ThreadData 对象调用 TcpSocket 的 SockFd() 接口了:
static void* HandlerSock(void* args)
{pthread_detach(pthread_self());ThreadData* td = static_cast<ThreadData*>(args);td->self->_service(td->sockfd, td->clientaddr);::close(td->sockfd->SockFd());// 不关闭会导致 文件描述符泄漏的问题(文件描述符不归还)delete td;return nullptr;
}
这个时候Service 服务我们不再直接使用原生 recv接口了,所以我们要对Service 进行改写,我们需要思考一个问题,我们怎么能保证自己读取的是一个完整的客户端请求(在原本的Service接口中我们并没有做这样的处理,也没关心过这样的问题,所以改写是必不可少的),尽管在Recv()中我们是进行追加接收信息的,但是发送信息的是Tcp,不一定会一次性发送一次完整的报文,所以我们无法保证每一次都是完整的请求。那么我们检测到如果当前的报文不完整,我们进行循环等待新的数据,直到报文完整:
void ServiceHelper(socket_sptr sockptr, InetAddr client)
{int sockfd = sockptr->SockFd();LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";std::string inbuffer;while (true){sleep(5);Request req;// 1. 读取数据int n = sockptr->Recv(&inbuffer);if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2. 分析数据std::string package = Decode(inbuffer);// 使用了Decode()接口,那么就一定能保证读取到一个完整的json串if(package.empty()) continue;req.Deserialize(package);// 反序列化有效载荷,就能够得到完整的信息了
}
我们将Service封装为一个类,这样方便将来进行回调,而回调函数就是具体的对已经反序列化的结果进行算术运算,参数应当是收到Request,返回一个Response:
using callback_t = std::function<Response(const Request &req)>;// 我们发送一个Request返回一个Responseclass Service
{
public:Service(callback_t cb): _cb(cb){}void ServiceHelper(socket_sptr sockptr, InetAddr client){int sockfd = sockptr->SockFd();LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";std::string inbuffer;while (true){sleep(5);Request req;// 1. 读取数据int n = sockptr->Recv(&inbuffer);if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2. 分析数据std::string package = Decode(inbuffer);// 使用了Decode()接口,那么就一定能保证读取到一个完整的json串if(package.empty()) continue;// 3. 反序列化req.Deserialize(package);// 反序列化有效载荷,就能够得到完整的信息了}}private:callback_t _cb;// 回调
};
以上,属于读取与分析数据的部分,以及反序列化获取完整报文。获取了完整的报文之后,我们就可以拿着客户端发来的数据做业务处理,我们的业务是模拟简单计算器,我们设置的回调就是本次的业务代码,我们单独将业务代码封装为一个简单的类:
#pragma once#include <iostream>
#include "Protocol.hpp"using namespace protocol_ns;class Calculate
{
public:Calculate(){}Response Excute(const Request& req)// 参数为 Request类型,返回值为Response类型的服务{Response resp(0, 0);switch (req._oper){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 = 1;}else{resp._result = req._x / req._y;}break;} case '%':{if(req._y == 0){resp._code = 2;}else{resp._result = req._x % req._y;}break;} default:resp._code = 3;break;}return resp;}~Calculate(){}
private:};
反序列化之后就需要处理客户端发来的请求,处理完请求我们就可以得到一个Response,也就是处理之后得到的结果,接着,服务器端就需要把这个结果返回给客户端,所以对Response进行序列化,并添加报头,最后再发送到对端,服务器端这次的工作就完成了:
while (true)
{sleep(5);Request req;// 1. 读取数据int n = sockptr->Recv(&inbuffer);if (n < 0){LOG(DEBUG, "client %s quit", clientaddr.c_str());break;}// 2. 分析数据std::string package = Decode(inbuffer);// 使用了Decode()接口,那么就一定能保证读取到一个完整的json串if(package.empty()) continue;// 3. 反序列化req.Deserialize(package);// 反序列化有效载荷,就能够得到完整的信息了// 4. 业务处理Response resp = _cb(req);// 5. 对应答进行序列化std::string send_str;resp.Serialize(&send_str);std::cout << send_str << std::endl;// 6. 添加长度报头send_str = Encode(send_str);// 7. 发送到对端sockptr->Send(send_str);
}
✈️服务器端完结
这样,我们将Service服务改写完成,而在main函数当中,我们需要运行我们的服务器,并且创建线程去处理相关的任务:
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return 1;}uint16_t port = std::stoi(argv[1]);EnableScreen();Calculate cal;// 构造计算服务对象Service calservice(std::bind(&Calculate::Excute, &cal, std::placeholders::_1));// Calservice类内实现的Excute是我们用来对客户端请求处理的函数,但是属于类内函数,带有隐藏this指针,所以我们需要对其进行绑定,将this 指针绑定进来io_service_t service = std::bind(&Service::ServiceHelper, &calservice, std::placeholders::_1, std::placeholders::_2);// 同样,service也是封装为了一个类,线程想要对其进行回调,每次都得传Service类的构造当做第一个参数,为了避免这种麻烦,使用bind将this绑定std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, service);// 正常创建对象tsvr->Loop();// 进行循环return 0;
}
这样,服务器端的工作我们就准备完毕。
相关文章:
【计网】自定义序列化反序列化(二) —— 实现网络版计算器【上】
🌎 实现网络版计算器【上】 文章目录: 实现网络版计算器【上】 自定义协议 制定自定义协议 Jsoncpp序列化反序列化 Json::Value类 Jsoncpp序列化 Jsoncpp反序列化 自定义协议序列化反序列化 …...
Harbor安装、HTTPS配置、修改端口后不可访问?
Harbor安装、HTTPS配置、修改端口后不可访问? 大家好,我是秋意零。今天分享Harbor相关内容,安装部分可完全参考官方文档,写的也比较详细。 安装Harbor 官方文档:https://goharbor.io/docs/2.12.0/install-config/ …...
React中高阶组件HOC详解
高阶组件(Higher-Order Component,简称 HOC)是 React 中的一种设计模式,用于复用组件逻辑。它本质上是一个函数,接收一个组件作为参数,并返回一个新的组件。 1. HOC 的定义 HOC 是一个函数,类…...
网络原理(一)—— http
什么是 http http 是一个应用层协议,全称为“超文本传输协议”。 http 自 1991 年诞生,目前已经发展为最主流使用的一种应用层协议。 HTTP 往往基于传输层的 TCP 协议实现的,例如 http1.0,http1.0,http2.0 http3 是…...
redis学习面试
1、数据类型 string 增删改查 set key valueget keydel kstrlen k 加减 incr articleincrby article 3decr articledecyby article 取v中特定位置数据 getrange name 0 -1getrange name 0 1setrange name 0 x 设置过期时间 setex pro 10 华为 等价于 set pro 华为expire pro…...
前端工程化18-邂逅Promise(待更新)
6、邂逅Promise 6.1、函数对象与实例对象 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>准备_函数对象与实例对象</title> </head> <body><script>/* 函数对象、实例对象…...
Linux(ubuntu)系统的一些基本操作和命令(持续更新)
操作: Ctrl Alt T(打开命令终端) Ctrl Shift (放大命令终端窗口) Ctrl c(退出当前在终端运行的程序) 在命令终端窗口按Tab键可以补全要写的命令 命令: pwd(查…...
IDEA Mac快捷键(自查询使用)
Editing(编辑) Control Space 基本的代码补全(补全任何类、方法、变量)Control Shift Space 智能代码补全(过滤器方法列表和变量的预期类型)Command Shift Enter 自动结束代码,行末自动添…...
认识redis 及 Ubuntu安装redis
文章目录 一. redis概念二. redis应用场景二. redis的特性四. 使用Ubuntu安装redis 一. redis概念 redis 是在内存中存储数据的中间件, 用在分布式系统 redis是客户端服务器结构的程序, 客户端服务器之间通过网络来通信 二. redis应用场景 redis可用作数据库 类似MySQL, 但…...
6.结果处理组件之ResponseHandler
前言 feign发送完请求后, 拿到返回结果, 那么这个返回结果肯定是需要经过框架进一步处理然后再返回到调用者的, 其中ResponseHandler就是用来处理这个返回结果的, 这也是符合正常思维的处理方式, 例如springmvc部分在调用在controller端点前后都会增加扩展点。 从图中可以看得…...
【C#】调用外部应用
以WINFORM应用程序为例,在C#应用程序中调用PYTHON程序(Matplotlib绘制图形程序),将调用PYTHON程序生成的窗口程序嵌入在WINFORM窗口中 窗口程序类 using System; using System.Collections.Generic; using System.Data; using S…...
JavaWeb--JDBC
JDBC(Java Database Connectivity,Java数据库连接)是一种Java API,可以让Java程序连接到数据库并进行数据的操作。它是Java平台的一部分,由Sun Microsystems(现为Oracle Corporation的一部分)开…...
神经网络归一化方法总结
在深度学习中,归一化 是提高训练效率和稳定性的关键技术。以下是几种常见的神经网络归一化方法的总结,包括其核心思想、适用场景及优缺点。 四种归一化 特性Batch NormalizationGroup NormalizationLayer NormalizationInstance Normalization计算维度…...
Debian/Ubuntu 、Fedora 、Arch Linux, 在Linux上,对文本文件进行多线程压缩 xz、pxz、zstd、7z、lrzip
Debian/Ubuntu 、Fedora 、Arch Linux, 在Linux上,对文本文件进行多线程压缩 xz、pxz、zstd、7z、lrzip 前言对比多线程压缩1. 使用 pxz安装 pxz使用 pxz 2. 使用 xz 的 -T 选项使用 xz -T 3. 其他压缩命令1. 使用 gzip2. 使用 bzip23. 使用 xz4. 使用 7…...
前端使用fontfaceobserver库实现字体设置
要使用FontFaceObserver来加载设置项目本地的字体,先确保字体文件位于项目中或者可以从服务端获取到,这样就可以使用FontFaceObserver来检测并加载这些字体 主要有以下几步: npm或者yarn安装引入fontfaceobserver字体资源引入和font-face配置…...
SSM--SpringMVC复习(二)
请求 URL匹配: RequestMapping RequestMapping 负责将请求映射到对应的控制器方法上。 RequestMapping 注解可用于类或方法上。用于类上,表示类中的所有响应请求的方法都以该地址作为父路径。 在整个 Web 项目中,RequestMapping 映射的请求…...
Oracle 11gR2 坏块修复实例一则
背景 前段时间在 Oracle 11gR2 数据库中发现了坏块问题。环境是 64 位 Linux 平台。本文将详细介绍如何使用 DBMS_REPAIR 进行在线修复,当然也可以基于备份和 RMAN 的修复方法这里暂时不做介绍。 发现坏块 1. 从 alert.log 中发现错误 在 alert.log 文件中发现了…...
使用 Docker Compose 来编排部署LMTNR项目
使用 Docker Compose 来部署一个包含 Linux、MySQL、Tomcat、Nginx 和 Redis 的完整项目的例子。假设我们要部署一个简单的 Java Web 应用,并且使用 Nginx 作为反向代理服务器。 项目目录结构 首先需要确保 Docker 和docker-compose已经安装并正在运行。docker --v…...
el-table 根据屏幕大小 动态调整max-height 的值
<template><div><p>窗口高度:{{ windowHeight }} px</p></div> </template><script> export default {data() {return {// 下面的 -250 表示减去一些表单元素高度 这个值需要自己手动调整windowHeight: document.docume…...
el-cascader 使用笔记
1.效果 2.官网 https://element.eleme.cn/#/zh-CN/component/cascader 3.动态加载(官网) <el-cascader :props"props"></el-cascader><script>let id 0;export default {data() {return {props: {lazy: true,lazyLoad (…...
Cookie概念和API
Cookie概念 Cookie在HTTP中它表示服务器送给客户端浏览器的小甜点。其实Cookie就是一个键和一个值构成的,随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。 Cookie是由服务器…...
Linux服务器安装mongodb
因为项目需要做评论功能,领导要求使用mongodb,所以趁机多学习一下。 在服务器我们使用docker安装mongodb 1、拉取mongodb镜像 docker pull mongo (默认拉取最新的镜像) 如果你想指定版本可以这样 docker pull mongo:4.4&#…...
32.4 prometheus存储磁盘数据结构和存储参数
本节重点介绍 : prometheus存储磁盘数据结构介绍 indexchunkshead chunksTombstoneswal prometheus对block进行定时压实 compactprometheus 查看支持的存储参数 prometheus存储示意图 内存和disk之间的纽带 wal WAL目录中包含了多个连续编号的且大小为128M的文件,…...
两个生活中的例子反向理解正/反向代理?
正向代理 场景:你在学校里想访问一个被限制的网站,比如某个社交媒体平台。 操作方式: 你把访问请求发送给学校的代理服务器(正向代理)。代理服务器代表你向互联网发出请求,去访问那个受限的网站。网站的响…...
数据结构-线性表
数据结构-线性表 线性表的任意元素存放地址:Ai a1 L *(i-1) 当i0 则 Aia0L*i 少了一次计算 按照计算方法,当下标为0,可以少执行一次减法,这也是c数组下标取0的原因。 无论n多大都是一个固定时间称之为O(1) 时间复杂度 顺序表的运…...
Python酷库之旅-第三方库Pandas(245)
目录 一、用法精讲 1156、pandas.tseries.offsets.MonthEnd.is_month_start方法 1156-1、语法 1156-2、参数 1156-3、功能 1156-4、返回值 1156-5、说明 1156-6、用法 1156-6-1、数据准备 1156-6-2、代码示例 1156-6-3、结果输出 1157、pandas.tseries.offsets.Mon…...
贵阳思普信息技术有限公司 OA系统 apilogin 接口存在SQL注入漏洞风险
免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...
重学 Android 自定义 View 系列(九):侧边字母选择器
前言 本文来实现一个侧边字母选择器,很常见的一个控件,和上篇文章星星评分用到的关键技术点类似,难度不大,本篇再来温故知新一下。 最终效果如下: 1. 效果分析 每个字母被均匀分布在整个控件区域中;触摸…...
网络原理->DNS协议和NAT协议解
前言 大家好我是小帅,今天我们来了解应用层的DNS协议和NAT技术 个人主页:再无B~U~G 文章目录 1.重要应⽤层协议DNS(Domain Name System)1.1 DNS背景 2. NAT技术3. 总结 1.重要应⽤层协议DNS(Domain Name System) DNS是⼀整套从域…...
亚马逊开发视频人工智能模型,The Information 报道
根据《The Information》周三的报道,电子商务巨头亚马逊(AMZN)已开发出一种新的生成式人工智能(AI),不仅能处理文本,还能处理图片和视频,从而减少对人工智能初创公司Anthropic的依赖…...
【JS】React与Vue的异步编程对比:深度解析与实战案例全面指南
文章目录 前言更多实用工具React与Vue概述ReactVue 异步编程基础回调函数PromiseAsync/Await React中的异步编程使用Axios进行数据请求异步操作与组件生命周期React Hooks中的异步处理 Vue中的异步编程使用Axios进行数据请求异步操作与Vue生命周期Vue Composition API中的异步处…...
CTF-WEB: 2024强网杯青少年专项赛 ezFindShell writeup
打开直接下载www.zip 通过百度网盘分享的文件:ezFindShell.zip 链接:https://pan.baidu.com/s/1JQjOk-qxaOf0s4f3Fgww7w?pwd1111 提取码:1111 --来自百度网盘超级会员V2的分享使用阿里webshell进行检测,找到可利用文件,或者直接全全局搜索…...
docker网络配置
文章目录 前言一、docker网络访问原理二、docker配置多台机器可以相互访问三、高级网络配置四、最佳实践总结前言 在当今的软件开发和运维领域,Docker 已经成为了容器化服务的标准之一。它不仅简化了应用的部署过程,还大大提高了资源利用率。然而,随着Docker应用的深入,网…...
AI生成的一个.netcore 经典后端架构
下面是一个完整的 .NET Core 后端项目示例,使用 Dapper 作为轻量级 ORM 访问 Oracle 数据库,并实现高性能架构。我们将实现学生表、课程表、成绩表和班级表的基本增删改查功能,以及查询某个班级学生成绩的功能,并使用自定义缓存来…...
docker学习的初识
一、docker官方安装地址: 根据官网找对应的环境,相关的安装命令自行官网不在累赘; 查看Docker的版本号:docker -v Docker version 27.3.1, build ce12230查看相应的镜像docker images REPOSITORY TAG IMAGE ID CREATED SIZE docker/wel…...
【Git】Git 完全指南:从入门到精通
Git 完全指南:从入门到精通 Git 是现代软件开发中最重要的版本控制工具之一,它帮助开发者高效地管理项目,支持分布式协作和版本控制。无论是个人项目还是团队开发,Git 都能提供强大的功能来跟踪、管理代码变更,并保障…...
webrtc ios h264 硬编解码
webrtc ios h264 硬编解码 一 ios 系统支持 从ios8开始,苹果公司开放了硬解码和硬编码API(即 VideoToolbox.framework API) 二 主要api 1 主要解码函数 VTDecompressionSessionCreate // 创建解码 session VTDecompressionSession…...
ubuntu20.04更换安装高版本CUDA以及多个CUDA版本管理
Ubuntu 20.04下多版本CUDA的安装与切换 CUDA安装配置环境变量软连接附上参考博客CUDA安装 cuda官方下载地址 因为我需要安装的是11.1版本的,所以这里按着11.1举例安装 安装命令如下: wget https://developer.download.nvidia.com/compute/cuda/11.1.0/local_installers/cu…...
生鲜食品o2o商城系统|Java|SSM|VUE| 前后端分离
【重要1⃣️】前后端源码万字文档部署文档 【重要2⃣️】正版源码有问题包售后 【重要3⃣️】可复制品不支持退换货 【包含内容】 【一】项目提供非常完整的源码注释 【二】相关技术栈文档 【三】源码讲解视频 【其它服务】 【一】可…...
C 语言静态库与动态库的生成和使用
在 YouTube 上找到一个视频 动态链接库静态链接库的生成和使用,它把用 GCC 生成静态库和动态库,以及如何使用他们说的很明白,有条件的可以直接看那个视频。本文就是一个观后的实操和笔记,加添了更多如何查看动态库,静态…...
分布式锁的实现方案有哪些?各自的原理是怎样的?使用场景有哪些?与单体架构中锁区别?存在哪些问题?如何解决?注意事项?
一、分布式锁的实现方案 分布式锁的实现方案主要包括以下几种: 基于数据库的分布式锁: 利用数据库的事务特性来实现锁功能。在数据库中创建一个具有唯一约束的锁表,加锁时插入一行记录,释放锁时删除这行记录。简单易用…...
企业如何落地搭建商业智能BI系统
随着新一代信息化、数字化技术的应用,引发了新一轮的科技革命,现代化社会和数字化的联系越来越紧密,数据也变成继土地、劳动力、资本、技术之后的第五大生产要素,这一切都表明世界已经找准未来方向,前沿科技也与落地并…...
python学习——元组的创建于删除
在 Python 中,元组(tuple)是一种内置的数据类型,用于存储不可变的有序元素集合。以下是关于 Python 元组的一些关键点: 文章目录 定义元组1. 使用圆括号 ()2. 使用 tuple() 函数3. 使用单个元素的元组4. 不使用圆括号…...
智能化图书馆导航系统方案之系统架构与核心功能设计
hello~这里是维小帮,点击文章最下方获取图书馆导航系统解决方案!如有项目需求和技术交流欢迎大家私聊我们~撒花! 针对传统图书馆在图书查找困难、座位紧张、空间导航不便方面的问题,本文深入剖析了基于高精度定位、3D建模、图书搜…...
Linux网络——IO模型和多路转接
通常所谓的IO,其本质就是等待通信和进行通信,即IO 等 拷贝。 那么想要做到高效的IO,就要在单位时间内,减少“等”的比重。 一.五种IO模型 阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方…...
浅谈网络 | 应用层之HTTP协议
目录 HTTP 请求的准备HTTP 请求的构建HTTP 请求的发送过程HTTP 返回的构建HTTP 2.0QUIC 协议HTTP 3.0 在讲完传输层之后,我们接下来进入应用层的内容。应用层的协议种类繁多,那从哪里开始讲起呢?不妨从我们最常用、最熟悉的 HTTP 协议 开始。…...
【CSS】页面滚动到一定位置时,指定区域固定不变
一. 需求 移动端:下滑时,当下滑到一定位置时,指定区域不跟随下滑-【固定跟随区域】一直在顶部效果 (1)未滚动前 (2)滚动后 二. 实现 <template><div class"global-application…...
瀚高创库建表pgsql
1.瀚高下载地址: 下载 (highgo.com)https://www.highgo.com/down_main.html 2.瀚高linux安装 上传deb文件到ubuntu系统中 执行 dpkg -i hgdb-see-4.5.8-fe4791c.x86_64.deb 命令安装数据库 安装完成后,会在/opt 目录下生成安装目录 数据库安装完毕后…...
Vim 高级操作与技巧指南
在上一篇文章中,我们了解了 Vim 的基本操作和模式,掌握了如何进行文件编辑、光标移动、文本操作等基本技能。现在,我们将深入探讨 Vim 的一些高级功能,包括插件管理、脚本编写、定制快捷键等内容,以进一步提高你的工作…...
Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
Vue 3.0 之所以使用 Proxy API 替代 Object.defineProperty,主要是为了提升性能、减少代码复杂性,并解决 Vue 2.x 在响应式处理中的一些局限性。下面我们通过对比这两种方式的工作原理、优缺点,并结合实际项目代码示例来详细讲解。 1. Object.defineProperty 的局限性 在 …...