网络编程(12)——完善粘包处理操作(id字段)
十二、day12
之前的粘包处理是基于消息头包含的消息体长度进行对应的切包操作,但并不完整。一般来说,消息头仅包含数据域的长度,但是如果要进行逻辑处理,就需要传递一个id字段表示要处理的消息id,当然可以不在包头传id字段,将id序列化到消息体也是可以的,但是我们为了便于处理也便于回调逻辑层对应的函数,最好是将id写入包头。
之前我们设计的消息结构是这样的
而本节需要加上id字段
在此之前,先完整的复习一下基于boost::asio实现的服务器逻辑层结构
1. 服务器架构设计
1)asio底层通信
前面的asio底层通信过程如下图所示
Asio底层的通信过程
1)首先,在应用层调用async_read时,相当于在io_context中注册了一个读事件,表示程序希望在指定socket上进行异步读取操作,并提供一个读回调函数以在读取完成后做相应的处理;
2)io_context用于管理所有异步操作和相应的回调函数,且当async_read被调用时,asio会将该socket、相应的读事件和回调函数注册到系统内部的模型中(数据结构);当io_context启动时,即io_context.run时,asio根据系统使用对应的模型管理这些事件(windows是iocp,linux是epoll);
3)模型进入一个死循环,会监听所有注册的socket,并监测其状态(可读?可写?),如果socket的状态发生变化(事件被触发),模型将该事件放入就绪事件队列中;
4)io_context::run 在轮询就绪事件队列时,会依次调用每个就绪事件的回调函数(已经放在就绪事件队列中),每个回调函数都包含了处理读操作的逻辑,比如读取数据、处理错误等。
2)逻辑层结构
而服务器架构除了上面的内容之外,一般还有一个逻辑层。
一般在解析完对端发送的数据之后,还要对该请求做更进一步地处理,比如根据不同的消息id执行不同的逻辑层函数或不同的操作,比如读数据库、写数据库,还比如游戏中可能需要给玩家叠加不同的buff、增加积分等等,这些都需要交给逻辑层处理,而不仅仅是把消息发给对端。
服务器架构
上图所示的是一个完成的服务器架构,一般需要将逻辑层独立出来,因为如果在解析完对端数据后需要执行一些复杂的操作,比如玩家需要叠加各自buff或者技能,此时可能会耗时1s甚至更多,如果没有独立的逻辑层进行操作,那么系统会一直停留在执行回调函数那一步,造成阻塞,直至操作结束。
而逻辑层是独立的,回调函数只需将数据投递给逻辑队列(回调函数将数据放入队列中之后系统会运行下一步,便不会被阻塞),逻辑系统会自动从队列中取数据并做相应操作,如果需要在执行完操作之后做相应回复,那么逻辑系统会调用写事件并注册写回调给asio网络层,网络层就是asio底层通信的网络层步骤。
以上操作是基于单线程,如果是在多线程的情况下,阻塞的情况会不会好一些?
asio的多线程有两种模式。
1)第一种模式是启动n个线程,每个线程负责一个io_context,每一个io_context负责一部分的socket。比如现在有两个io_context,一个是负责socket的id为奇数的io_context,一个是负责socket的id为偶数的io_context,但同样会造成阻塞的情况。因为不管是多线程还是单线程,只要在线程中有一个io_context中运行,那么它负责的那部分回调函数的处理操作如果比较复杂时,仍会造成阻塞的情况。
2)第二种模式是一个io_context跑在多个线程中,即多个线程共享一个io_context。这种模式下不会造成之前的那种阻塞情况,因为在就绪事件队列中的事件不是一个线程处理了,而是不同的线程共享一个就绪事件队列,不同线程会触发执行不同的回调函数,即使某个回调处理的比较慢,但由于其他事件被不同线程处理了,系统并不需要阻塞等待该回调处理完成之后在执行处理其他回调。
虽然模式二办法会解决系统阻塞、超时的问题,但在现实中需要有一个逻辑层独立于网络层和应用层,这样可以极大地提高网络线程的收发能力,用多线程的方式管理网络层。
2. 完善粘包处理操作
之前的消息结构并不完善,缺少一个消息id,本节进行代码的相应改进。
首先,之前的消息节点被收发共用,只不过收数据用的是第一种构造函数,发数据用的是第二种构造函数。为了减少耦合和歧义,需要重新设计消息节点。
1) 消息节点
重新构建一个MsgNode类,并派生出RecvNode 和SendNode
-
MsgNode 表示消息节点的基类,头部的消息用该结构存储
-
RecvNode 表示接收消息的节点
-
SendNode 表示发送消息的节点
#pragma once
#include <iostream>
#include <string>
#include <boost/asio.hpp>using std::cout;
using std::cin;
using std::endl;class MsgNode
{
public:short _cur_len;short _total_len;char* _msg;MsgNode(short max_len) :_total_len(max_len), _cur_len(0) {_msg = new char[_total_len + 1](); // 加()会将分配内存的每个元素初始化为0,不加不会初始化_msg[_total_len] = '\0';}~MsgNode() {std::cout << "destruct MsgNode" << endl;delete[] _msg;}void Clear() {::memset(_msg, 0, _total_len);_cur_len = 0;}
};
// 构造收节点
class RecvNode :public MsgNode {
private:short _msg_id;
public:RecvNode(short max_len, short msg_id);
};
// 构造发节点
class SendNode :public MsgNode {
private:short _msg_id;
public:SendNode(const char* msg, short max_len, short msg_id);
};
具体实现为:
#include "MsgNode.h"
#include "Const.h"RecvNode::RecvNode(short max_len, short msg_id) :MsgNode(max_len),
_msg_id(msg_id) {}// 发送的数据首地址、数据长度、消息id,发送节点总长度为消息体长度+头节点长度
SendNode::SendNode(const char* msg, short max_len, short msg_id) : MsgNode(max_len + HEAD_TOTAL_LEN)
, _msg_id(msg_id) {// 将消息id转换为网络序,并存储至至发送节点内short msg_id_host = boost::asio::detail::socket_ops::host_to_network_short(msg_id);memcpy(_msg, &msg_id_host, HEAD_ID_LEN);// 将消息体长度转换为网络序,并存储至至发送节点内short max_len_host = boost::asio::detail::socket_ops::host_to_network_short(max_len);memcpy(_msg + HEAD_ID_LEN, &max_len_host, HEAD_DATA_LEN);// 将消息内容存储至发送节点内memcpy(_msg + HEAD_ID_LEN + HEAD_DATA_LEN, msg, max_len);
}
Const.h 定义为
#pragma once
const size_t MAX_LENGTH = 1024 * 2;
const short MAX_RECVQUE = 10000;
const short MAX_SENDQUE = 1000;
const size_t HEAD_TOTAL_LEN = 4;
const size_t HEAD_ID_LEN = 2;
const size_t HEAD_DATA_LEN = 2;
构建SendNode节点时,需要将消息id和消息长度转换为网络序,然后写入数据域_msg ,前2字节存储id,id后为消息长度,偏移4字节后为消息体内容。
2)Session类
Session类和前面差不多,不过需要把收发的逻辑做相应的修改
首先,队列_send_que、消息头结构、消息体结构需要重新声明,分别使用SendNode,RecvNode,MsgNode作为元素类型。
std::queue<std::shared_ptr<SendNode> > _send_que;std::mutex _send_lock;std::shared_ptr<RecvNode> _recv_msg_node; // 收到的消息结构bool _b_head_parse; // 表示是否处理完头部信息std::shared_ptr<MsgNode> _recv_head_node; // 收到的头部结构
Session的构造函数也需要做相应变化,消息头结构的大小更改为4字节,包括id和消息体长度
CSession(boost::asio::io_context& ioc, CServer* server) : _socket(ioc), _server(server), _b_close(false),_b_head_parse(false) {// random_generator是函数对象,加()就是函数,再加一个()就是调用该函数boost::uuids::uuid a_uuid = boost::uuids::random_generator()();_uuid = boost::uuids::to_string(a_uuid);_recv_head_node = std::make_shared<MsgNode>(HEAD_TOTAL_LEN);}
重新定义Send函数,两个Send的重载都需要重新定义
参数列表增加msgid,构造发送节点时需输入三个参数msg, max_length, msgid(发送内容,内容长度,消息id)
void CSession::Send(char* msg, int max_length, short msgid) {bool pending = false; // 发送标志,true时有未完成的发送操作,false为空// 使用lock_guard锁住_send_lock,确保_send_lock(发送队列)访问的线程安全的// 锁的存在确保了多个线程不会同时修改发送队列std::lock_guard<std::mutex> lock(_send_lock);int send_que_size = _send_que.size();if (send_que_size > MAX_SENDQUE) {cout << "session: " << _uuid << " send que fulled, size is " << MAX_SENDQUE << endl;return;}// 判断队列是否有未完成的发送操作if (_send_que.size() > 0) {pending = true;}_send_que.push(std::make_shared<SendNode>(msg, max_length, msgid)); // 将发送消息存储至队列if (pending) { // 如果有未完成的发送,直接返回return;}// 异步发送auto& msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),std::bind(&CSession::haddle_write, this, std::placeholders::_1, shared_from_this()));
} // 当'}'结束后,_send_lock解锁,发送队列解锁void CSession::Send(std::string msg, short msgid) {bool pending = false; // 发送标志,true时有未完成的发送操作,false为空// 使用lock_guard锁住_send_lock,确保_send_lock(发送队列)访问的线程安全的// 锁的存在确保了多个线程不会同时修改发送队列std::lock_guard<std::mutex> lock(_send_lock);int send_que_size = _send_que.size();if (send_que_size > MAX_SENDQUE) {cout << "session: " << _uuid << " send que fulled, size is " << MAX_SENDQUE << endl;return;}// 判断队列是否有未完成的发送操作if (_send_que.size() > 0) {pending = true;}_send_que.push(std::make_shared<SendNode>(msg.c_str(), msg.length(),msgid)); // 将发送消息存储至队列if (pending) { // 如果有未完成的发送,直接返回return;}// 异步发送auto& msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_msg, msgnode->_total_len),std::bind(&CSession::haddle_write, this, std::placeholders::_1, shared_from_this()));
} // 当'}'结束后,_send_lock解锁,发送队列解锁
读回调也需更改,在文章10中haddle_write函数的基础上做修改,可参考该文章
https://zhuanlan.zhihu.com/p/722233898
void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred,std::shared_ptr<CSession> _self_shared) {if (!error) {// 打印缓存区的数据并将该线程暂停2s//PrintRecvData(_data, bytes_transferred);//std::chrono::milliseconds dura(2000);//std::this_thread::sleep_for(dura);// 每触发一次handale_read,它会返回实际读取的字节数bytes_transferred,copy_len表示已处理的长度,每处理一字节,copy_len便加一int copy_len = 0; // 已经处理的字符数while (bytes_transferred > 0) { // 只要读取到数据就对其处理if (!_b_head_parse) { // 判断消息头部是否已处理,_b_head_parse默认为false// 异步读取到的字节数 + 已接收到的头部长度 < 头部总长度if (bytes_transferred + _recv_head_node->_cur_len < HEAD_TOTAL_LEN) { // 收到的数据长度小于头部长度,说明头部还未全部读取// 如果未完全接收消息头,则将接收到的数据复制到头部缓冲区// _recv_head_node->_msg,更新当前头部的接收长度,并继续异步读取剩余数据。memcpy(_recv_head_node->_msg + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);_recv_head_node->_cur_len += bytes_transferred;// 缓冲区清零,无需更新copy_len追踪已处理的字符数,因为之前读取的数据已经全部写入头部节点,下一个// 读入的消息从头开始(copy_len=0)往头节点写::memset(_data, 0, MAX_LENGTH);// 继续读消息_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::headle_read, this,std::placeholders::_1, std::placeholders::_2, _self_shared));return;}// 如果接收到的数据量足够处理消息头部,则计算头部剩余的未接收字节,// 并将其从 _data 缓冲区复制到头部消息缓冲区 _recv_head_node->_msgint head_remain = HEAD_TOTAL_LEN - _recv_head_node->_cur_len; // 头部剩余未复制的长度// 填充头部节点memcpy(_recv_head_node->_msg + _recv_head_node->_cur_len, _data + copy_len, head_remain);copy_len += head_remain; // 更新已处理的data长度bytes_transferred -= head_remain; // 更新剩余未处理的长度short msg_id = 0; // 获取消息idmemcpy(&msg_id, _recv_head_node->_msg, HEAD_ID_LEN);//网络字节序转化为本地字节序msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);cout << "msg_id is " << msg_id << endl;// 判断id是否合法if (msg_id > MAX_LENGTH) {std::cout << "invaild msg_id is " << msg_id << endl;_server->ClearSession(_uuid);return;}short msg_len = 0; // 获取头部数据(消息长度)memcpy(&msg_len, _recv_head_node->_msg + HEAD_ID_LEN, HEAD_DATA_LEN);//网络字节序转化为本地字节序msg_len = boost::asio::detail::socket_ops::network_to_host_short(msg_len);cout << "msg_len is " << msg_len << endl;if (msg_len > MAX_LENGTH) { // 判断头部长度是否非法std::cout << "invalid data length is " << msg_len << endl;_server->ClearSession(_uuid);return;}_recv_msg_node = std::make_shared<RecvNode>(msg_len, msg_id); // 已知数据长度msg_len,构建消息内容载体//消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里if (bytes_transferred < msg_len) {memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);_recv_msg_node->_cur_len += bytes_transferred;// copy_len不用更新,缓冲区会清零,下一个读入data的数据从头开始写入,copy_len也会被初始化为0::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));_b_head_parse = true; //头部处理完成return;}// 接收的长度多于消息内容长度memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, msg_len);_recv_msg_node->_cur_len += msg_len;copy_len += msg_len;bytes_transferred -= msg_len;_recv_msg_node->_msg[_recv_msg_node->_total_len] = '\0';// cout << "receive data is " << _recv_msg_node->_msg << endl;// protobuf序列化//MsgData msgdata;//std::string receive_data;//msgdata.ParseFromString(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len));//std::cout << "receive msg id is " << msgdata.id () << " msg data is " << msgdata.data() << endl;//std::string return_str = "Server has received msg, msg data is " + msgdata.data();//MsgData msgreturn;//msgreturn.set_id(msgdata.id());//msgreturn.set_data(return_str);//msgreturn.SerializeToString(&return_str);//Send(return_str);// jsoncpp序列化Json::Reader reader;Json::Value root;reader.parse(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len), root);std::cout << "recevie msg id is " << root["id"].asInt() << " msg data is "<< root["data"].asString() << endl;root["data"] = "Server has received msg, msg data is " + root["data"].asString();std::string return_str = root.toStyledString();Send(return_str, root["id"].asInt());//Send(_recv_msg_node->_msg, _recv_msg_node->_total_len); // 回传// 清理已处理的头部消息并重置,准备解析下一条消息_b_head_parse = false;_recv_head_node->Clear();// 如果当前数据已经全部处理完,重置缓冲区 _data,并继续异步读取新的数据if (bytes_transferred <= 0) {::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));return;}continue; // 异步读取的消息未处理完,继续填充头节点乃至新的消息节点}//已经处理完头部,处理上次未接受完的消息数据int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;if (bytes_transferred < remain_msg) { //接收的数据仍不足剩余未处理的memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);_recv_msg_node->_cur_len += bytes_transferred;::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));return;}// 接收的数据多于剩余未处理的长度memcpy(_recv_msg_node->_msg + _recv_msg_node->_cur_len, _data + copy_len, remain_msg);_recv_msg_node->_cur_len += remain_msg;bytes_transferred -= remain_msg;copy_len += remain_msg;_recv_msg_node->_msg[_recv_msg_node->_total_len] = '\0';//cout << "receive data is " << _recv_msg_node->_msg << endl;// protobuf序列化//MsgData msgdata;//std::string receive_data;//msgdata.ParseFromString(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len));//std::cout << "receive msg id is " << msgdata.id() << " msg data is " << msgdata.data() << endl;//std::string return_str = "Server has received msg, msg data is " + msgdata.data();//MsgData msgreturn;//msgreturn.set_id(msgdata.id());//msgreturn.set_data(return_str);//msgreturn.SerializeToString(&return_str);//Send(return_str);//jsoncpp序列化Json::Reader reader;Json::Value root;reader.parse(std::string(_recv_msg_node->_msg, _recv_msg_node->_total_len), root);std::cout << "recevie msg id is " << root["id"].asInt() << " msg data is "<< root["data"].asString() << endl;root["data"] = "Server has received msg, msg data is " + root["data"].asString();std::string return_str = root.toStyledString();Send(return_str, root["id"].asInt());//此处可以调用Send发送测试//Send(_recv_msg_node->_msg, _recv_msg_node->_total_len);//继续轮询剩余未处理数据_b_head_parse = false;_recv_head_node->Clear();if (bytes_transferred <= 0) {::memset(_data, 0, MAX_LENGTH);_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),std::bind(&CSession::headle_read, this, std::placeholders::_1, std::placeholders::_2, _self_shared));return;}continue;}}else {std::cout << "handle read failed, error is " << error.what() << endl;Close();_server->ClearSession(_uuid);}
}
HandleRead函数中新增一段读取消息id的代码
首先,当消息头节点_recv_head_node填充完毕后,获取头节点中存储的消息id并转换为本地字节序,并判断id的合法性;然后,解析消息长度,并构建消息体节点_recv_msg_node,读取剩下的消息体内容
short msg_id = 0; // 获取消息idmemcpy(&msg_id, _recv_head_node->_msg, HEAD_ID_LEN);//网络字节序转化为本地字节序msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);cout << "msg_id is " << msg_id << endl;// 判断id是否合法if (msg_id > MAX_LENGTH) {std::cout << "invaild msg_id is " << msg_id << endl;_server->ClearSession(_uuid);return;}
3)客户端
客户端也需额外收发消息id
#include <boost/asio.hpp>
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>using namespace boost::asio::ip;
using std::cout;
using std::endl;
const int MAX_LENGTH = 1024 * 2; // 发送和接收的长度为1024 * 2字节
const int HEAD_LENGTH = 2;
const int HEAD_TOTAL = 4;int main()
{try {boost::asio::io_context ioc; // 创建上下文服务// 127.0.0.1是本机的回路地址,也就是服务器和客户端在一个机器上tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086); // 构造endpointtcp::socket sock(ioc);boost::system::error_code error = boost::asio::error::host_not_found; // 错误:主机未找到sock.connect(remote_ep, error);if (error) {cout << "connect failed, code is " << error.value() << " error msg is " << error.message();return 0;}Json::Value root;root["id"] = 1001;root["data"] = "hello world";std::string request = root.toStyledString();size_t request_length = request.length();char send_data[MAX_LENGTH] = { 0 };int msgid = 1001;int msgid_host = boost::asio::detail::socket_ops::host_to_network_short(msgid);memcpy(send_data, &msgid_host, 2);//转为网络字节序int request_host_length = boost::asio::detail::socket_ops::host_to_network_short(request_length);memcpy(send_data + 2, &request_host_length, 2);memcpy(send_data + 4, request.c_str(), request_length);boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 4));char reply_head[HEAD_TOTAL]; // 首先读取对端发送消息的总长度size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_TOTAL));msgid = 0;memcpy(&msgid, reply_head, HEAD_LENGTH);short msglen = 0; // 消息总长度memcpy(&msglen, reply_head + 2, HEAD_LENGTH); // 将消息总长度赋值给msglen//转为本地字节序msglen = boost::asio::detail::socket_ops::network_to_host_short(msglen);char msg[MAX_LENGTH] = { 0 }; // 构建消息体(不含消息总长度)size_t msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));Json::Reader reader;reader.parse(std::string(msg, msg_length), root);std::cout << "msg id is " << root["id"] << " msg is " << root["data"] << endl;getchar();}catch (std::exception& e) {std::cerr << "Exception: " << e.what() << endl;}return 0;
}
4)测试
相关文章:

网络编程(12)——完善粘包处理操作(id字段)
十二、day12 之前的粘包处理是基于消息头包含的消息体长度进行对应的切包操作,但并不完整。一般来说,消息头仅包含数据域的长度,但是如果要进行逻辑处理,就需要传递一个id字段表示要处理的消息id,当然可以不在包头传i…...
「3.3」虫洞 Wormholes
多组数据不清零——见祖宗 「3.3」虫洞 Wormholes 问题背景 「一本通3.3 练习2」 题目描述 John 在他的农场中闲逛时发现了许多虫洞。虫洞可以看作一条十分奇特的有向边,并可以使你返回到过去的一个时刻(相对你进入虫洞之前)。John 的每…...
网页篡改防御方法
网页篡改防御方法 将服务器安全补丁升级到最新版 操作系统、应用程序、数据库等都需要使用最新的安全补丁,打补丁主要是为防止攻击者利用缓冲溢出和设计缺陷等进行攻击。 封闭未使用但已经开放的网络服务端口及未使用的服务 对于Windows Server 2003操作系统&am…...

Pikachu-Cross-Site Scripting-xss盲打
xss盲打,不是一种漏洞类型,而是一个攻击场景;在前端、或者在当前页面是看不到攻击结果;而是在后端、在别的页面才看到结果。 登陆后台,查看结果;...
JAVA思维提升案例5
抢红包案例: 要求: 一个大V直播时发起了抢红包活动,分别有:9、666、188、520、99999五个红包。 请模拟粉丝来抽奖,按照先来先得,随机抽取,抽完即止,注意:一个红包只能被…...
PostgreSQL的字符集
PostgreSQL的字符集 PostgreSQL 支持多种字符集(character sets),也称为编码(encoding)。字符集决定了数据库存储和处理文本数据的方式。在创建数据库时,可以指定数据库的字符集,或者使用默认的…...

CUDA 参考文章
CUDA:NVCC编译过程和兼容性详解_nvcc把cuda代码转换成什么-CSDN博客https://blog.csdn.net/fb_help/article/details/80462853 1、CUDA:NVCC编译过程和兼容性详解 CUDA:NVCC编译过程和兼容性详解 https://codeyarns.com/2014/03/03/how-to-sp…...
强缓存和协商缓存的区别
强缓存和协商缓存是Web开发中用于优化页面加载性能的两种主要缓存机制,它们之间存在显著的区别。以下是对这两种缓存机制的详细比较: 一、定义与工作原理 强缓存 定义:强缓存是指在浏览器发送请求前,先检查本地缓存中是否存在可用…...
工控系统组成与安全需求分析
目录 工控系统安全威胁与需求分析工业控制系统安全需求分析 工控系统安全威胁与需求分析 工业控制系统是由各种控制组件监测组件数据处理与展示组件共同构成的,对工业生产过程进行控制和监控的业务流程管控系统。 就是现在有很多工厂,它比如说要生产鞋…...

C(十三)for、while、do - while循环的抉择 --- 打怪闯关情景
前言: 继C(十)for循环 --- 黑神话情景之后👉 https://blog.csdn.net/2401_87025655/article/details/142684637 今天,杰哥想用一个打怪闯关的场景让与大家一起初步认识一下for、while、do - while循环的抉择。…...
【Android 源码分析】Activity生命周期之onStop-2
忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。 – 服装…...
SpringCloudStream+RocketMQ多topic
之前写过两篇关于SpringCloudStream文章 spring-cloud-stream版本升级,告别旧注解EnableBinding,拥抱函数式编程_spring-cloud-stream output注解没有了-CSDN博客 SpringCloudStreamRocketMQ事务消息配置_spring-cloud-starter-stream-rocketmq-CSDN博…...
随记 前端框架React的初步认识
区分语言 像js 就是构建的最基本的 React框架和Vue框架,用自己的话来说的话,就是对js进行了一层封装,使使用js更加的方便 但是,React框架和Vue框架又不能这么简单的理解,因为这些框架里还封装了一些其他的东西。 向…...

数据结构 ——— 单链表oj题:链表分割(带哨兵位单向不循环链表实现)
目录 题目要求 手搓简易单链表 代码实现 题目要求 现有一链表的头指针 ListNode* head ,给一定值 x ,编写一段代码将所有小于 x 的节点排在其余节点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头节点 举例说明&a…...

华为 HCIP-Datacom H12-821 题库 (32)
🐣博客最下方微信公众号回复题库,领取题库和教学资源 🐤诚挚欢迎IT交流有兴趣的公众号回复交流群 🦘公众号会持续更新网络小知识😼 1.当一个运行 MSTP 协议的交换设备端口收到一个配置BPDU 时,会与设备保存的全局配…...
[C++][第三方库][brpc]详细讲解
目录 1.介绍2.安装3.类与接口介绍1.日志输出类与接口2.ProtoBuf类与接口3.服务端类与接口4.客户端类与接口 4.使用0.一般流程1.Server2.客户端 -- 同步调用3.客户端 -- 异步调用 1.介绍 brpc是用C编写的工业级RPC框架,常用于搜索、存储、机器学习、广告、推荐等高性…...
Python-Learning
补充不熟悉的python知识 1 **是表示平方 注释是用来阐述代码要做什么,以及是如何做的 先编写行之有效的代码,再决定是对其做进一步改进,还是转而去编写新代码 列表常用是append,但也有pop,这个pop是输出一个值&…...

如何让 Android 的前端页面像 iOS 一样“优雅”?
作者:方英杰(崇之) 最近在调研前端页面适配 Android 端异形屏的方案,调研过程中发现了一些比较有意思的点,本文主要是做一个总结。 一、提出问题 首先,我们需要知道 Android 上的前端适配面临着什么问题。 问题其实很…...

10.3学习
1.循环依赖 循环依赖其实就是循环引用,也就是两个或者两个以上的 Bean 互相持有对方,最终形成闭环。比如A 依赖于B,B又依赖于A Spring中循环依赖场景有: prototype 原型 bean循环依赖 构造器的循环依赖(构造器注入)…...

Shell文本处理(三)
Shell文本处理三:字符串处理 1、字符串截取(切片)2、字符串替换3、字符串删除4、去除空格5、大小写转换6、字符串分割7、去除中文在Shell中,字符串没有单独的数据类型,一切都是变量。但这并不意味着我们不能像在Java、Python等其他编程语言中那样处理字符串 1、字符串截取…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...

基于PHP的连锁酒店管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...

AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...