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

Qt项目:基于Qt实现的网络聊天室---TCP服务器和token验证

文章目录

  • TCP服务器设计
    • 客户端TCP管理者
    • ChatServer
    • AsioIOServicePool
    • Session层
    • LogicSystem
    • 总结
  • token验证模块
    • 完善proto
    • StatusServer验证token
    • 客户端处理登陆回包
    • 用户管理
    • 登陆界面

本篇完成的模块是TCP服务器的设计和token验证

TCP服务器设计

客户端TCP管理者

因为聊天服务要维持一个长链接,方便服务器和客户端双向通信,那么就需要一个TCPMgr来管理TCP连接

而实际开发中网络模块一般以单例模式使用,那我们就基于单例基类和可被分享类创建一个自定义的TcpMgr类,在QT工程中新建TcpMgr类,会生成头文件和源文件,头文件修改如下

#ifndef TCPMGR_H
#define TCPMGR_H
#include <QTcpSocket>
#include "singleton.h"
#include "global.h"
class TcpMgr:public QObject, public Singleton<TcpMgr>,public std::enable_shared_from_this<TcpMgr>
{Q_OBJECT
public:TcpMgr();
private:QTcpSocket _socket;QString _host;uint16_t _port;QByteArray _buffer;bool _b_recv_pending;quint16 _message_id;quint16 _message_len;
public slots:void slot_tcp_connect(ServerInfo);void slot_send_data(ReqId reqId, QString data);
signals:void sig_con_success(bool bsuccess);void sig_send_data(ReqId reqId, QString data);
};#endif // TCPMGR_H

接下来我们在构造函数中连接网络请求的各种信号

TcpMgr::TcpMgr():_host(""),_port(0),_b_recv_pending(false),_message_id(0),_message_len(0)
{QObject::connect(&_socket, &QTcpSocket::connected, [&]() {qDebug() << "Connected to server!";// 连接建立后发送消息emit sig_con_success(true);});QObject::connect(&_socket, &QTcpSocket::readyRead, [&]() {// 当有数据可读时,读取所有数据// 读取所有数据并追加到缓冲区_buffer.append(_socket.readAll());QDataStream stream(&_buffer, QIODevice::ReadOnly);stream.setVersion(QDataStream::Qt_5_0);forever {//先解析头部if(!_b_recv_pending){// 检查缓冲区中的数据是否足够解析出一个消息头(消息ID + 消息长度)if (_buffer.size() < static_cast<int>(sizeof(quint16) * 2)) {return; // 数据不够,等待更多数据}// 预读取消息ID和消息长度,但不从缓冲区中移除stream >> _message_id >> _message_len;//将buffer 中的前四个字节移除_buffer = _buffer.mid(sizeof(quint16) * 2);// 输出读取的数据qDebug() << "Message ID:" << _message_id << ", Length:" << _message_len;}//buffer剩余长读是否满足消息体长度,不满足则退出继续等待接受if(_buffer.size() < _message_len){_b_recv_pending = true;return;}_b_recv_pending = false;// 读取消息体QByteArray messageBody = _buffer.mid(0, _message_len);qDebug() << "receive body msg is " << messageBody ;_buffer = _buffer.mid(_message_len);}});// 处理错误(适用于Qt 5.15之前的版本)QObject::connect(&_socket, static_cast<void (QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),[&](QTcpSocket::SocketError socketError) {qDebug() << "Error:" << _socket.errorString() ;switch (socketError) {case QTcpSocket::ConnectionRefusedError:qDebug() << "Connection Refused!";emit sig_con_success(false);break;case QTcpSocket::RemoteHostClosedError:qDebug() << "Remote Host Closed Connection!";break;case QTcpSocket::HostNotFoundError:qDebug() << "Host Not Found!";emit sig_con_success(false);break;case QTcpSocket::SocketTimeoutError:qDebug() << "Connection Timeout!";emit sig_con_success(false);break;case QTcpSocket::NetworkError:qDebug() << "Network Error!";break;default:qDebug() << "Other Error!";break;}});// 处理连接断开QObject::connect(&_socket, &QTcpSocket::disconnected, [&]() {qDebug() << "Disconnected from server.";});QObject::connect(this, &TcpMgr::sig_send_data, this, &TcpMgr::slot_send_data);
}

连接对端服务器

void TcpMgr::slot_tcp_connect(ServerInfo si)
{qDebug()<< "receive tcp connect signal";// 尝试连接到服务器qDebug() << "Connecting to server...";_host = si.Host;_port = static_cast<uint16_t>(si.Port.toUInt());_socket.connectToHost(si.Host, _port);
}

因为客户端发送数据可能在任何线程,为了保证线程安全,我们在要发送数据时发送TcpMgr的sig_send_data信号,然后实现接受这个信号的槽函数

void TcpMgr::slot_send_data(ReqId reqId, QString data)
{uint16_t id = reqId;// 将字符串转换为UTF-8编码的字节数组QByteArray dataBytes = data.toUtf8();// 计算长度(使用网络字节序转换)quint16 len = static_cast<quint16>(data.size());// 创建一个QByteArray用于存储要发送的所有数据QByteArray block;QDataStream out(&block, QIODevice::WriteOnly);// 设置数据流使用网络字节序out.setByteOrder(QDataStream::BigEndian);// 写入ID和长度out << id << len;// 添加字符串数据block.append(data);// 发送数据_socket.write(block);
}

然后修改LoginDialog中的initHandlers中的收到服务器登陆回复后的逻辑,这里发送信号准备发起长链接到聊天服务器

void LoginDialog::initHttpHandlers()
{//注册获取登录回包逻辑_handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){int error = jsonObj["error"].toInt();if(error != ErrorCodes::SUCCESS){showTip(tr("参数错误"),false);enableBtn(true);return;}auto user = jsonObj["user"].toString();//发送信号通知tcpMgr发送长链接ServerInfo si;si.Uid = jsonObj["uid"].toInt();si.Host = jsonObj["host"].toString();si.Port = jsonObj["port"].toString();si.Token = jsonObj["token"].toString();_uid = si.Uid;_token = si.Token;qDebug()<< "user is " << user << " uid is " << si.Uid <<" host is "<< si.Host << " Port is " << si.Port << " Token is " << si.Token;emit sig_connect_tcp(si);});
}

在LoginDialog构造函数中连接信号,包括建立tcp连接,以及收到TcpMgr连接成功或者失败的信号处理

//连接tcp连接请求的信号和槽函数connect(this, &LoginDialog::sig_connect_tcp, TcpMgr::GetInstance().get(), &TcpMgr::slot_tcp_connect);
//连接tcp管理者发出的连接成功信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_con_success, this, &LoginDialog::slot_tcp_con_finish);

LoginDialog收到连接结果的槽函数

void LoginDialog::slot_tcp_con_finish(bool bsuccess)
{if(bsuccess){showTip(tr("聊天服务连接成功,正在登录..."),true);QJsonObject jsonObj;jsonObj["uid"] = _uid;jsonObj["token"] = _token;QJsonDocument doc(jsonObj);QString jsonString = doc.toJson(QJsonDocument::Indented);//发送tcp请求给chat serverTcpMgr::GetInstance()->sig_send_data(ReqId::ID_CHAT_LOGIN, jsonString);}else{showTip(tr("网络异常"),false);enableBtn(true);}}

在这个槽函数中我们发送了sig_send_data信号并且通知TcpMgr将数据发送给服务器。

ChatServer

一个TCP服务器必然会有连接的接收,维持,收发数据等逻辑。那我们就要基于asio完成这个服务的搭建。主服务是这个样子的

#include "LogicSystem.h"
#include <csignal>
#include <thread>
#include <mutex>
#include "AsioIOServicePool.h"
#include "CServer.h"
#include "ConfigMgr.h"
using namespace std;
bool bstop = false;
std::condition_variable cond_quit;
std::mutex mutex_quit;int main()
{try {auto &cfg = ConfigMgr::Inst();auto pool = AsioIOServicePool::GetInstance();boost::asio::io_context  io_context;boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);signals.async_wait([&io_context, pool](auto, auto) {io_context.stop();pool->Stop();});auto port_str = cfg["SelfServer"]["Port"];CServer s(io_context, atoi(port_str.c_str()));io_context.run();}catch (std::exception& e) {std::cerr << "Exception: " << e.what() << endl;}}

CServer类的声明

#include <boost/asio.hpp>
#include "CSession.h"
#include <memory.h>
#include <map>
#include <mutex>
using namespace std;
using boost::asio::ip::tcp;
class CServer
{
public:CServer(boost::asio::io_context& io_context, short port);~CServer();void ClearSession(std::string);
private:void HandleAccept(shared_ptr<CSession>, const boost::system::error_code & error);void StartAccept();boost::asio::io_context &_io_context;short _port;tcp::acceptor _acceptor;std::map<std::string, shared_ptr<CSession>> _sessions;std::mutex _mutex;
};

构造函数中监听对方连接

CServer::CServer(boost::asio::io_context& io_context, short port):_io_context(io_context), _port(port),
_acceptor(io_context, tcp::endpoint(tcp::v4(),port))
{cout << "Server start success, listen on port : " << _port << endl;StartAccept();
}

接受连接的函数

void CServer::StartAccept() {auto &io_context = AsioIOServicePool::GetInstance()->GetIOService();shared_ptr<CSession> new_session = make_shared<CSession>(io_context, this);_acceptor.async_accept(new_session->GetSocket(), std::bind(&CServer::HandleAccept, this, new_session, placeholders::_1));
}

AsioIOServicePool

从AsioIOServicePool中返回一个可用的iocontext构造Session,然后将接受的新链接的socket写入这个Session保管

AsioIOServicePool已经在前面讲解很多次了,它的声明如下

#include <vector>
#include <boost/asio.hpp>
#include "Singleton.h"
class AsioIOServicePool:public Singleton<AsioIOServicePool>
{friend Singleton<AsioIOServicePool>;
public:using IOService = boost::asio::io_context;using Work = boost::asio::io_context::work;using WorkPtr = std::unique_ptr<Work>;~AsioIOServicePool();AsioIOServicePool(const AsioIOServicePool&) = delete;AsioIOServicePool& operator=(const AsioIOServicePool&) = delete;// 使用 round-robin 的方式返回一个 io_serviceboost::asio::io_context& GetIOService();void Stop();
private:AsioIOServicePool(std::size_t size = std::thread::hardware_concurrency());std::vector<IOService> _ioServices;std::vector<WorkPtr> _works;std::vector<std::thread> _threads;std::size_t                        _nextIOService;
};

AsioIOServicePool具体实现

#include "AsioIOServicePool.h"
#include <iostream>
using namespace std;
AsioIOServicePool::AsioIOServicePool(std::size_t size):_ioServices(size),
_works(size), _nextIOService(0){for (std::size_t i = 0; i < size; ++i) {_works[i] = std::unique_ptr<Work>(new Work(_ioServices[i]));}//遍历多个ioservice,创建多个线程,每个线程内部启动ioservicefor (std::size_t i = 0; i < _ioServices.size(); ++i) {_threads.emplace_back([this, i]() {_ioServices[i].run();});}
}AsioIOServicePool::~AsioIOServicePool() {std::cout << "AsioIOServicePool destruct" << endl;
}boost::asio::io_context& AsioIOServicePool::GetIOService() {auto& service = _ioServices[_nextIOService++];if (_nextIOService == _ioServices.size()) {_nextIOService = 0;}return service;
}void AsioIOServicePool::Stop(){//因为仅仅执行work.reset并不能让iocontext从run的状态中退出//当iocontext已经绑定了读或写的监听事件后,还需要手动stop该服务for (auto& work : _works) {//把服务先停止work->get_io_context().stop();work.reset();}for (auto& t : _threads) {t.join();}
}

CServer的处理连接逻辑

void CServer::HandleAccept(shared_ptr<CSession> new_session, const boost::system::error_code& error){if (!error) {new_session->Start();lock_guard<mutex> lock(_mutex);_sessions.insert(make_pair(new_session->GetUuid(), new_session));}else {cout << "session accept failed, error is " << error.what() << endl;}StartAccept();
}

Session层

上面的逻辑接受新链接后执行Start函数,新链接接受数据,然后Server继续监听新的连接

void CSession::Start(){AsyncReadHead(HEAD_TOTAL_LEN);
}

先读取头部数据

void CSession::AsyncReadHead(int total_len)
{auto self = shared_from_this();asyncReadFull(HEAD_TOTAL_LEN, [self, this](const boost::system::error_code& ec, std::size_t bytes_transfered) {try {if (ec) {std::cout << "handle read failed, error is " << ec.what() << endl;Close();_server->ClearSession(_uuid);return;}if (bytes_transfered < HEAD_TOTAL_LEN) {std::cout << "read length not match, read [" << bytes_transfered << "] , total ["<< HEAD_TOTAL_LEN << "]" << endl;Close();_server->ClearSession(_uuid);return;}_recv_head_node->Clear();memcpy(_recv_head_node->_data, _data, bytes_transfered);//获取头部MSGID数据short msg_id = 0;memcpy(&msg_id, _recv_head_node->_data, HEAD_ID_LEN);//网络字节序转化为本地字节序msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);std::cout << "msg_id is " << msg_id << endl;//id非法if (msg_id > MAX_LENGTH) {std::cout << "invalid msg_id is " << msg_id << endl;_server->ClearSession(_uuid);return;}short msg_len = 0;memcpy(&msg_len, _recv_head_node->_data + HEAD_ID_LEN, HEAD_DATA_LEN);//网络字节序转化为本地字节序msg_len = boost::asio::detail::socket_ops::network_to_host_short(msg_len);std::cout << "msg_len is " << msg_len << endl;//id非法if (msg_len > MAX_LENGTH) {std::cout << "invalid data length is " << msg_len << endl;_server->ClearSession(_uuid);return;}_recv_msg_node = make_shared<RecvNode>(msg_len, msg_id);AsyncReadBody(msg_len);}catch (std::exception& e) {std::cout << "Exception code is " << e.what() << endl;}});
}

上面的逻辑里调用asyncReadFull读取整个长度,然后解析收到的数据,前两个字节为id,之后两个字节为长度,最后n个长度字节为消息内容

//读取完整长度
void CSession::asyncReadFull(std::size_t maxLength, std::function<void(const boost::system::error_code&, std::size_t)> handler )
{::memset(_data, 0, MAX_LENGTH);asyncReadLen(0, maxLength, handler);
}

读取指定长度

//读取指定字节数
void CSession::asyncReadLen(std::size_t read_len, std::size_t total_len, std::function<void(const boost::system::error_code&, std::size_t)> handler)
{auto self = shared_from_this();_socket.async_read_some(boost::asio::buffer(_data + read_len, total_len-read_len),[read_len, total_len, handler, self](const boost::system::error_code& ec, std::size_t  bytesTransfered) {if (ec) {// 出现错误,调用回调函数handler(ec, read_len + bytesTransfered);return;}if (read_len + bytesTransfered >= total_len) {//长度够了就调用回调函数handler(ec, read_len + bytesTransfered);return;}// 没有错误,且长度不足则继续读取self->asyncReadLen(read_len + bytesTransfered, total_len, handler);});
}

读取头部成功后,其回调函数内部调用了读包体的逻辑

void CSession::AsyncReadBody(int total_len)
{auto self = shared_from_this();asyncReadFull(total_len, [self, this, total_len](const boost::system::error_code& ec, std::size_t bytes_transfered) {try {if (ec) {std::cout << "handle read failed, error is " << ec.what() << endl;Close();_server->ClearSession(_uuid);return;}if (bytes_transfered < total_len) {std::cout << "read length not match, read [" << bytes_transfered << "] , total ["<< total_len<<"]" << endl;Close();_server->ClearSession(_uuid);return;}memcpy(_recv_msg_node->_data , _data , bytes_transfered);_recv_msg_node->_cur_len += bytes_transfered;_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';cout << "receive data is " << _recv_msg_node->_data << endl;//此处将消息投递到逻辑队列中LogicSystem::GetInstance()->PostMsgToQue(make_shared<LogicNode>(shared_from_this(), _recv_msg_node));//继续监听头部接受事件AsyncReadHead(HEAD_TOTAL_LEN);}catch (std::exception& e) {std::cout << "Exception code is " << e.what() << endl;}});
}

读取包体完成后,在回调中继续读包头。以此循环往复直到读完所有数据。如果对方不发送数据,则回调函数就不会触发。不影响程序执行其他工作,因为我们采用的是asio异步的读写操作

当然我们解析完包体后会调用LogicSystem单例将解析好的消息封装为逻辑节点传递给逻辑层进行处理

LogicSystem

我们在逻辑层处理

void LogicSystem::RegisterCallBacks() {_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,placeholders::_1, placeholders::_2, placeholders::_3);
}void LogicSystem::LoginHandler(shared_ptr<CSession> session, const short &msg_id, const string &msg_data) {Json::Reader reader;Json::Value root;reader.parse(msg_data, root);std::cout << "user login uid is  " << root["uid"].asInt() << " user token  is "<< root["token"].asString() << endl;std::string return_str = root.toStyledString();session->Send(return_str, msg_id);
}

并在构造函数中注册这些处理流程

LogicSystem::LogicSystem():_b_stop(false){RegisterCallBacks();_worker_thread = std::thread (&LogicSystem::DealMsg, this);
}

总结

到此,完成了ChatServer收到QT客户端发送过来的长链接请求,并解析读取的数据,将收到的数据通过tcp发送给对端

token验证模块

完善proto

在proto文件里新增登陆验证服务

message LoginReq{int32 uid = 1;string token= 2;
}message LoginRsp {int32 error = 1;int32 uid = 2;string token = 3;
}service StatusService {rpc GetChatServer (GetChatServerReq) returns (GetChatServerRsp) {}rpc Login(LoginReq) returns(LoginRsp);
}

接下来是调用grpc命令生成新的pb文件覆盖原有的,并且也拷贝给StatusServer一份

我们完善登陆逻辑,先去StatusServer验证token是否合理,如果合理再从内存中寻找用户信息,如果没找到则从数据库加载一份

void LogicSystem::LoginHandler(shared_ptr<CSession> session, const short &msg_id, const string &msg_data) {Json::Reader reader;Json::Value root;reader.parse(msg_data, root);auto uid = root["uid"].asInt();std::cout << "user login uid is  " << uid << " user token  is "<< root["token"].asString() << endl;//从状态服务器获取token匹配是否准确auto rsp = StatusGrpcClient::GetInstance()->Login(uid, root["token"].asString());Json::Value  rtvalue;Defer defer([this, &rtvalue, session]() {std::string return_str = rtvalue.toStyledString();session->Send(return_str, MSG_CHAT_LOGIN_RSP);});rtvalue["error"] = rsp.error();if (rsp.error() != ErrorCodes::Success) {return;}//内存中查询用户信息auto find_iter = _users.find(uid);std::shared_ptr<UserInfo> user_info = nullptr;if (find_iter == _users.end()) {//查询数据库user_info = MysqlMgr::GetInstance()->GetUser(uid);if (user_info == nullptr) {rtvalue["error"] = ErrorCodes::UidInvalid;return;}_users[uid] = user_info;}else {user_info = find_iter->second;}rtvalue["uid"] = uid;rtvalue["token"] = rsp.token();rtvalue["name"] = user_info->name;
}

StatusServer验证token

在StatusServer验证token之前,我们需要在StatusServer中的GetServer的服务里将token写入内存

Status StatusServiceImpl::GetChatServer(ServerContext* context, const GetChatServerReq* request, GetChatServerRsp* reply)
{std::string prefix("llfc status server has received :  ");const auto& server = getChatServer();reply->set_host(server.host);reply->set_port(server.port);reply->set_error(ErrorCodes::Success);reply->set_token(generate_unique_string());insertToken(request->uid(), reply->token());return Status::OK;
}

接下来我们实现登陆验证服务

Status StatusServiceImpl::Login(ServerContext* context, const LoginReq* request, LoginRsp* reply)
{auto uid = request->uid();auto token = request->token();std::lock_guard<std::mutex> guard(_token_mtx);auto iter = _tokens.find(uid);if (iter == _tokens.end()) {reply->set_error(ErrorCodes::UidInvalid);return Status::OK;}if (iter->second != token) {reply->set_error(ErrorCodes::TokenInvalid);return Status::OK;}reply->set_error(ErrorCodes::Success);reply->set_uid(uid);reply->set_token(token);return Status::OK;
}

这样当GateServer访问StatusServer的Login服务做验证后,就可以将数据返回给QT前端了

客户端处理登陆回包

QT 的客户端TcpMgr收到请求后要进行对应的逻辑处理。所以我们在TcpMgr的构造函数中调用initHandlers注册消息

void TcpMgr::initHandlers()
{//auto self = shared_from_this();_handlers.insert(ID_CHAT_LOGIN_RSP, [this](ReqId id, int len, QByteArray data){qDebug()<< "handle id is "<< id << " data is " << data;// 将QByteArray转换为QJsonDocumentQJsonDocument jsonDoc = QJsonDocument::fromJson(data);// 检查转换是否成功if(jsonDoc.isNull()){qDebug() << "Failed to create QJsonDocument.";return;}QJsonObject jsonObj = jsonDoc.object();if(!jsonObj.contains("error")){int err = ErrorCodes::ERR_JSON;qDebug() << "Login Failed, err is Json Parse Err" << err ;emit sig_login_failed(err);return;}int err = jsonObj["error"].toInt();if(err != ErrorCodes::SUCCESS){qDebug() << "Login Failed, err is " << err ;emit sig_login_failed(err);return;}UserMgr::GetInstance()->SetUid(jsonObj["uid"].toInt());UserMgr::GetInstance()->SetName(jsonObj["name"].toString());UserMgr::GetInstance()->SetToken(jsonObj["token"].toString());emit sig_swich_chatdlg();});
}

并且增加处理请求

void TcpMgr::handleMsg(ReqId id, int len, QByteArray data)
{auto find_iter =  _handlers.find(id);if(find_iter == _handlers.end()){qDebug()<< "not found id ["<< id << "] to handle";return ;}find_iter.value()(id,len,data);
}

用户管理

为管理用户数据,需要创建一个UserMgr类,统一管理用户数据,我们这么声明

#ifndef USERMGR_H
#define USERMGR_H
#include <QObject>
#include <memory>
#include <singleton.h>class UserMgr:public QObject,public Singleton<UserMgr>,public std::enable_shared_from_this<UserMgr>
{Q_OBJECT
public:friend class Singleton<UserMgr>;~ UserMgr();void SetName(QString name);void SetUid(int uid);void SetToken(QString token);
private:UserMgr();QString _name;QString _token;int _uid;
};#endif // USERMGR_H

简单实现几个功能

#include "usermgr.h"UserMgr::~UserMgr()
{}void UserMgr::SetName(QString name)
{_name = name;
}void UserMgr::SetUid(int uid)
{_uid = uid;
}void UserMgr::SetToken(QString token)
{_token = token;
}UserMgr::UserMgr()
{}

详细和复杂的管理后续不断往这里补充就行了

登陆界面

登陆界面响应TcpMgr返回的登陆请求,在其构造函数中添加

   //连接tcp管理者发出的登陆失败信号connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_login_failed, this, &LoginDialog::slot_login_failed);

并实现槽函数

void LoginDialog::slot_login_failed(int err)
{QString result = QString("登录失败, err is %1").arg(err);showTip(result,false);enableBtn(true);
}

到此完成了登陆的请求和响应,接下来要实现响应登陆成功后跳转到聊天界面。下一篇先实现聊天布局。

相关文章:

Qt项目:基于Qt实现的网络聊天室---TCP服务器和token验证

文章目录 TCP服务器设计客户端TCP管理者ChatServerAsioIOServicePoolSession层LogicSystem总结 token验证模块完善protoStatusServer验证token客户端处理登陆回包用户管理登陆界面 本篇完成的模块是TCP服务器的设计和token验证 TCP服务器设计 客户端TCP管理者 因为聊天服务要…...

深入理解C++构造函数

目录 1.引言 2.默认构造函数 3.自定义构造函数 4.带继承关系类的构造函数 5.带多重继承关系类的构造函数 6.带虚继承关系类的构造函数 7.总结 1.引言 对于学过C的来说&#xff0c;构造函数是非常熟悉不过的了。但是你真正了解它吗&#xff1f;构造函数内部初始化变量的顺…...

J025_斗地主游戏案例开发(简版)

一、需求描述 完成斗地主游戏的案例开发。 业务&#xff1a;总共有54张牌&#xff1b; 点数&#xff1a;3、4、5、6、7、8、9、10、J、Q、K、A、2 花色&#xff1a;黑桃、红桃、方片、梅花 大小王&#xff1a;大王、小王 点数分别要组合4种花色&#xff0c;大小王各一张。…...

路径规划 | 飞蛾扑火算法求解二维栅格路径规划(Matlab)

目录 效果一览基本介绍程序设计参考文献 效果一览 基本介绍 路径规划 | 飞蛾扑火算法求解二维栅格路径规划&#xff08;Matlab&#xff09;。 飞蛾扑火算法&#xff08;Firefly Algorithm&#xff09;是一种基于自然界萤火虫行为的优化算法&#xff0c;在路径规划问题中也可以应…...

优化Cocos Creator 包体体积

优化Cocos Creator 包体体积 引言一、优化图片文件体积&#xff1a;二、优化声音文件体积&#xff1a;三、优化引擎代码体积&#xff1a;四、 优化字体字库文件大小&#xff1a; 引言 优化Cocos Creator项目的包体体积是一个常见且重要的任务&#xff0c;尤其是在移动设备和网…...

TCPDump协议分析工具

TCPDump协议分析工具 TCPDump是一个强大的命令行工具&#xff0c;用于捕获和分析网络数据包。它能够实时监控和记录网络流量&#xff0c;帮助网络管理员和安全专家排查网络问题、分析流量和检测网络攻击。以下是TCPDump的详细介绍&#xff0c;包括其安装、基本使用、过滤规则和…...

土壤分析仪:解密土壤之奥秘的科技先锋

在农业生产和生态保护的道路上&#xff0c;土壤的质量与状况一直是我们关注的焦点。土壤分析仪&#xff0c;作为现代科技在农业和环保领域的杰出代表&#xff0c;以其高效、精准的分析能力&#xff0c;为我们揭示了土壤的奥秘&#xff0c;为农业生产提供了科学指导&#xff0c;…...

计算1的数量

1. 计算1的数量 题目ID&#xff1a;9809必做题100分 最新提交&#xff1a; Accepted 100 分 历史最高&#xff1a; Accepted 100 分 时间限制: 1000ms 空间限制: 524288kB 题目描述 给定一个n*m的二进制矩阵&#xff0c;请你数一数矩阵中完全被0上下左右包围的1的数…...

Linux udp编程

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…...

【开源项目】Rust开发复制文件夹目录结构工具

说明 由于我经常需要在多个大容量的移动硬盘中查找和新增文件&#xff0c;我希望把硬盘的目录结构放到服务器的自建网盘中&#xff0c;因此开发了这个工具&#xff0c;使得在不同硬盘之间的文件管理变得更加便捷 项目地址&#xff1a;https://github.com/VinciYan/folder_clon…...

PostgreSQL的pg_dirtyread工具

PostgreSQL的pg_dirtyread工具 pg_dirtyread 是一个第三方PostgreSQL扩展&#xff0c;它允许用户读取数据库文件中的“脏”数据&#xff0c;即那些被标记为删除或不再可见的数据。这个扩展对于数据恢复和调试非常有用&#xff0c;尤其是在需要恢复被删除或更新前的数据时。 以…...

苹果梦碎:Vision Pro的辉煌与失落,苹果已决定暂停 Vision Pro 后续产品的研发工作

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 苹果Vision Pro&#xff1a;科技巨头的跌宕起伏 在科技的海洋中&#xff0c;苹果公司一直以其创新精神和卓越品质引领潮流。然而&#xff0c;即…...

推荐一款uniapp拖动验证码插件

插件地址&#xff1a;易盾验证码 - DCloud 插件市场 具体使用方式访问插件地址自行获取...

十年期国债收益率

十年期国债收益率是指政府发行的、期限为十年的国债的年化收益率。它被广泛视为一个国家经济健康状况和未来经济前景的重要指标&#xff0c;同时也是金融市场中的一个重要基准利率。 下面我将详细解释十年期国债收益率的相关内容及其意义。 十年期国债收益率的意义 经济健康的…...

使用Go编写的持续下行测速脚本,快速消耗流量且不伤硬盘

GoSpeed 声明 此工具仅用于测试与学习,请勿用于非法用途,如使用此程序请确保所有下载的内容都拥有合法的使用权或分发权&#xff0c;避免侵犯版权、恶意访问 此工具仅用于测试与学习,请勿用于非法用途,如使用此程序请确保所有下载的内容都拥有合法的使用权或分发权&#xff0c…...

保护国外使用代理IP的安全方法

为了保护在国外使用代理IP的安全&#xff0c;用户可以采取以下方法&#xff1a; 1. 选择可信的代理服务器 在选择代理服务器时&#xff0c;用户应该选择那些经过验证和信任的服务器&#xff0c;如知名的VPN服务提供商。这些服务器通常具有更高的安全性和隐私保护措施。 2. 使用…...

18集 学习ESP32的ESP-DL深度学习教程-《MCU嵌入式AI开发笔记》

18集 学习ESP32的ESP-DL深度学习教程-《MCU嵌入式AI开发笔记》 参考文档&#xff1a;https://docs.espressif.com/projects/esp-dl/zh_CN/latest/esp32/tutorials/index.html 使用TVM自动生成模型部署项目 本案例介绍了使用 TVM 部署模型的完整流程。 该项目基于 TVM v0.14…...

jmeter-beanshell学习9-放弃beanshell

写这篇时候道心不稳了&#xff0c;前面写了好几篇benashell元件&#xff0c;突然发现应该放弃。想回去改前面的文章&#xff0c;看了看无从下手&#xff0c;反正已经这样了&#xff0c;我淋了雨&#xff0c;那就希望别人也没有伞吧&#xff0c;哈哈哈哈&#xff0c;放在第九篇送…...

Web 性能入门指南-1.5 创建 Web 性能优化文化的最佳实践

最成功的网站都有什么共同点&#xff1f;那就是他们都有很强的网站性能和可用性文化。以下是一些经过验证的有效技巧和最佳实践&#xff0c;可帮助您建立健康、快乐、值得庆祝的性能文化。 创建强大的性能优化文化意味着在你的公司或团队中创建一个如下所示的反馈循环&#xff…...

【Android】Service介绍和生命周期

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 介绍 Service是Android程序中四大基础组件之一&#xff0c;它和Activity一样都是Context的子类&#xff0c;只不…...

【力扣数据库知识手册笔记】索引

索引 索引的优缺点 优点1. 通过创建唯一性索引&#xff0c;可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度&#xff08;创建索引的主要原因&#xff09;。3. 可以加速表和表之间的连接&#xff0c;实现数据的参考完整性。4. 可以在查询过程中&#xff0c;…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明

AI 领域的快速发展正在催生一个新时代&#xff0c;智能代理&#xff08;agents&#xff09;不再是孤立的个体&#xff0c;而是能够像一个数字团队一样协作。然而&#xff0c;当前 AI 生态系统的碎片化阻碍了这一愿景的实现&#xff0c;导致了“AI 巴别塔问题”——不同代理之间…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...

在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案

这个问题我看其他博主也写了&#xff0c;要么要会员、要么写的乱七八糟。这里我整理一下&#xff0c;把问题说清楚并且给出代码&#xff0c;拿去用就行&#xff0c;照着葫芦画瓢。 问题 在继承QWebEngineView后&#xff0c;重写mousePressEvent或event函数无法捕获鼠标按下事…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

MySQL JOIN 表过多的优化思路

当 MySQL 查询涉及大量表 JOIN 时&#xff0c;性能会显著下降。以下是优化思路和简易实现方法&#xff1a; 一、核心优化思路 减少 JOIN 数量 数据冗余&#xff1a;添加必要的冗余字段&#xff08;如订单表直接存储用户名&#xff09;合并表&#xff1a;将频繁关联的小表合并成…...

tomcat入门

1 tomcat 是什么 apache开发的web服务器可以为java web程序提供运行环境tomcat是一款高效&#xff0c;稳定&#xff0c;易于使用的web服务器tomcathttp服务器Servlet服务器 2 tomcat 目录介绍 -bin #存放tomcat的脚本 -conf #存放tomcat的配置文件 ---catalina.policy #to…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...

针对药品仓库的效期管理问题,如何利用WMS系统“破局”

案例&#xff1a; 某医药分销企业&#xff0c;主要经营各类药品的批发与零售。由于药品的特殊性&#xff0c;效期管理至关重要&#xff0c;但该企业一直面临效期问题的困扰。在未使用WMS系统之前&#xff0c;其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...