【计网】自定义序列化反序列化(二) —— 实现网络版计算器【上】
🌎 实现网络版计算器【上】
文章目录:
实现网络版计算器【上】
自定义协议
制定自定义协议
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反序列化 自定义协议序列化反序列化 …...

数据结构2:顺序表
目录 1.线性表 2.顺序表 2.1概念及结构 2.2接口实现 1.线性表 线性表是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 线性表在逻辑上是线性结构,也就说…...
python学习——元组
在 Python 中,元组(tuple)是一种内置的数据类型,用于存储不可变的有序元素集合。以下是关于 Python 元组的一些关键点: 文章目录 定义元组1. 使用圆括号 ()2. 使用 tuple() 函数3. 使用单个元素的元组4. 不使用圆括号…...

apache实现绑定多个虚拟主机访问服务
1个网卡绑定多个ip的命令 ip address add 192.168.45.140/24 dev ens33 ip address add 192.168.45.141/24 dev ens33 在linux服务器上,添加多个站点资料,递归创建三个文件目录 分别在三个文件夹下,建立测试页面 修改apache的配置文件http.…...

无需插件,如何以二维码网址直抵3D互动新世界?
随着Web技术的飞速发展,一个无需额外插件,仅凭二维码或网址即可直接访问的三维互动时代已经悄然来临。这一变革,得益于WebGL技术与先进web3D引擎的完美融合,它们共同构建了51建模网这样一个既便捷又高效的在线三维互动平台&#x…...

系统思考—感恩自己
生命中,真正值得我们铭记与感恩的,不是路途上的苦楚与风雨,而是那个在风雨中依然清醒、勇敢前行的自己,和那些一路同行、相互扶持的伙伴们。 感恩自己,感恩每一个与我们携手并进的人,也期待更多志同道合的…...

Java多线程详解①①(全程干货!!!) 实现简单的线程池 || 定时器 || 简单实现定时器 || 时间轮实现定时器
这里是Themberfue 上一节讲了 线程池 线程池中的拒绝策略 等相关内容 实现简单的线程池 一个线程池最核心的方法就是 submit,通过 submit 提交 Runnable 任务来通知线程池来执行 Runnable 任务 我们简单实现一个特定线程数量的线程池,这些线程应该在…...

DAMODEL丹摩|部署FLUX.1+ComfyUI实战教程
本文仅做测评体验,非广告。 文章目录 1. FLUX.1简介2. 实战2. 1 创建资源2. 1 ComfyUI的部署操作2. 3 部署FLUX.1 3. 测试5. 释放资源4. 结语 1. FLUX.1简介 FLUX.1是由黑森林实验室(Black Forest Labs)开发的开源AI图像生成模型。它拥有12…...

请求(request)
目录 前言 request概述 request的使用 获取前端传递的数据 实例 请求转发 特点 语法 实例 实例1 实例2 【关联实例1】 域对象 组成 作用范围: 生命周期: 使用场景: 使用步骤 存储数据对象 获得数据对象 移除域中的键值…...

关于VNC连接时自动断联的问题
在服务器端打开VNC Server的选项设置对话框,点左边的“Expert”(专家),然后找到“IdleTimeout”,将数值设置为0,点OK关闭对话框。搞定。 注意,服务端有两个vnc服务,这俩都要设置ide timeout为0才行 附件是v…...
C语言strtok()函数用法详解!
strtok 是 C 标准库中的字符串分割函数,用于将一个字符串拆分成多个部分(token),以某些字符(称为分隔符)为界限。 函数原型 char *strtok(char *str, const char *delim);参数: str:…...
【docker 拉取镜像超时问题】
问题描述 在centosStream8上安装docker,使用命令sudo docker run hello-world 后出现以下错误: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Ti…...

模拟手机办卡项目(移动大厅)--结合面向对象、JDBC、MYSQL、dao层模式,使用JAVA控制台实现
目录 1. 项目需求 2. 项目使用的技术 3.项目需求分析 3.1 实体类和接口 4.项目结构 5.业务实现 5.1 登录 5.1.1 实现步骤 5.1.2 原生代码问题 编辑 5.1.3 解决方法 1.说明: 2. ResultSetHandler结果集处理 5.1.4 代码 5.1.5 实现后的效果图 登录成功…...
机器学习—大语言模型:推动AI新时代的引擎
云边有个稻草人-CSDN博客 目录 引言 一、大语言模型的基本原理 1. 什么是大语言模型? 2. Transformer 架构 3. 模型训练 二、大语言模型的应用场景 1. 文本生成 2. 问答系统 3. 编码助手 4. 多语言翻译 三、大语言模型的最新进展 1. GPT-4 2. 开源模型 …...

C++:探索哈希表秘密之哈希桶实现哈希
文章目录 前言一、链地址法概念二、哈希表扩容三、哈希桶插入逻辑四、析构函数五、删除逻辑六、查找七、链地址法代码实现总结 前言 前面我们用开放定址法代码实现了哈希表: C:揭秘哈希:提升查找效率的终极技巧_1 对于开放定址法来说&#…...

具身智能高校实训解决方案——从AI大模型+机器人到通用具身智能
一、 行业背景 在具身智能的发展历程中,AI 大模型的出现成为了关键的推动力量。这些大模型具有海量的参数和强大的语言理解、知识表示能力,能够为机器人的行为决策提供更丰富的信息和更智能的指导。然而,单纯的大模型在面对复杂多变的现实…...

【消息序列】详解(8):探秘物联网中设备广播服务
目录 一、概述 1.1. 定义与特点 1.2. 工作原理 1.3. 应用场景 1.4. 技术优势 二、截断寻呼(Truncated Page)流程 2.1. 截断寻呼的流程 2.2. 示例代码 2.3. 注意事项 三、无连接外围广播过程 3.1. 设备 A 启动无连接外围设备广播 3.2. 示例代…...
【RL Base】强化学习核心算法:深度Q网络(DQN)算法
📢本篇文章是博主强化学习(RL)领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对相关等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅…...
深入浅出 Python 网络爬虫:从零开始构建你的数据采集工具
在大数据时代,网络爬虫作为一种数据采集技术,已经成为开发者和数据分析师不可或缺的工具。Python 凭借其强大的生态和简单易用的语言特点,在爬虫领域大放异彩。本文将带你从零开始,逐步构建一个 Python 网络爬虫,解决实…...

美国发布《联邦风险和授权管理计划 (FedRAMP) 路线图 (2024-2025)》
文章目录 前言一、战略目标实施背景2010年12月,《改革联邦信息技术管理的25点实施计划》2011年2月,《联邦云计算战略》2011年12月,《关于“云计算环境中的信息系统安全授权”的首席信息官备忘录》2022年12月,《FedRAMP 授权法案》…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...

ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...