网络编程(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)测试

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

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

相关文章

Linux网络编程

文章目录 参考资料在前1. 前置知识2. 进程概述2.1 fork()函数2.2 守护进程 3. 浅谈printf()函数与write()函数3.1 printf()函数缓存问题3.2 write()函数思考 4. 网络编程剖析4.1 listen()监听套接字4.2 阻塞/非阻塞IO4.3 同步/异步IO4.4 TCP/IP设计4.4.1 三次握手4.4.2 四次挥手…

机器人的性能指标

1. 负荷能力 负荷能力负荷能力是指机器人在满足其他性能要求的情况下,能够承载的负荷重量。例如,一台机器人的最大负荷能力可能远大于它的额定负荷能力,但是达到最大负荷时,机器人的工作精度可能会降低,可能无法准确地沿着预定的轨迹运动,或者产生额外的偏差。机器人的负荷量与…

【重学 MySQL】四十一、子查询举例与分类

【重学 MySQL】四十一、子查询举例与分类 引入子查询在SELECT子句中引入子查询在FROM子句中引入子查询在WHERE子句中引入子查询注意事项 子查询分类标量子查询列子查询行子查询表子查询 子查询注意事项子查询的位置子查询的返回类型别名的使用性能考虑相关性错误处理逻辑清晰 总…

Flet介绍:平替PyQt的好用跨平台Python UI框架

随着Python在各个领域的广泛应用&#xff0c;特别是在数据科学和Web开发领域&#xff0c;对于一个简单易用且功能强大的用户界面&#xff08;UI&#xff09;开发工具的需求日益增长。传统的Python GUI库如Tkinter、PyQt虽然功能强大&#xff0c;但在易用性和现代感方面略显不足…

数据结构--二叉树的顺序实现(堆实现)

引言 在计算机科学中&#xff0c;二叉树是一种重要的数据结构&#xff0c;广泛应用于各种算法和程序设计中。本文将探讨二叉树的顺序实现&#xff0c;特别是堆的实现方式。 一、树 1.1树的概念与结构 树是⼀种⾮线性的数据结构&#xff0c;它是由 n(n>0) 个有限结点组成…

【HTML5】html5开篇基础(5)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…

vue-live2d看板娘集成方案设计使用教程

文章目录 前言v1.1.x版本&#xff1a;vue集成看板娘&#xff08;暂不使用&#xff0c;在v1.2.x已替换&#xff09;集成看板娘实现看板娘拖拽效果方案资源备份存储 当前最新调研&#xff1a;2024.10.2开源方案1&#xff1a;OhMyLive2D&#xff08;推荐&#xff09;开源方案2&…

【设计模式】软件设计原则——接口隔离迪米特

接口隔离原则引出 接口隔离原则 定义&#xff1a;用多个专门的接口,不使用单一的总接口,客户端不应该依赖它不需要的接口; 一个类对另一个类的依赖,应该建立在最小接口上;如果有一个大接口,里面有很多方法,如果使用一个类实现该接口,所有的类都要实现&#xff0c;导致代码冗余;…

android 全面屏最底部栏沉浸式

Activity的onCreate方法中添加 this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); Android 系统 Bar 沉浸式完美兼容方案自 Android 5.0 版本&#xff0c;Android 带来了沉浸式系统 ba - 掘金 (juejin.cn)https://juejin.cn/post/7075578…

【HTTP(3)】(状态码,https)

【认识状态码】 状态码最重要的目的&#xff0c;就是反馈给浏览器:这次请求是否成功&#xff0c;若失败&#xff0c;则出现失败原因 常见状态码: 200:OK&#xff0c;表示成功 404:Not Found&#xff0c;浏览器访问的资源在服务器上没有找到 403:Forbidden&#xff0c;访问被…

【每天学个新注解】Day 15 Lombok注解简解(十四)—@UtilityClass、@Helper

UtilityClass 生成工具类的注解 将一个类通过注解变成一个工具类&#xff0c;并没有什么用&#xff0c;本来代码中的工具类数量就极为有限&#xff0c;并不能达到减少重复代码的目的 1、如何使用 加在需要委托将其变为工具类的普通类上。 2、代码示例 例&#xff1a; Uti…

基于Java,SpringBoot,Vue智慧校园健康驿站体检论坛请假管理系统

摘要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#xf…

景区+商业,如何实现1+1>2?

景区商业&#xff0c;如何实现11&#xff1e;2&#xff1f; 近两年&#xff0c;随着旅游业的蓬勃发展&#xff0c;旅游热潮持续升温&#xff0c;游客的消费观念也在逐步升级。为了适应这一趋势&#xff0c;各大景区纷纷着手打造具有鲜明特色的文旅项目&#xff0c;希望能够吸引…

C++ | Leetcode C++题解之第457题环形数组是否存在循环

题目&#xff1a; 题解&#xff1a; class Solution { public:bool circularArrayLoop(vector<int>& nums) {int n nums.size();auto next [&](int cur) {return ((cur nums[cur]) % n n) % n; // 保证返回值在 [0,n) 中};for (int i 0; i < n; i) {if …

cherry-markdown开源markdown组件详细使用教程

文章目录 前言开发定位目标调研技术方案前提工作量安排数据库表设计实现步骤1、引入依赖2、实现cherry-markdown的vue组件&#xff08;修改上传接口路径&#xff09;3、支持draw.io组件4、支持展示悬浮目录toc前端使用&#xff1a;编辑状态使用cherry-markdown的vue组件前端使用…

解决npm安装不了element库(目前未解决。。。)

根据您提供的错误信息&#xff0c;安装 element-plus 时出现了一些问题。这些错误主要可以分为两类&#xff1a;权限问题和网络问题。以下是一些解决这些问题的建议&#xff1a; 1. 解决权限问题 您遇到的 EPERM: operation not permitted 错误通常与文件系统权限有关。尝试以…

Stable Diffusion绘画 | 插件-Deforum:动态视频生成(中篇)

本篇文章重点讲解参数最多的 关键帧 模块。 「动画模式」选择「3D」&#xff1a; 下方「运动」Tab 会有一系列参数&#xff1a; 以下4个参数&#xff0c;只有「动画模式」选择「2D」才会生效&#xff0c;可忽略&#xff1a; 运动 平移 X 让镜头左右移动&#xff1a; 大于0&a…

卷积神经网络(CNN)的计算量和参数怎么准确估计?

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 1. 卷积层&#xff08;Convolutional Layer&#xff09; a) 计算量估计&#xff1a; 卷积层的 FLOPs 2 * H_out * W_out * C_in * C_out * K_h * K_w 详细解释&#xff1a; H_out, W_out&#xff…

YOLO11改进|注意力机制篇|引入HAT超分辨率重建模块

目录 一、HAttention注意力机制1.1HAttention注意力介绍1.2HAT核心代码 二、添加HAT注意力机制2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、HAttention注意力机制 1.1HAttention注意力介绍 HAT模型 通过结合卷积特征提取与多尺度注意…

推荐 uniapp 相对好用的海报生成插件

插件地址&#xff1a;自定义canvas样式海报 - DCloud 插件市场 兼容性也是不错的&#xff1a;