当前位置: 首页 > news >正文

Linux | 网络通信 | 序列化和反序列化的讲解与实现

文章目录

    • 为什么要序列化?
    • 协议的实现
    • 服务端与客户端代码实现

为什么要序列化?

由于默认对齐数的不同,不同的平台对相同数据进行内存对齐后,可能得到不同的数据。如果直接将这些数据进行网络传输,对方很可能无法正确的获取这些数据,通信也就失去了意义,为保证通信数据的准确性,我们需要设计序列化方式,将数据序列化成字节流,再进行传输,接收方通过对应的反序列化方式对字节流进行解析,从而得到原始且正确的数据。总而言之,序列化与反序列化是为了使通信正确的进行而对数据进行的处理操作

序列化与反序列化只是一种指导思想,关于它的具体实现有很多种,如XML,JSON等等。为了更好理解序列化与反序列化的过程,我在这里自己定制一个协议(这个协议只是模拟实现),该协议是针对简单计算进行设计的

协议的实现

在进行网络传输之前,我们要将数据转换成字符串的形式,以字节流的方式发送信息,这里就涉及到有效载荷和报头的相关知识,报头在有效载荷的前面,也就是字符串的最开始,报头和有效载荷之间用分隔符分割,其表明了有效载荷的长度,比如报头表示有效载荷的长度为n字节,那么分隔符向后n个字节都表示有效载荷。但是报头的长度谁来表示?读取字节流时,一直读取直到遇到分隔符,分隔符之前的数据就是报头。但这里有一个问题?为什么需要使用报头表示有效载荷的长度?有效载荷之间用分隔符分隔不就行了吗?注意一个问题:假如分隔符恰好是有效载荷的一部分数据,直接用分隔符分隔有效载荷的做法不够严谨,需要使用报头来表示有效载荷的长度,并且表示长度的报头一般不会包含分隔符(或者说其中的数据不会和分隔符冲突)

客户端需要和服务端进行通信,通信肯定要发送数据吧,但是客户端上的数据大多是结构化的数据,由于内存对其的问题,不能直接向服务端发送数据,因此,我们需要将结构化的数据转换成特定格式的字符串格式,这个过程分为两步:一是将结构化数据转换成字符串形式的有效载荷,二是为有效载荷添加报头。如此就得到了报头+有效载荷,此时才能将其发送给服务端

我们将:为有效载荷添加/删除报头的操作称为encode/decode,生成有效载荷与解析有效载荷的操作称为serialize/deserialize,也可以叫做序列化和反序列化。综上,客户端发送数据前需要serialize+encode,服务端接收数据后需要decode+deserialize。关于协议的定制,就是这4个接口的具体实现

先说明encode,我们将需要进行encode的字符串str作为参数,然后创建一个新的字符串ret,在ret后追加str的长度(当然了,需要将整数形式的长度转换成字符串形式),接着追加分隔符,最后追加str和分隔符,返回ret

#define CRLF "\n\r"
// 根据参数str,返回编码得到的ret
string encode(const string& str)
{string ret = to_string(str.size());ret += CRLF;ret += str;ret += CRLF;return ret;
}

接着是decode,客户端可能发送一次完成的请求,也可能发送一次不完整的请求,还可能发送多次请求,但我们只需要区分客户发送不完整请求的情况。我们将需要decode的字符串str作为函数参数,再将一个uint32_t类型的数据len以引用的方式作为第二个参数,如果有效载荷不完整,len的值为0,这样请求是否完整就有了判断依据。

接着调用str的find方法,查找分隔符,find将返回分隔符在str中第一次出现的下标位置,如果分隔符没有在str中出现,find返回的数值等于string::npos。所以这里进行判断,如果find返回npos,decode返回空串,用户发送的请求不完整,需要再次进行发送。找到分隔符在str中第一次出现的下标后,我们就可以取出报头,接着解析报头,拿到有效载荷的长度,判断str剩下空间是否足以存储有效载荷,如果剩余空间不足以存储这个有效载荷,那么decode返回空串,用户发送的请求不完整。如果剩余空间足以存储有效载荷,那么我们要将有效载荷返回,修改len为有效载荷的程度,并且删除str中第一个:报头+有效载荷数据。

// 根据参数str,对其解码,并检测str是否有完整的有效载荷,函数返回解码得到的字符串
string decode(string& str, uint32_t& payload_len)
{payload_len = 0;// 查找报头size_t head_pos = str.find(CRLF);if (head_pos == string::npos)return "";// 获取有效载荷的长度string head = str.substr(0, head_pos);int tmp_len = atoi(head.c_str());// 判断有效载荷是否完整if (str.size() - head_pos - 2 * strlen(CRLF) < tmp_len) // 有效载荷不完整return "";// 有效载荷完整,删除报头数据与有效载荷// 这是才修改payload_len参数payload_len = tmp_len; string package = str.substr(head_pos + strlen(CRLF), payload_len); // 删除之前保存有效载荷,因为要返回str.erase(0, payload_len + head_pos + 2 * strlen(CRLF));// 返回有效载荷的字符串return package;
}

关于substr的边界确定,这里就不展开赘述,自己把握就行了。说明一下,服务端每次接收客户端的请求,都是将这些请求追加到一个string后面,所以当我们获取了一次完整的报头+有效载荷,我们就要将其删除,没有获取到完整的报头+有效载荷是,就将下次用户的请求追加到string后面,再次判断是否有完整的请求

encode和decode就讲解完了,接下来是serialize和deserialize。由于客户端发送的数据和服务端发送的数据不同,所以序列化和反序列化要设计两套接口。我们将客户端要发送的数据封装为Request,服务端要响应的数据封装为Response,这两个类有不同的serialize和deserialize方法。由于我是针对简单计算定制的序列化协议,所以客户端的Request就只有两个操作数,一个操作符,就比如1+1,然后服务端对于请求的响应Response,需要保存简单计算的结果与退出码(如果计算遇到错误就会设置退出码)

class Request
{
public:int _x;int _y;char _op;
};class Response
{
public:  // 退出码,0表示正常退出int _exit_code = 0;// 计算结果int _result;
};

关于Request的序列化接口serialize,我们用引用的方式接收用户传入的字符串str,这里默认str是空串,然后将_x,_op,_y对象依次转换成字符串(调用to_string接口),追加到str后,并在两个对象之间插入空格(追加一次对象再追加一次空格),这样就完成了Request的序列化。对于Request的反序列化,deserialize的参数str接收需要反序列化的字符串,使用str的find接口查找空格(因为计算表达式中,不会出现空格,所以我们将空格作为每个对象之间的分隔符),利用空格找到每个对象的位置,调用substr与atoi接口将它们从字符串转换成整形(操作符也是一个整数,这个可以看ASCII码表),为Request对象赋上相应的值。

// 用来发送的计算请求
class Request
{
public:void serialize(string& str){str += to_string(_x);str += SPACE;str+= to_string(_op);str += SPACE;str += to_string(_y);}bool deserialize(string& str){// 获取字符串中的两个空格,以及判断其正确性size_t spaceone_pos = str.find(SPACE);if (spaceone_pos == string::npos)return false;size_t spacetwo_pos = str.rfind(SPACE);if (spacetwo_pos == string::npos)return false;// 根据空格获取操作数与操作符string op = str.substr(spaceone_pos + strlen(SPACE), spacetwo_pos - spaceone_pos - strlen(SPACE));string x = str.substr(0, spaceone_pos);string y = str.substr(spacetwo_pos + strlen(SPACE));// 将得到的字符转换成类成员_x = atoi(x.c_str());_y = atoi(y.c_str());_op = atoi(op.c_str());return true;}int _x;int _y;char _op;
};

Response的序列化serialize也是如此,函数以引用的方式接收一个空串,在空串后追加exitcode和result,每个对象用空格分隔。反序列化deserialize也是接收需要反序列化的字符串str,用find查找空格的位置,根据空格获取其他操作数的位置,也是用substr和atoi接口

class Response
{
public:void serialisze(string& str){str += to_string(_exit_code);str += SPACE;str += to_string(_result);}bool deserialize(string& str){size_t space_pos = str.find(SPACE);if (space_pos == string::npos)return false;_exit_code = atoi(str.substr(0, space_pos).c_str());_result = atoi(str.substr(space_pos + strlen(SPACE)).c_str());return true;}// 退出码,0表示正常退出int _exit_code = 0;// 计算结果int _result = 0;
};

然后就是将用户输入的字符串转换成Request对象的函数,由于用户可能输入"1+1",“1 +1”,“1+ 1”,所以这里需要对这些空格进行特殊处理。首先使用strtok将两个操作数分开,对于左操作数,假设它现在是"1 “,我们从最后开始遍历,遍历到数字停下,在数字后插入’\0’就消除了这些多于空格。对于右操作数,它可能是” 1",我们就从头开始遍历,将指向右操作数的指针不断++,直到该指针指向的数据不再是空格。最后就是调用atoi得到整数形式的操作数

// 将message转换成Request
bool makeRequest(Request& req, char* message)
{char copy[1024] = {0};strcpy(copy, message);// 分割左右操作数char* left = strtok(copy, "+-*/%");char* right = strtok(nullptr, "+-*/%");if (!left || !right)return false;int left_len = strlen(left);int right_len = strlen(right);req._op = message[left_len];// 消除多余空格 int i = 0;for (i = left_len - 1; i >= 0; --i){if (left[i] != ' ')break;}left[i + 1] = '\0';for (i = 0; i < right_len; ++i){if (right[i] != ' ')break;elseright++;}// 将字符串形式的操作数转换成整数req._x = atoi(left);req._y = atoi(right);return true;
}

对于序列化和反序列化除了自己定义方法,也可以使用Json协议,使用别人定制好的成熟的方法,关于Json第三方库的安装,可以使用命令

sudo yum install -y jsoncpp-deve

至于要使用自己的方法还是Json协议,这里可以使用条件编译的方式,将两者方法都写进代码中

// 用来发送的计算请求
class Request
{
public:// 110 + 120void serialize(string& str){#ifdef MYSELFstr += to_string(_x);str += SPACE;str += to_string(_op);str += SPACE;str += to_string(_y);#else// 万能Json对象Json::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;// 定义写对象Json::FastWriter fw;// 将value序列化,保存结果到str中str = fw.write(root);#endif}bool deserialize(string& str){#ifdef MYSELF// 获取字符串中的两个空格,以及判断其正确性size_t spaceone_pos = str.find(SPACE);if (spaceone_pos == string::npos)return false;size_t spacetwo_pos = str.rfind(SPACE);if (spacetwo_pos == string::npos)return false;// 根据空格获取操作数与操作符string op = str.substr(spaceone_pos + strlen(SPACE), spacetwo_pos - spaceone_pos - strlen(SPACE));string x = str.substr(0, spaceone_pos);string y = str.substr(spacetwo_pos + strlen(SPACE));// 将得到的字符转换成类成员_x = atoi(x.c_str());_y = atoi(y.c_str());_op = atoi(op.c_str());return true;#elseJson::Value root;Json::Reader rd;rd.parse(str, root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();return true;#endif}int _x;int _y;char _op;
};// 对请求做出的响应
class Response
{
public:void serialisze(string& str){#ifdef MYSELFstr += to_string(_exit_code);str += SPACE;str += to_string(_result);#elseJson::Value root;root["exitcode"] = _exit_code;root["result"] = _result;Json::FastWriter fw;str = fw.write(root);#endif}bool deserialize(string& str){#ifdef MYSELFsize_t space_pos = str.find(SPACE);if (space_pos == string::npos)return false;_exit_code = atoi(str.substr(0, space_pos).c_str());_result = atoi(str.substr(space_pos + strlen(SPACE)).c_str());return true;#elseJson::Value root;Json::Reader rd;rd.parse(str, root);_exit_code = root["exitcode"].asInt();_result = root["result"].asInt();return true;#endif}// 退出码,0表示正常退出int _exit_code = 0;// 计算结果int _result = 0;
};

关于Json接口的使用:Json序列化,形成的字符串是kv格式的,比如说1+1序列化后就是

{“op”:43,“x”:1,“y”:1}

数据前面的字符串就是为反序列化建立的索引,先说序列化:Value是Json中的一个万能对象,用它可以序列化不同格式的数据,创建Value对象root,root[“x”, _x]就是在root中插入了一个键值对,把所有的数插入到root后,创建FastWrite对象,以root为参数调用其write方法,用string类型对象str接收write的返回值,str中保存的就是{“op”:43,“x”:1,“y”:1}这样的字符串,是将数据序列化后的结果。

至于Json的反序列化:我们创建Reader类型对象rd与Value类型对象root,调用rd的parse方法,将root和序列化后的字符串str作为parse的参数,parse方法会将反序列化后的结果写入root,此时我们就能根据当时创建root对象时,为数据建立的索引来还原数据,比如root[“x”].asInt(),将root中以"x"为key的value值以int的格式返回,这样我们就能将数据还原到我们的结构体中

关于条件编译,我们可以在makefile文件中,以命令行的方式,在编译源文件时创建宏,具体是-D 宏的名字
在这里插入图片描述

服务端与客户端代码实现

对于具体的服务端与客户端通信细节,可以看我的这篇文章,这里不再赘述,只说明大概的思路。

客户端与服务端建立链接后,客户端从键盘读取用户的输入,由于输入的是字符串,并且格式可能不规范,所以客户端需要对用户输入的字符串进行处理,将其转换成Request对象req,接着调用req的serialize方法,将计算表达式序列化,然后是encode,为有效载荷添加报头,最后将这样的数据发送给服务端。注意,不是发送完就结束的了,用户需要得到服务端的响应,得到一个计算结果,所以客户端需要调用read方法,读取服务端的响应,所以这里需要将得到的响应decode,得到有效载荷,接着deserialize,反序列化,将数据填充到Response对象res中,至此,一次客户端与服务端的通信完成。

对于服务端,与客户端连接后,需要接收来自客户端的请求,得到请求后,decode+deserialize,得到一个Request对象req,服务端是要提供服务的,所以这里需要服务端调用一个计算函数,将req的计算表达式计算出一个具体值,并且将结果和退出码保存到Response对象res中。然后对res序列化+encode,将这样的字符串返回给客户端,一次通信中服务端的工作才算完成

#include "util.hpp"
#include "protocol.hpp"
void usage(const char *filename)
{std::cout << "usage:\n\t"<< filename << "IP port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3){usage(argv[0]);exit(USAG_ERRO);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 服务器套接字的填充struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 套接字的创建int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket: fail" << std::endl;exit(SOCK_FAIL);}// 与服务器的连接if (connect(sockfd, (const struct sockaddr*)&server, sizeof(server)) < 0){std::cerr << "connect: fail" << std::endl;exit(CONN_FAIL);}std::cout << "connect done" << std::endl;while (true){std::cout << "请输入计算表达式#";char message[1024] = {0};std::cin.getline(message, sizeof(message));if (strcasecmp("quit", message) == 0){// 注意不要退出,让客户端向服务器发送quit,服务器接收quit将关闭服务ssize_t w_ret = write(sockfd, message, sizeof(message));break;}// 创建请求对象Request req;// 用用户的输入填充req对象makeRequest(req, message);// 序列化,得到可以发送的字符流string package = "";req.serialize(package);// 对字节流编码package = encode(package);ssize_t w_ret = write(sockfd, package.c_str(), package.size());// 发送package失败if (w_ret <= 0){std::cerr << "write: fail" << std::endl;break;}// 发送package成功,接收服务端的响应char re_tmp[1024] = {0};ssize_t r_ret = read(sockfd, re_tmp, sizeof(re_tmp));// 接收失败if (r_ret <= 0){std::cerr << "read: fail" << std::endl;break;}// 接收成功re_tmp[r_ret] = '\0';string re_package = re_tmp;// 解码packageuint32_t payload_len = 0;re_package = decode(re_package, payload_len);// 创建Response对象 Response res;// 将接收的有效载荷反序列化到res中res.deserialize(re_package);// for testcout << "result:" << res._result << ", exit_code:" << res._exit_code << endl;}return 0;
}
#include "util.hpp"
#include "task.hpp"
#include "threadpool.hpp"
#include <signal.h>
#include <sys/wait.h>
#include "protocol.hpp"Response calculate(const Request& req)
{Response res;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._exit_code = -1; // -1表示除0错误elseres._result = req._x / req._y;break;case '%':if (req._y == 0)res._exit_code = -2; // -2表示模0错误elseres._result = req._x % req._y;break;default:res._exit_code = -3; // -3表示操作符错误break;}return res;
}void netCal(int sockfd)
{while (true){// 获取客户端的请求char tmp_buf[1024] = {0};// total_packag用来保存客户端的输入,就算输入信息不完整string total_package = "";// 读取客户端输入的信息ssize_t r_ret = read(sockfd, tmp_buf, sizeof(tmp_buf));   // 读取失败if (r_ret < 0){cout << "read fail" << endl;break;}// 客户端退出else if (r_ret == 0){cout << "client quit" << endl;break;}// 读取成功 else{tmp_buf[r_ret] = '\0';total_package += tmp_buf;Response res;Request req;uint32_t payload_len = 0;// 解码客户发送的信息string cli_package = decode(total_package, payload_len);// 没有获取完整的有效载荷if (payload_len == 0) continue;// 反序列化有效载荷,得到数据req.deserialize(cli_package);// for testcout << "x:" << req._x << ' ' << "op:" << req._op << ' ' << "y:" << req._y << endl;// 得到响应请求的对象res = calculate(req);// 序列化响应对象string package = "";res.serialisze(package);// 对响应进行编码package = encode(package);// 向客户端发送响应write(sockfd, package.c_str(), package.size());// for testcout << "完成一次计算" << endl;}}
}class tcpServer
{
public:tcpServer(uint16_t port, std::string ip = "") : _ip(ip), _port(port) {}~tcpServer() {}void init(){// 创建套接字文件_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd < 0){std::cerr << "socket: fail" << std::endl;exit(SOCK_FAIL);}// 填充套接字信息struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);_ip.empty() ? local.sin_addr.s_addr = INADDR_ANY : inet_aton(_ip.c_str(), &local.sin_addr);// 将信息绑定到套接字文件中if (bind(_listen_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){std::cerr << "bind: fail" << std::endl;exit(BIND_FAIL);}// 至此,套接字创建完成,所有的步骤与udp通信一样// 使套接字进入监听状态if (listen(_listen_sockfd, 5) < 0){std::cerr << "listen: fail" << std::endl;exit(LSTE_FAIL);}// 套接字初始化完成std::cout << "listen done" << std::endl;// 获取线程池的单例_tp = threadpool<Task>::get_instance();std::cout << "threadpool ready" << endl;}void loop(){// 先启动线程池_tp->start();// signal(SIGCHLD, SIG_IGN); // 设置SIGCHLD信号为忽略,这样子进程就会自动释放资源// 创建保存套接字信息的结构体struct sockaddr_in peer;socklen_t peer_len = sizeof(peer);// 接受监听队列中的套接字请求while (1){int server_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &peer_len);if (server_sockfd < 0){std::cerr << "accept: fail" << std::endl;continue;}std::cout << "accept done" << std::endl;// 提取请求方的套接字信息uint16_t peer_port = ntohs(peer.sin_port);std::string peer_ip = inet_ntoa(peer.sin_addr);// 打印请求方的套接字信息std::cout << "accept: " << peer_ip << " [" << peer_port << "]" << std::endl;// 使用线程池技术提供服务Task t(netCal, server_sockfd);_tp->push(t);}}private:std::string _ip;uint16_t _port;int _listen_sockfd;threadpool<Task>* _tp; // 线程池的引入
};int main()
{tcpServer server(8081);server.init();server.loop();return 0;
}

测试结果在这里插入图片描述
若读取需要进行测试,关于服务端与客户端通信的其他文件,我放在了我的gitee中,需要自取

相关文章:

Linux | 网络通信 | 序列化和反序列化的讲解与实现

文章目录为什么要序列化&#xff1f;协议的实现服务端与客户端代码实现为什么要序列化&#xff1f; 由于默认对齐数的不同&#xff0c;不同的平台对相同数据进行内存对齐后&#xff0c;可能得到不同的数据。如果直接将这些数据进行网络传输&#xff0c;对方很可能无法正确的获…...

C#的委托原理刨析and事件原理刨析和两者的比较

什么是委托委托是一种引用类型&#xff0c;表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时&#xff0c;你可以将其实例与任何具有兼容参数和返回类型的方法进行绑定。 你可以通过委托实例调用方法。简单的理解&#xff0c;委托是方法的抽象类&#xff0c;它定…...

Redis学习【8】之Redis RDB持久化

文章目录Redis 持久化1 持久化基本原理2 RDB(Redis DataBase) 持久化2.1 持久化的执行2.2 手动 save 命令2.3 手动 bgsave 命令2.4 自动条件触发2.5 查看持久化时间3 RDB 优化配置3.1 save3.2 stop-write-on-bgsave-error3.3 rdbcompression3.4 rdbchecksum3.5 sanitize-dump-p…...

SpringSecurity认证

文章目录登陆校验流程依赖yaml实现建表、工具类、实体类加密器、AuthenticationManager登录逻辑登录过滤器、配置过滤器登出登陆校验流程 认证 登录&#xff1a; ​ ①自定义登录接口 ​ 调用ProviderManager的方法进行认证 如果认证通过生成token&#xff0c;根据userId把用…...

Socket套接字

概念 Socket套接字&#xff0c;是由系统提供用于网络通信的技术&#xff0c;是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。 分类 Socket套接字主要针对传输层协议划分为如下三类&#xff1a; 流套接字&#xff1a;使用传输层TCP…...

mysql详解之innoDB

索引 Mysql由索引组织&#xff0c;所以索引是mysql多重要概念之一。 聚簇索引 InnoDB和MyISAm一样都是采用B树结构&#xff0c;但不同点在于InnoDB是聚簇索引&#xff08;或聚集索引&#xff09;&#xff0c;将数据行直接放在叶子节点后面。 这里可能存在一个误区&#xff1…...

电信运营商的新尝试:探索非通信领域的发展

近年来&#xff0c;随着电信运营商竞争的日趋激烈和网络建设的成本不断攀升&#xff0c;许多电信运营商已经开始缩减IT投资。然而&#xff0c;在如此情况下&#xff0c;电信运营商仍然需要寻找新的增长机会。那么&#xff0c;在持续缩减IT投资的情况下&#xff0c;电信运营商可…...

第07章_单行函数

第07章_单行函数 讲师&#xff1a;尚硅谷-宋红康&#xff08;江湖人称&#xff1a;康师傅&#xff09; 官网&#xff1a;http://www.atguigu.com 1. 函数的理解 1.1 什么是函数 函数在计算机语言的使用中贯穿始终&#xff0c;函数的作用是什么呢&#xff1f;它可以把我们经…...

Echarts实现多柱状图重叠重叠效果

有两种重叠效果: 1. 多个柱子重叠为一个 2. 多个柱子重叠为两组 第一种,图例: 这个灰色不是阴影哦, 是柱子. 1. 使用详解 (1) series.Z 折线图组件的所有图形的 z 值。控制图形的前后顺序。 z 值小的图形会被 z 值大的图形覆盖。z 相比 zlevel 优先级更低&#xff0c;而且不会…...

PHP学习笔记(一谦四益)

前言 上一篇文章 PHP学习笔记&#xff08;观隅反三&#xff09;分享了数组的知识&#xff0c;这篇文章接着分享和数组相关的算法。 算法效率 算法效率分为两种&#xff1a;第一种是时间效率&#xff0c;第二种是空间效率。时间效率被称为时间复杂度&#xff0c;而空间效率被称…...

Jvm -堆对象的划分

堆对于一个jvm进程来说是唯一的&#xff0c;一个进程只有一个jvm&#xff0c;但是进程半酣多个线程&#xff0c;多个线程共享一个堆。 也就是说&#xff0c;一个jvm实例只存在一个堆&#xff0c;同时对也是Java内存管理的核心区域。 Java堆区域的大小在jvm启动时就已经被确定…...

2023美赛F题讲解+数据领取

我们给大家准备了F题的数据&#xff0c;免费领取&#xff01;在文末 国内生产总值(GDP)可以说是一个国家经济健康状况最著名和最常用的指标之--。它通常用于确定一个国家的购买力和获得贷款的机会,为各国提出提高GDP的政策和项目提供动力。GDP“衡量一个国家在给定时间段内生产…...

【博客625】keepalived开启garp refresh的重要性

keepalived开启garp refresh的重要性 1、场景 1-1、对keepavlied master机器热迁移后出现vip不通&#xff0c;过后恢复 原因&#xff1a;机器迁移后网关那边的arp表没有刷新&#xff0c;流量还是转发到老的端口&#xff0c;但是机器已经迁移到别的端口了&#xff0c;于是网络…...

nginx防护规则,拦截非法字符,防止SQL注入、防XSS,nginx过滤url访问,屏蔽垃圾蜘蛛,WordPress安全代码篇

nginx防护规则,拦截非法字符,防止SQL注入、防XSS,nginx过滤url访问,屏蔽垃圾蜘蛛,WordPress安全代码篇 精心强化,小白一键复制 资源宝分享:www.httple.net 宝塔为例:/www/server/panel/vhost/nginx/你的网站域名.conf,复制代码点击保存 修改www.xx.net你自己域名incl…...

【计算机网络】网络层

文章目录网络层概述网络层提供的两种服务IPv4地址IPv4地址概述分类编址的IPv4地址划分子网的IPv4地址无分类编址的IPv4地址IPv4地址的应用规划IP数据报的发送和转发过程静态路由配置及其可能产生的路由环路问题路由选择路由选择协议概述路由信息协议RIP的基本工作原理开放最短路…...

产品经理知识体系:1.什么是互联网思维?

互联网思维 思考 笔记 用户思维 是要注重用户体验&#xff0c;产品带给用户的价值是什么&#xff0c;是能帮助用户获取想要的商品、解决生活中的问题、获取想要的信息&#xff0c;还是产品能通过兜售参与感、满足感等来满足用户的心理需求。 贯穿产品的整个生命周期过程。 简…...

【数据结构】单链表的接口实现(附图解和源码)

单链表的接口实现&#xff08;附图解和源码&#xff09; 文章目录单链表的接口实现&#xff08;附图解和源码&#xff09;前言一、定义结构体二、接口实现&#xff08;附图解源码&#xff09;1.开辟新空间2.头插数据3.头删数据4.打印整个单链表5.尾删数据6.查找单链表中的数据7…...

TikTok话题量超30亿,这款承载美好记忆的剪贴簿引发讨论

回忆风剪贴簿在TikTok引起关注小超在浏览超店有数后台时发现&#xff0c;有一款平平无奇的剪贴簿的种草视频爆火&#xff0c;在24h内收获了9.9K点赞&#xff0c;播放量更是突破了100W&#xff0c;直接冲到了【种草视频飙升榜】第六名的位置&#xff0c;并且这个数字目前仍在继续…...

了解Dubbo

1.注册中心挂了&#xff0c;消费者还能不能调用生产者&#xff1f; 注册中心挂了&#xff0c; 消费者依然可以调用生产者。生产者和消费者都会在本地缓存注册中心的服务列表&#xff0c;当注册中心宕机时&#xff0c;消费者会读取本地的缓存数据&#xff0c;直接访问生产者&am…...

2023年前端面试知识点总结(JavaScript篇)

近期整理了一下高频的前端面试题&#xff0c;分享给大家一起来学习。如有问题&#xff0c;欢迎指正&#xff01; 1. JavaScript有哪些数据类型 总共有8种数据类型&#xff0c;分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt Null 代表的含义是空对象…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

条件运算符

C中的三目运算符&#xff08;也称条件运算符&#xff0c;英文&#xff1a;ternary operator&#xff09;是一种简洁的条件选择语句&#xff0c;语法如下&#xff1a; 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true&#xff0c;则整个表达式的结果为“表达式1”…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...

Java面试专项一-准备篇

一、企业简历筛选规则 一般企业的简历筛选流程&#xff1a;首先由HR先筛选一部分简历后&#xff0c;在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如&#xff1a;Boss直聘&#xff08;招聘方平台&#xff09; 直接按照条件进行筛选 例如&#xff1a…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

Reasoning over Uncertain Text by Generative Large Language Models

https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...