wap网站前台/seo引擎优化方案

wap网站前台,seo引擎优化方案,网站建设人员任职要求,网站建设开题报告ppt该博客对于学完C和linux操作系统,但不知道如何用C开发项目,已经不知道C如何使用第三方库的人来说一定很有帮助,请耐心看完! 先看一下游戏会显示的前端界面,对理解这个游戏的前后端交互过程会有帮助 1. 开发环境 1.1 …

该博客对于学完C++和linux操作系统,但不知道如何用C++开发项目,已经不知道C++如何使用第三方库的人来说一定很有帮助,请耐心看完!

先看一下游戏会显示的前端界面,对理解这个游戏的前后端交互过程会有帮助

1. 开发环境

1.1 使用的操作系统:Ubuntu-22.04

我们可以先在虚拟机或者服务器上选择或者安装这个Ubuntu-22.04操作系统。最后我们这个网页对战五子棋的服务器是要部署在服务器上的。博主选用的是腾讯云的服务器。

1.2 安装gcc/g++编译器

我们程序使用g++进行编译。我们在命令行输入下面指令即可安装

sudo apt-get install gcc g++

1.3 安装gdb调试器

我们程序使用gdb进行调试。我们在命令行输入下面指令即可安装

sudo apt-get install gdb

1.4 安装git⼯具。

我的代码已经上传到码云之后:https://gitee.com/xwyg/Cpp_project.git

sudo apt-get install git

1.5 安装cmake项⽬构建⼯具

我们项目的自动化构建和编译只用到了make,而cmake用来执行websocket库的构建和编译。

sudo apt-get install cmake

1.6 安装第三方库:

安装jsoncpp库和 安装jsoncpp库

sudo apt-get install libjsoncpp-dev
sudo apt-get install libboost-all-dev

Jsoncpp库用来处理JSON格式数据,我们在进行客户端和服务器通信过程中正文部分传递的是字符串,单纯字符串的提取处理要复杂一些,我们将其转换成Json格式的数据,便于我们在请求和响应中提取对于信息。

我们采取的是Restful风格的网络通信接口,即为使用GET/POST/PUT/DELETE代表不同请求类型,并且正文格式都是使用JSON格式序列化后的字符串

安装websocketpp库

Websocketpp没有官方维护的预编译包直接供apt-get使用,因此它的安装比较复杂,需要我们自己下载源代码进行编译,大家可以在deepseek或者chatgpt中之间搜安装教程。用这两个生成的安装方法还是很准确的。

Websocketpp库是我们服务器使用的主要库,它依赖于boost库,处理WebSocket通信.

Websocket介绍: 

WebSocket协议是从HTML5开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制。

• 传统的web程序都是属于"⼀问⼀答"的形式,即客⼾端给服务器发送了⼀个HTTP请求,服务器给客⼾端返回⼀个HTTP响应。这种情况下服务器是属于被动的⼀⽅,如果客⼾端不主动发起请求服务器就无法主动给客户端响应

• 网页即时聊天 或者 我们做的五子棋游戏这样的程序都是⾮常依赖"消息推送"的,即需要服务器主动推动消息到客户端。如果只是使⽤原⽣的HTTP协议,要想实现消息推送⼀般需要通过客户端"轮询"的⽅式实现,而轮询的成本⽐较⾼并且也不能及时的获取到消息的响应。

基于上述两个问题,就产⽣了WebSocket协议。WebSocket更接近于TCP这种级别的通信⽅式,⼀旦连接建⽴完成客户端或者服务器都可以主动的向对⽅发送数据。

WebSocket原理解析

WebSocket协议本质上是⼀个基于TCP的协议。为了建⽴⼀个WebSocket连接,客⼾端浏览器⾸先要向服务器发起⼀个HTTP请求,这个请求和通常的HTTP请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握⼿过程并升级协议的过程。

WebSocket也有自己的报文格式,但是其实与本项目关系没有那么大,我们之间WebSocketpp调用对应的接口即可,在后面项目代码中会有具体的介绍。

WebSocketpp同时⽀持HTTP和Websocket两种⽹络协议,⽐较适⽤于我们本次的项⽬,所以我们选⽤该库作为项⽬的依赖库⽤来搭建HTTP和WebSocket服务器。

1.7 mysql安装

博主安装的是mysql 5.7版本的数据库,各位安装8.0版本的数据库也是一样的,各位从deepseek或者chatgpt搜mysql的安装教程比较靠谱。

2.项目流程

这个流程图非常重要,关系到我们整个游戏的运行流程,已经前后端交互流程,还有服务器的各个模块之间的交互和联系。

2.1 客户端流程

客户端流程: 进入用户注册页面--->完成用户注册--->跳转到用户登录页面-->完成用户登录
---->跳转到游戏大厅页面--->点击按钮进入游戏匹配-->匹配成功跳转到游戏房价页面
---->游戏房间可以进行下棋或者聊天等操作--->游戏结束,加分或者扣分
--->该房间内无法下棋,弹出"回到大厅"按钮--->用户又可以点击按钮进入游戏匹配

2. 2 服务器流程

首先玩家想要访问我们的服务器得通过访问101.35.46.142:7080/register.html 注册页面或者101.35.46.142:7080/login.html登录页面,访问这两个页面会向我们的服务器发送http请求。

对于注册页面发送的http请求,服务器从请求正文中获取此时输入的用户名和密码,然后进行数据库中数据的插入; 对于登录页面发送的htpp请求,服务器从请求正文中获取此时输入的用户名和密码,然后服务器会建立用户的session并保存( 此后每次用户发送http或者websocket请求过来都会进行session的验证), 此时客户端跳转到游戏大厅页面,发送 大厅websocket长连接建立请求,服务器此时建立同客户端的长连接,当客户端点击游戏大厅页面的 "进入匹配" 按钮之后,会向客户端发送Websocket消息,服务器会按照该用户的等级分数 把他加入对应的匹配队列之中,等匹配成功之后会向两个进行匹配的客户端发送匹配成功的消息,客户端收到该请求进入游戏房间,此时参与匹配的两个客户端(玩家)都会向服务器发送 房间webhasocket长连接建立请求,服务器同客户端建立两个房间长连接,此时用户进行下棋或者聊天动作,都会向服务器发送对应请求,服务器也会给出对应的响应。

3.websocketpp库的介绍

3.1  websocketpp常用接口

namespace websocketpp {
typedef lib::weak_ptr<void> connection_hdl;
template <typename config>
class endpoint : public config::socket_type {typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;typedef typename connection_type::ptr connection_ptr;typedef typename connection_type::message_ptr message_ptr;typedef lib::function<void(connection_hdl)> open_handler;typedef lib::function<void(connection_hdl)> close_handler;typedef lib::function<void(connection_hdl)> http_handler;typedef lib::function<void(connection_hdl,message_ptr)> message_handler;* websocketpp::log::alevel::none 禁⽌打印所有⽇志*/void set_access_channels(log::level channels);/*设置⽇志打印等级*/void clear_access_channels(log::level channels);/*清除指定等级的⽇志*//*设置指定事件的回调函数*/void set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/void set_message_handler(message_handler h);/*websocket消息回调处理函数*/void set_http_handler(http_handler h);/*http请求回调处理函数*//*关闭连接接⼝*/void close(connection_hdl hdl, close::status::value code, std::string& reason);/*获取connection_hdl 对应连接的connection_ptr*/connection_ptr get_con_from_hdl(connection_hdl hdl);/*websocketpp基于asio框架实现,init_asio⽤于初始化asio框架中的io_service调度器*/void init_asio();/*设置是否启⽤地址重⽤*/void set_reuse_addr(bool value);/*设置endpoint的绑定监听端⼝*/void listen(uint16_t port);/*对io_service对象的run接⼝封装,⽤于启动服务器*/std::size_t run();/*websocketpp提供的定时器,以毫秒为单位*/timer_ptr set_timer(long duration, timer_handler callback);
};template <typename config>
class server : public endpoint<connection<config>,config> {/*初始化并启动服务端监听连接的accept事件处理*/void start_accept();
}

看到这些代码可能会一脸懵,我们只需要知道websocketpp命名空间下定义了 server类(继承自endpoint),它就是我们要启动的服务器,调用它的方法,我们就可以对服务器进行各种设置,同时它有一些回调函数:

set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/
void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/
void set_message_handler(message_handler h);/*websocket消息回调处理函数*/
void set_http_handler(http_handler h);/*http请求回调处理函数*/

这些函数需要我们传入一个函数对象,这个函数对象是一个 void (connection_hdl hdl)类型,请看使用实例:

#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;// websocket连接成功的回调函数
void OnOpen(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"连接成功"<<endl;
}// websocket连接成功的回调函数
void OnClose(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"连接关闭"<<endl;
}// websocket连接收到消息的回调函数
void OnMessage(websocketsvr *server,websocketpp::connection_hdl hdl,message_ptr msg){cout << "收到消息" << msg->get_payload() << endl;// 收到消息将相同的消息发回给websocket客⼾端server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}// websocket连接异常的回调函数
void OnFail(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"连接异常"<<endl;
}
// 处理http请求的回调函数 返回⼀个html欢迎⻚⾯
void OnHttp(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"处理http请求"<<endl;websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);std::stringstream ss;ss << "<!doctype html><html><head>"<< "<title>hello websocket</title><body>"<< "<h1>hello websocketpp</h1>"<< "</body></head></html>";con->set_body(ss.str());  //设置http响应正文con->set_status(websocketpp::http::status_code::ok);  //设置http响应状态码
}

这是main(),告诉我们server如何初始化,格式都是一样的,同时还需要绑定(注册)对应请求来时的处理动作。

int main()
{// 使⽤websocketpp库创建服务器websocketsvr server;// 设置websocketpp库的⽇志级别 all表⽰打印全部级别⽇志  none表⽰什么⽇志都不打印server.set_access_channels(websocketpp::log::alevel::none);/*初始化asio*/server.init_asio();// 注册http请求的处理函数server.set_http_handler(bind(&OnHttp, &server, ::_1));// 注册websocket请求的处理函数server.set_open_handler(bind(&OnOpen, &server, ::_1));server.set_close_handler(bind(&OnClose, &server, _1));server.set_message_handler(bind(&OnMessage, &server, _1, _2));// 监听8888端⼝server.listen(8888);// 开始接收tcp连接server.start_accept();// 开始运⾏服务器server.run();return 0;
}

Http客⼾端,使⽤浏览器作为http客⼾端即可,访问服务器的8888端⼝。

任意浏览器输入即可请求我们服务器:

前端如何发送websocket请求,这个不是我们的重点,但是也大致了解一下前端代码:

<html>
<body><input type="text" id="message"><button id="submit">提交</button><script>// 创建 websocket 实例// ws://192.168.51.100:8888// 类⽐http// ws表⽰websocket协议// 192.168.51.100 表⽰服务器地址// 8888表⽰服务器绑定的端⼝let websocket = new WebSocket("ws://192.168.51.100:8888");// 处理连接打开的回调函数websocket.onopen = function () {console.log("连接建⽴");}// 处理收到消息的回调函数// 控制台打印消息websocket.onmessage = function (e) {console.log("收到消息: " + e.data);}// 处理连接异常的回调函数websocket.onerror = function () {console.log("连接异常");}// 处理连接关闭的回调函数websocket.onclose = function () {console.log("连接关闭");}// 实现点击按钮后, 通过 websocket实例 向服务器发送请求let input = document.querySelector('#message');let button = document.querySelector('#submit');button.onclick = function () {console.log("发送消息: " + input.value);websocket.send(input.value);}</script>
</body></html>

服务器启动,我们将上面的代码复制到 abc.html文件中,打开并在输入框输入hello,即可得到以下内容:

 new WebSocket("ws://192.168.51.100:8888");然后将他绑定在一个按钮中,点击按钮即可向后端发送websocket请求,此时服务器收到请求,会同客户端建立websocket长连接

4.项目实现

4.1 日志宏的实现

我们不采用websocketpp库中的日志类,而是自己编写一个日志类,定义一个日志宏,传入日志等级和可变参数,然后将线程ID,文件名,行号,时间与传入的可变字符串拼接起来,一起向终端(或文件)打印。

1. strftime函数

其中 strftime函数:将时间格式化为字符串。我们可以用snprintf(time_buffer, sizeof(time_buffer), "%d",format_time->tm_year + 1900)代替。

2.宏参数允许 进行 字符串拼接

 fprintf中第二个参数,要求传入一个const char*字符串,我们可以直接拼接我们需要的格式化字符串和传入的格式化字符串,这在函数无法实现   "[%p %s %s:%d] " format " \n"。

3.宏中可变参数

C++98允许宏传入可变参数,由##__VA_ARGS__代替

代码如下:

#pragma once#include <stdio.h>
#include <time.h>
#include<pthread.h>
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG  //超过这个日志等级才会被输出
// 宏里面有多条语句,使用do while(0),
#define LOG(level, format, ...)                                               \do                                                                        \{                                                                         \if (level < LOG_LEVEL)                                                \break;                                                            \time_t t = time(NULL);                                                \struct tm *ltm = localtime(&t);                                       \char tmp[32] = {0};                                                   \strftime(tmp, 31, "%H:%M:%S", ltm);                                   \fprintf(stdout, "[%p %s %s:%d] " format " \n", (void *)pthread_self(), \tmp, __FILE__, __LINE__, ##__VA_ARGS__);                      \}while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

4.2 工具类的实现

我们实现四个工具类,工具类中封装有静态方法,全局都可以使用该方法

首先是  json_util,提供序列化和反序列化的方法。

序列化通过Json::StreamWriter对象指针将Json::Value对象写入 str字符串中,反序列化通过Json::CharReader将str字符串写入Json::Value对象中

class json_util
{
public:// 将jsonvalue对象写入 str字符串中static bool serialize(const Json::Value &value, std::string &str){Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;int ret = sw->write(value, &ss);if (ret != 0){std::cout << "json serialize failed!" << std::endl;return false;}str = ss.str();return true;}// 将json字符串写回到value对象中,由用户自己做[""].asInt等处理static bool unserialize(const std::string &str, Json::Value &value){Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());bool ret = cr->parse(str.c_str(), str.c_str() + str.size(),&value, nullptr);if (!ret){ERR_LOG("json unserialize failed!");return false;}return true;}
};

第二个是mysql_util,提供数据库的创建,销毁,执行方法

class mysql_util
{
public:static MYSQL *mysql_create(const std::string &host,const std::string &user,const std::string &pass,const std::string &db,int port){MYSQL *mysql = mysql_init(NULL);if (mysql == NULL){ERR_LOG("mysql init failed!");return NULL;}if (mysql_real_connect(mysql, host.c_str(), user.c_str(),pass.c_str(), db.c_str(), port, NULL, 0) == NULL){ERR_LOG("mysql connect server failed! %s", mysql_error(mysql));mysql_close(mysql);return NULL;}if (mysql_set_character_set(mysql, "utf8") != 0){ERR_LOG("mysql set character failed!");mysql_close(mysql);return NULL;}DBG_LOG("mysql connect success!");return mysql;}static void mysql_destroy(MYSQL *mysql){if (mysql == NULL){return;}mysql_close(mysql);}static bool mysql_exec(MYSQL *mysql, const std::string &sql){if (mysql_query(mysql, sql.c_str()) != 0){ERR_LOG("SQL: %s", sql.c_str());ERR_LOG("ERR: %s", mysql_error(mysql));return false;}return true;}
};

第三个是string_util,提供字符串分割方法,因为我们涉及http 某一个请求头的提取已经某一个cookie的提取,提取不会改变原有的字符串和分割字符串,提取到一个vector<string>中

class string_util
{
public:// 字符串 子串分割功能, 将分割的子串保存到一个字符串数组之中static int split(const std::string &in, const std::string &sep,std::vector<std::string> &arry){arry.clear();size_t pos, idx = 0; // pos保存为查找结果,如果pos和idx相等,该位置就是sep,则不保存while (idx < in.size()){pos = in.find(sep, idx);if (pos == std::string::npos){arry.push_back(in.substr(idx));break;}if (pos != idx){arry.push_back(in.substr(idx, pos - idx)); // 当前位置,长度}idx = pos + sep.size();}return arry.size();}
};

第四个是file_util,对静态请求处理时将html文件返回给客户端

class file_util
{
public:static bool read(const std::string &filename, std::string &body){std::ifstream file;// 打开⽂件file.open(filename.c_str(), std::ios::in | std::ios::binary);if (!file){std::cout << filename << " Open failed!" << std::endl;return false;}// 计算⽂件⼤⼩file.seekg(0, std::ios::end);body.resize(file.tellg());file.seekg(0, std::ios::beg);file.read(&body[0], body.size());if (file.good() == false){std::cout << filename << " Read failed!" << std::endl;file.close();return false;}file.close();return true;}
};

4.3 数据库操作模块实现

我们先定义数据库中的表,我们这个项目比较简单,只有一张user表,提供用户id,username,password,score,对战总场次和获胜场次这些字段

我们可以将每个表都封装到一个类之中,提供一个对外的MYSQL句柄,执行对应的数据库操作。

由于字符串中拼接比较麻烦,我们选择#define 定义格式化字符串,再有sprintf()函数去进行写入。

我们所需要进行数据库操作的地方有 用户插入,用户登录,通过用户名查询用户,通过用户id查询用户,用户获胜和用户失败这些情况,分别实现这些函数

#pragma once
#include "util.hpp"
#include <mutex>
#include <assert.h>
//  user_table类,将所要执行数据库操作的地方全部封装到该类之中,包含一个MYSQL指针和mutex互斥量
//  调用了 mysql_util类中的 创建销毁和执行方法
//  提供了 insert(user),login(user),win,lose,select_by_uid等等函数// 每个函数所要执行的sql由宏定义给出,sql的字符串都要以;结尾,同时varchar类型都要在''里面
// mysql_query是线程安全的,但是它和mysql_store_result(_mysql)保存一起就不是线程安全的了
class user_table
{
private:MYSQL *_mysql;std::mutex _mutex;public:user_table(const std::string &host,const std::string &user,const std::string &pass,const std::string &db,int port = 3306){_mysql = mysql_util::mysql_create(host, user, pass, db, port);assert(_mysql != NULL);}// 网络中传输的是字符串,需要讲它们序列化到一个个的request对象中,再调用_cal计算并将结果,反序列化成字符串返回bool insert(Json::Value &user){
#define INSERT_USER "insert user values(null, '%s', password('%s'), 1000, 0,0);"if (user["password"].isNull() || user["username"].isNull()){DBG_LOG("INPUT PASSWORD OR USERNAME");return false;}char sql[4096] = {0};sprintf(sql, INSERT_USER, user["username"].asCString(),user["password"].asCString());bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("insert user info failed!!\n");return false;}return true;}bool login(Json::Value & user) // 用户登录,并返回完整的用户信息{if (user["password"].isNull() || user["username"].isNull()){DBG_LOG("INPUT PASSWORD OR USERNAME");return false;}// 以用户名和密码共同查询,查询到数据则表⽰⽤⼾名密码⼀致,没有信息则用户名密码错误
#define LOGIN_USER "select id, score, total_count,win_count from user where username='%s' and password=password('%s');"char sql[4096] = {0};sprintf(sql, LOGIN_USER, user["username"].asCString(),user["password"].asCString());MYSQL_RES *res = NULL;{// std::lock_guard<std::mutex> lock(_mutex);std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("user login failed!!\n");return false;}// 将查询结果保存到本地res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("mysql_store_result exec error!!");return false;}}std::cout << res << std::endl;// 根据结果集获取条目输了int num_row = mysql_num_rows(res);if (num_row == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 查询结果集的四行数据设置进 原有user中user["id"] = std::stoi(row[0]);    // 如果数据范围小,默认int够用则无需转换user["score"] = std::stoi(row[1]); // (Json::UInt64)user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);std::cout << "jjjjj" << std::endl;mysql_free_result(res);return true;}bool select_by_name(const std::string &name, Json::Value &user) // 通过用户名查询用户{
#define USER_BY_NAME "select id, score, total_count, win_count from user where username = '%s';"char sql[4096] = {0};sprintf(sql, USER_BY_NAME, name.c_str());MYSQL_RES *res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("get user by name failed!!\n");return false;}// 按理说要么有数据,要么没有数据,就算有数据也只能有⼀条数据res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("hmysql_store_result!!");return false;}}int row_num = mysql_num_rows(res);if (row_num == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stoi(row[0]);user["username"] = name;user["score"] = (Json::UInt64)std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}bool select_by_id(int id, Json::Value &user) // 通过id查询用户{
#define USER_BY_ID "select username,score,total_count,win_count from user where id = %d;"MYSQL_RES *res = NULL;char sql[4096] = {0};sprintf(sql, USER_BY_ID, id);{std::lock_guard<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("select_by_id mysql_exec error");return false;}res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("mysql_store_result error!!");return false;}}int row_num = mysql_num_rows(res);if (row_num == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["username"] = row[0];user["score"] = std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}bool win(int id) // 用户胜利时,总场次和胜利场次都加1{
#define USER_WIN "update user set score=score+30,total_count=total_count+1,  \win_count=win_count+1 where id=%d;"char sql[1024] = {0};sprintf(sql, USER_WIN, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("update win user info failed!!\n");return false;}return true;}bool lose(int id) // 用户失败时,总场次加1,分数不变{
#define USER_LOSE "update user set score=score-30,total_count=total_count+1 where id=%d;"char sql[1024] = {0};sprintf(sql, USER_LOSE, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("update win user info failed!!\n");return false;}return true;}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = NULL;}};

4.4 用户在线管理模块实现

用户在线管理模块记录了用户进入我们服务器之后所处的存在状态,是否离线,是在大厅还是房间

用户首先发送http请求注册页面,输入用户名密码完成数据库插入,然后进入登录页面,登录成功之后我们就需要将这个用户管理起来,因为存在好多的客户端,我们需要根据用户id找到这些客户端,因此选用 std::unordered_map<int, websocket_server::connection_ptr> _game_hall建立游戏大厅中用户的管理和std::unordered_map<int, websocket_server::connection_ptr> _game_room;游戏房间中用户的管理。

#pragma once
#include "util.hpp"
#include <mutex>
#include <unordered_map>// 在线用户的管理类,在线用户要么在游戏大厅,要么在游戏房间
// 维护用户id到服务器连接的 游戏大厅map和用户id到服务器连接的 游戏房间map,以及一个互斥量mutex
// 提供进入(退出)大厅,进入(退出)房间,获取这个用户的连接 等操作class online_manager
{// 使用map维护 从id到connection的关系
private:/*游戏⼤厅的客⼾端连接管理*/std::unordered_map<int, websocket_server::connection_ptr> _game_hall;/*游戏房间的客⼾端连接管理*/std::unordered_map<int, websocket_server::connection_ptr> _game_room;std::mutex _mutex;public:/*进⼊游戏⼤厅--游戏⼤厅连接建⽴成功后调⽤*/void enter_game_hall(int uid, const websocket_server::connection_ptr &conn){std::unique_lock<std::mutex> lock(_mutex);_game_hall.insert(std::make_pair(uid, conn));}/*退出游戏⼤厅--游戏⼤厅连接断开后调⽤*/void exit_game_hall(int uid){std::unique_lock<std::mutex> lock(_mutex);_game_hall.erase(uid);}/*进⼊游戏房间--游戏房间连接建⽴成功后调⽤*/void enter_game_room(int uid, const websocket_server::connection_ptr &conn){std::unique_lock<std::mutex> lock(_mutex);_game_room.insert(std::make_pair(uid, conn));}/*退出游戏房间--游戏房间连接断开后调⽤*/void exit_game_room(int uid){std::unique_lock<std::mutex> lock(_mutex);_game_room.erase(uid);}/*判断用户是否在游戏⼤厅*/bool in_game_hall(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_hall.find(uid);if (it == _game_hall.end()){return false;}return true;}/*判断用户是否在游戏房间*/bool in_game_room(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_room.find(uid);if (it == _game_room.end()){return false;}return true;}/*从游戏⼤厅中获取指定⽤⼾关联的Socket连接*/websocket_server::connection_ptr get_conn_from_game_hall(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_hall.find(uid);if (it == _game_hall.end()){return nullptr;}return it->second;}/*从游戏房间中获取指定⽤⼾关联的Socket连接*/websocket_server::connection_ptr get_conn_from_game_room(int uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_room.find(uid);if (it == _game_room.end()){return nullptr;}return it->second;}online_manager(){}~online_manager(){}
};

4.5 会话管理模块实现

现在基本所有网络通信都要实现一个会话管理模块,当用户登录成功之后,服务器使用一个SeeionId需要标记这个用户,这样后续用户每次操作都会发送sessionid给服务器,服务器也可以做用户验证,同时识别这是哪个客户端。

我们在类的设计上需要实现两个类,一个会话类,一个会话管理类,

会话类中包含会话id,用户id,会话状态,定时器

这个定时器主要是看这个session下是否设置了定时器过期任务,如果websocket_server::timer_ptr为空,则为永久存在。

#pragma once
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "util.hpp"typedef enum
{LOGIN,UNLOGIN
} ss_statu;// 一个会话类和一个会话管理类。
// 会话类中包含      会话id,用户id,会话状态,定时器(不过期和何时过期)
// 会话管理类中:  互斥锁,websocket服务器(给它设置定时任务) 和
// 分配的下一个sessionid和sessionid到整个会话的map。 提供创建session 和设置过期时间的函数class session
{
private:uint64_t _ssid; // 标识符int _uid;ss_statu _statu;websocket_server::timer_ptr _tp; // 该session相关定时器
public:session(uint64_t ssid) : _ssid(ssid){DBG_LOG("SESSION %p 被创建!!", this);}~session() { DBG_LOG("SESSION %p 被释放!!", this); }uint64_t ssid() { return _ssid; };void set_statu(ss_statu statu) { _statu = statu; }void set_user(int uid) { _uid = uid; }uint64_t get_user() { return _uid; }bool is_login() { return (_statu == LOGIN); }void set_timer(const websocket_server::timer_ptr &tp) { _tp = tp; }websocket_server::timer_ptr &get_timer() { return _tp; }
};

会话管理类中包含,需要分配的下一个会话id,websocket服务器(用于设置定时任务),一个会话id到整个会话的映射map. 

注意,websocket定时器取消时,它取消绑定函数会执行一次(不一定马上执行),所以需要重新添加。

#define SESSION_TIMEOUT 3000
#define SESSION_FOREVER -1
using session_ptr = std::shared_ptr<session>;
class session_manager
{
private:uint64_t _next_ssid;std::mutex _mutex;std::unordered_map<uint64_t, session_ptr> _session;websocket_server *_server;public:session_manager(websocket_server *srv) : _next_ssid(1), _server(srv){DBG_LOG("session管理器初始化完毕!");}~session_manager() { DBG_LOG("session管理器即将销毁!"); }session_ptr create_session(uint64_t uid, ss_statu statu){std::unique_lock<std::mutex> lock(_mutex);session_ptr ssp(new session(_next_ssid));ssp->set_statu(statu);ssp->set_user(uid);  //创建会话时需要将用户id 和用户状态都设置进去_session.insert(std::make_pair(_next_ssid, ssp));_next_ssid++;return ssp;}void append_session(const session_ptr &ssp){std::unique_lock<std::mutex> lock(_mutex);_session.insert(std::make_pair(ssp->ssid(), ssp));}session_ptr get_session_by_ssid(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);auto it = _session.find(ssid);// 不存在这个ssid就返回空指针if (it == _session.end()){return session_ptr();}return it->second;}void remove_session(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);_session.erase(ssid);}// 定时器tp->cancel 不是立即执行的,所以在_server->set_timer执行插入// session的定时任务重置需要 先取消再重新添加。  void set_session_expire_time(uint64_t ssid, int ms){session_ptr ssp = get_session_by_ssid(ssid);if (ssp.get() == nullptr){return;}websocket_server::timer_ptr tp = ssp->get_timer();if (tp.get() == nullptr && ms == SESSION_FOREVER){// 1. 在session永久存在的情况下,设置永久存在return;}else if (tp.get() == nullptr && ms != SESSION_FOREVER){// 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tmp_tp);}else if (tp.get() != nullptr && ms == SESSION_FOREVER){// 3. 在session设置了定时删除的情况下,将session设置为永久存在// 取消定时任务tp->cancel();ssp->set_timer(websocket_server::timer_ptr());_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));}else{// 4. 在session设置了定时删除的情况下,将session重置删除时间。// 先取消定时任务,再把该session对象添加到管理队列中tp->cancel();ssp->set_timer(websocket_server::timer_ptr()); _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));// 重新绑定新的定时任务websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tmp_tp);}}
};

4.6 房间管理模块

用户进入游戏大厅后,存在于在线管理模块的map之中,然后用户选择进入匹配,此时为用户创建匹配队列,匹配成功创建房间,按照逻辑下来是先有匹配队列再有房间,但是匹配队列中必须调用创建房间的接口,去帮用户进入房间之中。因此先介绍房间管理模块。

第一个房间类,里面有成员房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库的管理句柄。它需要提供处理用户请求(聊天或者下棋)的函数,以及判断输赢,将响应返回给所有房间用户。

第二个房间管理类,分配房间的roomid,维护两个map,即为房间id到整个房间的映射map和用户id到房间id的映射map。

代码如下:

#pragma once#include <memory>
#include "db.hpp"
#include "online.hpp"// 房间类和房间管理类
// 房间类中有房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库 管理句柄
//  提供handle_request识别请求,进行下棋或者聊天,同时有 用户退出,广播等等动作
// 房间管理类 分配的roomid,两个map,提供房间的 create remove selectbyroomid等接口typedef enum
{GAME_START,GAME_OVER
} room_status;#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2class room
{
private:uint64_t _room_id;room_status _status;int _player_count;int _white_id;int _black_id;user_table *_tb_user;std::vector<std::vector<int>> _board;online_manager *_online_user;public:room(){}room(uint64_t room_id, user_table *tb, online_manager *online_user): _room_id(room_id), _status(GAME_START), _player_count(0),_tb_user(tb), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0)){DBG_LOG("%lu 房间创建成功!!", _room_id);}~room(){DBG_LOG("%lu 房间销毁成功!!", _room_id);}/*添加白棋黑棋用户,获取房间id等接口*/uint64_t id() { return _room_id; }room_status statu() { return _status; }int player_count() { return _player_count; }void add_white_user(int uid){_white_id = uid;_player_count++;}void add_black_user(int uid){_black_id = uid;_player_count++;}int get_white_user() { return _white_id; }int get_black_user() { return _black_id; }bool five(int row, int col, int row_off, int col_off, int color){// row和col是下棋位置, row_off和col_off是偏移量,也是⽅向int count = 1;int search_row = row + row_off;int search_col = col + col_off;while (search_row >= 0 && search_row < BOARD_ROW &&search_col >= 0 && search_col < BOARD_COL &&_board[search_row][search_col] == color){// 同⾊棋⼦数量++count++;// 检索位置继续向后偏移search_row += row_off;search_col += col_off;}search_row = row - row_off;search_col = col - col_off;while (search_row >= 0 && search_row < BOARD_ROW &&search_col >= 0 && search_col < BOARD_COL &&_board[search_row][search_col] == color){// 同⾊棋⼦数量++count++;// 检索位置继续向后偏移search_row -= row_off;search_col -= col_off;}return (count >= 5);}int check_win(int row, int col, int color){// 从下棋位置的四个不同⽅向上检测是否出现了5个及以上相同颜⾊的棋⼦(横⾏,纵 列,正斜,反斜)if (five(row, col, 0, 1, color) ||five(row, col, 1, 0, color) ||five(row, col, -1, 1, color) ||five(row, col, -1, -1, color)){// 任意⼀个⽅向上出现了true也就是五星连珠,则设置返回值return color == CHESS_WHITE ? _white_id : _black_id;}return 0;}/*处理下棋动作*/Json::Value handle_chess(Json::Value &req){Json::Value json_resp = req;// 2. 判断房间中两个玩家是否都在线,任意⼀个不在线,就是另⼀⽅胜利。int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();uint64_t cur_uid = req["uid"].asUInt64();if (_online_user->in_game_room(_white_id) == false){json_resp["result"] = true;json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";json_resp["winner"] = (Json::UInt64)_black_id;return json_resp;}if (_online_user->in_game_room(_black_id) == false){json_resp["result"] = true;json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";json_resp["winner"] = (Json::UInt64)_white_id;return json_resp;}// 3. 获取⾛棋位置,判断当前⾛棋是否合理(位置是否已经被占⽤)if (_board[chess_row][chess_col] != 0){json_resp["result"] = false;json_resp["reason"] = "当前位置已经有了其他棋⼦!";return json_resp;}int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;_board[chess_row][chess_col] = cur_color;// 4. 判断是否有玩家胜利(从当前⾛棋位置开始判断是否存在五星连珠)int winner_id = check_win(chess_row, chess_col, cur_color);if (winner_id != 0){json_resp["reason"] = "五星连珠,国服棋王,你无敌了!";}json_resp["result"] = true;json_resp["winner"] = (Json::UInt64)winner_id;return json_resp;}/*处理聊天动作*/Json::Value handle_chat(const Json::Value &req){Json::Value json_resp = req;std::string chat_message = req["message"].asString();if (chat_message.find("垃圾") != std::string::npos || chat_message.find("你干嘛") != std::string::npos){json_resp["result"] = false;json_resp["reason"] = "嘻嘻,请说喜欢你";return json_resp;}json_resp["result"] = true;return json_resp;}/*处理退出动作*/void handle_exit(int uid){Json::Value json_resp;// 如果是下棋状态中退出,一方胜利if (_status == GAME_START){int winner_id = uid == _white_id ? _black_id : _white_id;json_resp["optype"] = "put_chess";json_resp["result"] = true;json_resp["reason"] = "对⽅掉线,不战⽽胜!";json_resp["room_id"] = (Json::UInt64)_room_id;json_resp["uid"] = uid;json_resp["row"] = -1;json_resp["col"] = -1;json_resp["winner"] = winner_id;int loser_id = winner_id == _white_id ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_status = GAME_OVER;broadcast(json_resp);}// 房间中玩家数量--_player_count--;}/*总的请求处理函数,区分不同请求类型,调用不同函数执行对应响应,得到响应进行广播*/void handle_request(Json::Value &req){// 1. 校验房间号是否匹配Json::Value json_resp;uint64_t room_id = req["room_id"].asUInt64();if (room_id != _room_id){json_resp["optype"] = req["optype"].asString();json_resp["result"] = false;json_resp["reason"] = "房间号不匹配!";return broadcast(json_resp);}// 2. 根据不同的请求类型调⽤不同的处理函数if (req["optype"].asString() == "put_chess"){json_resp = handle_chess(req);if (json_resp["winner"].asUInt64() != 0){uint64_t winner_id = json_resp["winner"].asUInt64();uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_status = GAME_OVER;}}else if (req["optype"].asString() == "chat"){json_resp = handle_chat(req);}else{json_resp["optype"] = req["optype"].asString();json_resp["result"] = false;json_resp["reason"] = "未知请求类型";}std::string body;json_util::serialize(json_resp, body);DBG_LOG("房间-⼴播动作: %s", body.c_str());return broadcast(json_resp);}/*将指定的信息广播给房间中的所有用户,即返回响应给所有用户*/void broadcast(const Json::Value &resp){// 1. 对要响应的信息进⾏序列化,将Json::Value中的数据序列化成为json格式字符串std::string body;json_util::serialize(resp, body);// 2. 获取房间中所有⽤⼾的通信连接// 3. 发送响应信息websocket_server::connection_ptr white_conn =_online_user->get_conn_from_game_room(_white_id);if (white_conn.get() != nullptr){white_conn->send(body);}else{DBG_LOG("房间-⽩棋玩家连接获取失败");}websocket_server::connection_ptr bconn = _online_user->get_conn_from_game_room(_black_id);if (bconn.get() != nullptr){bconn->send(body);}else{DBG_LOG("房间-⿊棋玩家连接获取失败");}return;}
};
using room_ptr = std::shared_ptr<room>;class room_manager
{
private:uint64_t _next_rid;std::mutex _mutex;user_table *_tb_user;online_manager *_online_user;std::unordered_map<uint64_t, room_ptr> _rooms; // 房间id到整个房间的映射std::unordered_map<int, uint64_t> _users;      // 用户id到房间id的映射public:room_manager(user_table *ut, online_manager *om): _next_rid(1000),_tb_user(ut),_online_user(om){DBG_LOG("房间管理模块初始化完毕!");}~room_manager(){DBG_LOG("房间管理模块即将销毁!");}/*两个用户匹配成功的用户创建房间*/room_ptr create_room(int uid1, int uid2){// 1. 校验两个⽤⼾是否都还在游戏⼤厅中,只有都在才需要创建房间if (_online_user->in_game_hall(uid1) == false){DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid1);return room_ptr();}if (_online_user->in_game_hall(uid2) == false){DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid2);return room_ptr();}// 2. 创建房间,将⽤⼾信息添加到房间中room_ptr rp(new room(_next_rid, _tb_user, _online_user)); // 智能指针管理指针对象,传入指针进行构造rp->add_white_user(uid1);rp->add_black_user(uid2);// 3. 将房间信息管理起来_rooms.insert(std::make_pair(_next_rid, rp));_users.insert(std::make_pair(uid1, _next_rid));_users.insert(std::make_pair(uid2, _next_rid));_next_rid++;// 4. 返回房间信息return rp;}/*通过房间id获取房间*/room_ptr get_room_by_rid(uint64_t room){std::unique_lock<std::mutex> lock(_mutex);auto rit = _rooms.find(room);if (rit == _rooms.end()){return room_ptr();}return rit->second; // 等价_rooms[room];}/*通过用户id获取房间*/room_ptr get_room_by_uid(int uid){std::unique_lock<std::mutex> lock(_mutex); // 加锁??// 1. 通过⽤⼾ID获取房间IDauto uit = _users.find(uid);if (uit == _users.end()){return room_ptr();}uint64_t rid = uit->second;// 2. 通过房间ID获取房间信息auto rit = _rooms.find(rid);if (rit == _rooms.end()){return room_ptr();}return rit->second;}/*通过房间id删除房间*/void remove_room(uint64_t rid){// 因为房间信息,是通过shared_ptr在_rooms中进⾏管理,因此只要将shared_ptr从_rooms中移除// 则shared_ptr计数器==0,外界没有对房间信息进⾏操作保存的情况下就会释放// 1. 通过房间ID,获取房间信息room_ptr rp = get_room_by_rid(rid);if (rp.get() == nullptr){return;}// 2. 通过房间信息,获取房间中所有⽤⼾的IDuint64_t uid1 = rp->get_white_user();uint64_t uid2 = rp->get_black_user();// 3. 移除房间管理中的⽤⼾信息std::unique_lock<std::mutex> lock(_mutex);_users.erase(uid1);_users.erase(uid2);// 4. 移除房间管理信息_rooms.erase(rid);// auto it = _rooms.find(room);// if (it == _rooms.end())// {//     return;// }// std::unique_lock<std::mutex> lock(_mutex);// _users.erase(_rooms[room]->get_black_user());// _users.erase(_rooms[room]->get_white_user());// _rooms.erase(room);}/*删除房间中指定⽤⼾,如果房间中没有⽤⼾了,则销毁房间,⽤⼾连接断开时被调⽤*/void remove_room_by_user(int user){auto it = get_room_by_uid(user);if (it.get() == nullptr){return;}it->handle_exit(user);if (it->player_count() == 0){remove_room(it->id());}}
};

4.7 匹配管理模块

我们将根据用户得分维护三个匹配队列,每次用户匹配请求都在各自所属的段位里面进行匹配。

匹配队列类:包含好多用户id,mutex和条件变量cond, 还有push,wait,pop,remove等接口。

匹配管理类: 包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数,线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作.

#pragma once
#include "room.hpp"
#include <list>
#include <condition_variable>// 提供匹配队列和 匹配队列的管理类
// 匹配队列中包含好多用户id,mutex和条件变量cond,  还有push,wait,pop,remove等接口// 匹配队列的管理类 ,包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数
// 线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作
// 其提供add(uid)和del(uid)两个函数// T就是int类型就是每个队列中一个个的用户id
template <class T>
class match_queue
{
private:std::list<T> _list; // 我们使用list是因为我们需要 remove某些用户idstd::mutex _mutex;std::condition_variable _cond; //条件变量,在该条件变量下 进行wait阻塞等待public:match_queue(){}~match_queue(){}int size(){std::unique_lock<std::mutex> lock(_mutex);return _list.size();}bool empty(){std::unique_lock<std::mutex> lock(_mutex);return _list.empty();}/*阻塞队列*/void wait(){std::unique_lock<std::mutex> lock(_mutex);_cond.wait(lock);}/*入队数据,并唤醒线程*/void push(const T &data){std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);_cond.notify_all();}/*出队数据*/bool pop(T &data){std::unique_lock<std::mutex> lock(_mutex);if (_list.empty()){return false;}data = _list.front();_list.pop_front();return true;}void remove(T &data){std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);}
};// 需要了解网络通信接口的格式,匹配成功时回复给两个用户什么信息
class matcher
{
private:/*普通选⼿匹配队列*/match_queue<int> _q_normal;/*⾼⼿匹配队列*/match_queue<int> _q_high;/*⼤神匹配队列*/match_queue<int> _q_super;/*对应三个匹配队列的处理线程*/std::thread _th_normal;std::thread _th_high;std::thread _th_super;room_manager *_rm;user_table *_ut;online_manager *_om;void handle_match(match_queue<int> &mq){while(1){// 队列人数小于2,则阻塞while(mq.size()<2){mq.wait();}// 出队两个玩家,int id1,id2;bool ret= mq.pop(id1);if(ret==false){continue;}ret= mq.pop(id2);if(ret==false){mq.push(id2);continue;}//检测两个玩家是否在线websocket_server::connection_ptr conn1=_om->get_conn_from_game_hall(id1);if(conn1.get()==nullptr){mq.push(id2);continue;}websocket_server::connection_ptr conn2=_om->get_conn_from_game_hall(id2);if(conn2.get()==nullptr){mq.push(id1);continue;}//为两个玩家创建房间room_ptr rp= _rm->create_room(id1,id2);if(rp.get()==nullptr){mq.push(id1);mq.push(id2);continue;}//给两个玩家返回响应Json::Value resp;resp["result"]=true;resp["optype"]="match_success";std::string body;json_util::serialize(resp,body);conn1->send(body);conn2->send(body);}}// 三个线程的入口函数void th_normal_entry(){handle_match(_q_normal);}void th_high_entry(){handle_match(_q_high);}void th_super_entry(){handle_match(_q_super);}public:matcher(room_manager *rm, user_table *ut, online_manager *om): _rm(rm), _ut(ut), _om(om),_th_normal(&matcher::th_normal_entry,this),_th_high(&matcher::th_high_entry,this),_th_super(&matcher::th_super_entry,this){DBG_LOG("游戏匹配模块初始化完毕....");}bool add(int id){Json::Value user;bool ret = _ut->select_by_id(id, user);if (ret == false){DBG_LOG("获取玩家:%d 信息失败!!", id);return false;}int score = user["score"].asInt();if (score < 2000){_q_normal.push(id);}else if (score >= 2000 && score <= 3000){_q_high.push(id);}else{_q_super.push(id);}return true;}bool del(int id){Json::Value user;bool ret = _ut->select_by_id(id, user);if (ret == false){DBG_LOG("获取玩家:%d 信息失败!!", id);return false;}int score = user["score"].asInt();if (score < 2000){_q_normal.remove(id);}else if (score >= 2000 && score <= 3000){_q_high.remove(id);}else{_q_super.remove(id);}return true;}~matcher(){}
};

4.7 服务器模块

最后服务器模块应该包含前面所有的模块,服务器类为第三方库websocketpp的 websocket服务器,因此其成员应该有这些:

    std::string _web_root;   // 静态资源根⽬录websocket_server _wssrv; // websocket_server对象user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;

接收到http或websocket连接请求,websocket请求是会调用相关的回调函数,我们只需要注册这些回调函数即可,同上面所写的websocket服务器框架相同

http请求,客户端发送http请求只有 刚开始访问服务器时,请求注册或者登陆页面或者根目录(静态资源请求),还有就是点击注册或登录时(功能请求),还有刚进入游戏大厅时,请求用户信息(功能请求).我们对这些请求进行判断,执行对应的函数。

注意req获得的uri是我们再http服务器所发送的/ 后面的资源路径,它是不带/的

    void http_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string method = req.get_method();if (method == "POST" && uri == "/reg"){return reg(conn);}else if (method == "POST" && uri == "/login"){return login(conn);}else if (method == "GET" && uri == "/info"){return info(conn);}else{return file_handler(conn);}}

客户端向服务器发送websocket请求有两次,第一次是大厅获取用户信息成功之后,会发送建立大厅长连接请求,第二次是进入游戏房间页面之后,自动发送建立房间长连接请求,我们对此执行对应函数。

void wsopen_callback(websocketpp::connection_hdl hdl){// websocket长连接 建立成功之后 根据uri分辨是上面的哪一种websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建⽴了游戏⼤厅的⻓连接return wsopen_game_hall(conn);}else if (uri == "/room"){// 建⽴了游戏房间的⻓连接return wsopen_game_room(conn);}}

长连接关闭逻辑也同上

void wsclose_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建⽴了游戏⼤厅的⻓连接return wsclose_game_hall(conn);}else if (uri == "/room"){// 建⽴了游戏房间的⻓连接return wsclose_game_room(conn);}}

用户长连接消息请求有两种,一种是大厅发出的开始匹配请求,第二种是房间发出的下棋或者聊天请求。我们可以按照如下方式得到请求消息的Json::Value对象,注意这个与http请求获取正文内容的方式有所不同。

std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg){// websocket长连接通信处理回调函数// 1.判断是哪里的请求websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 游戏⼤厅⻓连接的消息return wsmsg_game_hall(conn, msg);}else if (uri == "/room"){return wsmsg_game_room(conn, msg);}}

总的服务器server.hpp

#pragma once#include "room.hpp"
#include "online.hpp"
#include "session.hpp"
#include "matcher.hpp"
#include "db.hpp"
#include <string>#define WWWROOT "./wwwroot/"// 用户先进行注册(ajax请求),然后(跳转)登录(ajax请求),然后(跳转)匹配大厅(ajax请求)
// 点击开始匹配 进入匹配队列,客户端需要隔一段时间就问一下是否匹配成功// websocket服务器可以返回http响应,con->setStatus,也可以返回websocket响应(直接send)
class gobang_server
{
private:std::string _web_root;   // 静态资源根⽬录 ./wwwroot/  ->./wwwroot/register.htmlwebsocket_server _wssrv; // websocket_server对象user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;// 静态网页的返回void file_handler(websocket_server::connection_ptr &conn){websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string file_path = WWWROOT + uri;// 如果请求路径是一个目录,则在路径后面加上loginif (file_path.back() == '/'){file_path += "login.html";}std::string body;bool ret = file_util::read(file_path, body);if (ret == false){std::string No_path = WWWROOT;No_path += "404.html";file_util::read(No_path, body);conn->set_status(websocketpp::http::status_code::not_found);conn->set_body(body);return;}// 5. 设置响应正⽂conn->set_body(body);conn->set_status(websocketpp::http::status_code::ok);}void http_resp(websocket_server::connection_ptr &conn, bool result,websocketpp::http::status_code::value code, const std::string &reason){Json::Value resp;resp["result"] = result;resp["reason"] = reason;std::string body;json_util::serialize(resp, body);conn->set_status(code);conn->append_header("Content-Type", "application/json");conn->set_body(body);return;}void reg(websocket_server::connection_ptr &conn){// ⽤⼾注册功能请求的处理websocketpp::http::parser::request req = conn->get_request();// 1. 获取到请求正⽂std::string req_body = conn->get_request_body();// 2. 对正⽂进⾏json反序列化,得到⽤⼾名和密码Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DBG_LOG("反序列化注册信息失败");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");}// 3. 进⾏数据库的⽤⼾新增操作if (login_info["username"].isNull() ||login_info["password"].isNull()){DBG_LOG("⽤⼾名密码不完整");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");}ret = _ut.insert(login_info);if (ret == false){DBG_LOG("向数据库插⼊数据失败");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "⽤⼾名已经被占⽤!");}// 如果成功了,则返回200return http_resp(conn, true, websocketpp::http::status_code::ok, "注册⽤⼾成功");}/*用户登录请求处理*/void login(websocket_server::connection_ptr &conn){// 1. 获取请求正⽂,并进⾏json反序列化,得到⽤⼾名和密码std::string req_body = conn->get_request_body();Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DBG_LOG("反序列化登录信息失败");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");}// 2. 校验正⽂完整性,进⾏数据库的⽤⼾信息验证if (login_info["username"].isNull() ||login_info["password"].isNull()){DBG_LOG("⽤⼾名密码不完整");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");}ret = _ut.login(login_info);if (ret == false){// 1. 如果验证失败,则返回400DBG_LOG("⽤⼾名密码错误");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "⽤⼾名密码错误");}// 如果创建成功,则创建一个会话,并通过set-cookie返回会话int uid = login_info["id"].asInt();session_ptr ssp = _sm.create_session(uid, LOGIN);if (ssp.get() == nullptr){DBG_LOG("创建会话失败");return http_resp(conn, false,websocketpp::http::status_code::internal_server_error, "创建会话失败");}_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 4. 设置响应头部:Set-Cookie,  将sessionid通过cookie返回std::string cookie_session_id = "SSID=" + std::to_string(ssp->ssid());conn->append_header("Set-Cookie", cookie_session_id);return http_resp(conn, true, websocketpp::http::status_code::ok,"登录成功");}bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val){// Cookie: SSID=XXX; path=/;   Cookie之间以;作为间隔,// 1. 我们对字符串进⾏分割,得到各个单个的cookie信息std::string sep = ";";std::vector<std::string> arr;string_util::split(cookie_str, sep, arr);for (auto str : arr){// 2. 对单个cookie字符串,以 = 为间隔进⾏分割,得到key和valstd::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2){continue;}if (tmp_arr[0] == key){val = tmp_arr[1];return true;}}return false;}// 用户会将 Cookie=abc 返回  先找cookie,再找cookie对应的SSID,再找SSID对应的会话,再找用户信息返回,然后设置会话过期时间void info(websocket_server::connection_ptr &conn){// ⽤⼾信息获取功能请求的处理Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){// 如果没有cookie,返回错误:没有cookie信息,让客⼾端重新登录return http_resp(conn, true,websocketpp::http::status_code::bad_request, "无cookie信息,请重新登录");}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false){// cookie中没有ssid,返回错误:没有ssid信息,让客⼾端重新登录return http_resp(conn, true,websocketpp::http::status_code::bad_request, "找不到cookie的对应ssid信息,请重新登录");}// 2. 在session管理中查找对应的会话信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr){// 没有找到session,则认为登录已经过期,需要重新登录return http_resp(conn, true,websocketpp::http::status_code::bad_request, "登录过期,请重新登录");}// 3. 从数据库中取出⽤⼾信息,进⾏序列化发送给客⼾端uint64_t uid = ssp->get_user();Json::Value user_info;ret = _ut.select_by_id(uid, user_info);if (ret == false){// 获取⽤⼾信息失败,返回错误:找不到⽤⼾信息return http_resp(conn, true,websocketpp::http::status_code::bad_request, "找不到⽤⼾信息,请重新登录");}std::string body;json_util::serialize(user_info, body);conn->set_body(body);conn->append_header("Content-Type", "application/json");conn->set_status(websocketpp::http::status_code::ok);// 4. 刷新session的过期时间_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}// 一般的回调函数传入一个websocket服务器和连接管理句柄(必须传),我们有this可以访问服务器// 通过  服务器和连接处理句柄  我们可以获取这个连接,这个连接被我们传入各个功能函数void http_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string method = req.get_method();if (method == "POST" && uri == "/reg"){return reg(conn);}else if (method == "POST" && uri == "/login"){return login(conn);}else if (method == "GET" && uri == "/info"){return info(conn);}else{return file_handler(conn);}}// 用户建立长连接之后,服务器使用send发送信息给客户端void ws_resp(websocket_server::connection_ptr conn, Json::Value &resp){std::string body;json_util::serialize(resp, body);conn->send(body);}// 封装从 客户端的cookie 获取session信息session_ptr get_session_by_cookie(websocket_server::connection_ptr conn){Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){err_resp["optype"] = "hall_ready";err_resp["result"] = false;err_resp["reason"] = "没有cookie信息,请重新登录";ws_resp(conn, err_resp);return session_ptr();}std::string value;bool ret = get_cookie_val(cookie_str, "SSID", value);if (ret == false){err_resp["optype"] = "hall_ready";err_resp["result"] = false;err_resp["reason"] = "cookie中没有用户会话信息,请重新登录";ws_resp(conn, err_resp);return session_ptr();}session_ptr ssp = _sm.get_session_by_ssid(std::stol(value));if (ssp.get() == nullptr){// 没有找到session,则认为登录已经过期,需要重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到session信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}return ssp;}void wsopen_game_hall(websocket_server::connection_ptr conn){// 游戏⼤厅⻓连接建⽴成功Json::Value resp_json;// 1. 登录验证--判断当前客⼾端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2. 判断当前客⼾端是否是重复登录if (_om.in_game_hall(ssp->get_user()) ||_om.in_game_room(ssp->get_user())){resp_json["optype"] = "hall_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3. 将当前客⼾端以及连接加⼊到游戏⼤厅,游戏大厅维护了用户id到连接的map_om.enter_game_hall(ssp->get_user(), conn);// 4. 给客⼾端响应游戏⼤厅连接建⽴成功resp_json["optype"] = "hall_ready";resp_json["reason"] = "游戏大厅进入成功!";resp_json["result"] = true;ws_resp(conn, resp_json);// 5. 记得将session设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);}// 逻辑:大厅中加入匹配队列,线程创建房间并返回前端match_success, 前端离开在线用户管理模块void wsopen_game_room(websocket_server::connection_ptr conn){// 1. 获取当前客户端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2.判断该用户是否在其它房间或者大厅中,如果是则出错Json::Value resp_json;if (_om.in_game_hall(ssp->get_user()) || _om.in_game_room(ssp->get_user())){resp_json["optype"] = "room_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3.判断当前用户是否创建好房间room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "room_ready";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 4. 将当前⽤⼾添加到在线⽤⼾管理的游戏房间中_om.enter_game_room(ssp->get_user(), conn);// 5. 将session重新设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);// 6. 向前端回复房间准备完毕resp_json["optype"] = "room_ready";resp_json["result"] = true;resp_json["room_id"] = (Json::UInt64)rp->id();resp_json["uid"] = ssp->get_user();resp_json["white_id"] = rp->get_white_user();resp_json["black_id"] = rp->get_black_user();return ws_resp(conn, resp_json);}// 长连接建立有两种,第一种是进入匹配队列,第二种是进入游戏房间的时候void wsopen_callback(websocketpp::connection_hdl hdl){// websocket长连接 建立成功之后 根据uri分辨是上面的哪一种websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建⽴了游戏⼤厅的⻓连接return wsopen_game_hall(conn);}else if (uri == "/room"){// 建⽴了游戏房间的⻓连接return wsopen_game_room(conn);}}// 玩家离开掉网页之后,会发送一个关掉网页连接的请求,调用该函数void wsclose_game_hall(websocket_server::connection_ptr conn){// 游戏⼤厅⻓连接断开的处理// 1. 登录验证--判断当前客⼾端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 将玩家从游戏⼤厅中移除_om.exit_game_hall(ssp->get_user());// 2. 将session恢复⽣命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}void wsclose_game_room(websocket_server::connection_ptr conn){// 获取会话信息,识别客⼾端session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 将玩家从在线⽤⼾管理中移除_om.exit_game_room(ssp->get_user());// 2. 将session回复⽣命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 3. 将玩家从游戏房间中移除,房间中所有⽤⼾退出了就会销毁房间_rm.remove_room_by_user(ssp->get_user());}void wsclose_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建⽴了游戏⼤厅的⻓连接return wsclose_game_hall(conn);}else if (uri == "/room"){// 建⽴了游戏房间的⻓连接return wsclose_game_room(conn);}}// 玩家进入大厅建立长连接,同时玩家开始/停止匹配请求时 调用该函数void wsmsg_game_hall(websocket_server::connection_ptr conn, websocket_server::message_ptr msg){Json::Value resp_json;// 1. ⾝份验证,当前客⼾端到底是哪个玩家session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return; // get_session_by_cookie内部已经返回了错误响应}// 2. 获取请求信息Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["result"] = false;resp_json["reason"] = "请求信息解析失败";return ws_resp(conn, resp_json);}// 3.对请求进行处理if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){// 开始对战匹配:通过匹配模块,将⽤⼾添加到匹配队列中_mm.add(ssp->get_user());resp_json["optype"] = "match_start";resp_json["result"] = true;return ws_resp(conn, resp_json);}else if (!req_json["optype"].isNull() &&req_json["optype"].asString() == "match_stop"){// 停⽌对战匹配:通过匹配模块,将⽤⼾从匹配队列中移除_mm.del(ssp->get_user());resp_json["optype"] = "match_stop";resp_json["result"] = true;return ws_resp(conn, resp_json);}resp_json["optype"] = "unknow";resp_json["reason"] = "请求类型未知";resp_json["result"] = false;return ws_resp(conn, resp_json);}void wsmsg_game_room(websocket_server::connection_ptr conn, websocket_server::message_ptr msg){// 进入房间页面,建立房间的长连接Json::Value resp_json;// 1. 获取当前客⼾端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){DBG_LOG("房间-没有找到会话信息");return;}// 2. 获取客⼾端房间信息room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "unknow";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;DBG_LOG("房间-没有找到玩家房间信息");return ws_resp(conn, resp_json);}// 3. 对消息进⾏反序列化Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["optype"] = "unknow";resp_json["reason"] = "请求解析失败";resp_json["result"] = false;DBG_LOG("房间-反序列化请求失败");return ws_resp(conn, resp_json);}DBG_LOG("房间:收到房间请求,开始处理....");// 4. 通过房间模块进⾏消息请求的处理return rp->handle_request(req_json);}void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg){// websocket长连接通信处理回调函数// 1.判断是哪里的请求websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 游戏⼤厅⻓连接的消息return wsmsg_game_hall(conn, msg);}else if (uri == "/room"){return wsmsg_game_room(conn, msg);}}public:/*进⾏成员初始化,以及服务器回调函数的设置*/gobang_server(const std::string &host,const std::string &user,const std::string &pass,const std::string &dbname,uint16_t port = 3306,const std::string &wwwroot = WWWROOT) : _web_root(wwwroot), _ut(host, user, pass, dbname, port),_rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om){_wssrv.set_access_channels(websocketpp::log::alevel::none);_wssrv.init_asio();_wssrv.set_reuse_addr(true);_wssrv.set_http_handler(std::bind(&gobang_server::http_callback,this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback,this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this,std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this,std::placeholders::_1, std::placeholders::_2));}/*启动服务器*/void start(int port){_wssrv.listen(port);_wssrv.start_accept();_wssrv.run();}
};

主函数gobang.cc

#include "room.hpp"
#include"session.hpp"#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASSWD "123456"
#define DBNAME "gobang"
#include"matcher.hpp"
#include"server.hpp"int main()
{// user_table ut(HOST, USER, PASSWD, DBNAME, PORT);// match_queue<int> mq;// online_manager om;// room_manager rm(&ut,&om);// matcher mt(&rm,&ut,&om);gobang_server s(HOST, USER, PASSWD, DBNAME, PORT);s.start(7080);return 0;
}

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

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

相关文章

LabVIEW 与 PLC 通讯的常见方式

在工业自动化和数据采集系统中&#xff0c;PLC&#xff08;可编程逻辑控制器&#xff09; 广泛用于控制和监测各种设备&#xff0c;而 LabVIEW 作为强大的图形化编程工具&#xff0c;常用于上位机数据处理和可视化。为了实现 LabVIEW 与 PLC 的高效通讯&#xff0c;常见的方法包…

2025 polarctf春季个人挑战赛web方向wp

来个弹窗 先用最基础的xss弹窗试一下 <script>alert("xss")</script>没有内容&#xff0c;猜测过滤了script&#xff0c;双写绕过一下 <scrscriptipt>alert("xss")</scscriptript>background 查看网页源代码 查看一下js文件 类…

【Ai】--- 可视化 DeepSeek-r1 接入 Open WebUI(超详细)

在编程的艺术世界里,代码和灵感需要寻找到最佳的交融点,才能打造出令人为之惊叹的作品。而在这座秋知叶i博客的殿堂里,我们将共同追寻这种完美结合,为未来的世界留下属于我们的独特印记。【Ai】--- 可视化 DeepSeek-r1 接入 Open WebUI(超详细) 开发环境一、前情提要:你…

Redis--redis客户端

目录 一、引言 二、数据库管理命令 三、redis客户端 四、Java客户端使用Redis 五、相关命令使用 1.get&#xff0c;set 2.exists&#xff0c;del 3.keys 4.expire&#xff0c;ttl 六、总结 一、引言 在之前学了redis相关类型命令之后&#xff0c;本篇文章&#xff0c;…

SpringBoot3.0不建议使用spring.factories,使用AutoConfiguration.imports新的自动配置方案

文章目录 一、写在前面二、使用imports文件1、使用2、示例比对3、完整示例 参考资料 一、写在前面 spring.factories是一个位于META-INF/目录下的配置文件&#xff0c;它基于Java的SPI(Service Provider Interface)机制的变种实现。 这个文件的主要功能是允许开发者声明接口的…

鸿蒙特效教程10-卡片展开/收起效果

鸿蒙特效教程10-卡片展开/收起效果 在移动应用开发中&#xff0c;卡片是一种常见且实用的UI元素&#xff0c;能够将信息以紧凑且易于理解的方式呈现给用户。 本教程将详细讲解如何在HarmonyOS中实现卡片的展开/收起效果&#xff0c;通过这个实例&#xff0c;你将掌握ArkUI中状…

hn航空app hnairSign unidbg 整合Springboot

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 学习unidbg补环境。先弄一个…

奇怪的异形选项卡样式、弧形边框选项卡

<template><div :class"$options.name"><div class"tab">默认选项卡</div><div class"tab" active>选中选项卡</div><el-divider /><el-tabs v-model"tabActiveName" tab-click"(t…

特殊行车记录仪DAT视频丢失的恢复方法

行车记录仪是一种常见的车载记录仪&#xff0c;和常见的“小巧玲珑”的行车记录仪不同&#xff0c;一些特种车辆使用的记录仪的外观可以用“笨重”来形容。下边我们来看看特种车载行车记录仪删除文件后的恢复方法。 故障存储: 120GB存储设备/文件系统:exFAT /簇大小:128KB 故…

SQL Server 2022 安装问题

一、安装与配置问题 1. SQL Server 2022 安装失败怎么办&#xff1f; 常见原因&#xff1a; 硬件或操作系统不满足最低要求&#xff08;如内存、磁盘空间不足&#xff09;。未关闭防火墙或杀毒软件。之前版本的 SQL Server 残留文件未清理。 解决方案&#xff1a; 确保硬件配…

解锁 AWX+Ansible 自动化运维新体验:快速部署实战

Ansible 和 AWX 是自动化运维领域的强大工具组合。Ansible 是一个简单高效的 IT 自动化工具&#xff0c;而 AWX 则是 Ansible 的开源 Web 管理平台&#xff0c;提供图形化界面来管理 Ansible 任务。本指南将带你一步步在 Ubuntu 22.04 上安装 Ansible 和 AWX&#xff0c;使用 M…

【xiaozhi赎回之路-2:语音可以自己配置就是用GPT本地API】

固件作用 打通了网络和硬件的沟通 修改固件实现【改变连接到小智服务器的】 回答逻辑LLM自定义 自定义了Coze&#xff08;比较高级&#xff0c;自定义程度比较高&#xff0c;包括知识库&#xff0c;虚拟脚色-恋人-雅思老师-娃娃玩具{可能需要使用显卡对开源模型进行微调-产…

(UI自动化测试web端)第二篇:元素定位的方法_xpath扩展(工作当中用的比较多)

看代码里的【driver.find_element_by_xpath( )】()里的路径怎么写&#xff1f; xpath元素定位有多种写法&#xff0c;那我们现在说的就是在元素定位时&#xff0c;根据网页的实际情况来选择适合的xpath元素定位的写法。信我 &#xff0c;你真正工作当中每个都有用的&#xff01…

[已解决]服务器CPU突然飙高98%----Java程序OOM问题 (2024.9.5)

目录 问题描述问题排查问题解决参考资料 问题描述 业主单位服务器自8月29日晚上21:00起CPU突然飙高至98%&#xff0c;内存爆满&#xff0c;一直到9月5日&#xff1a; 问题排查 ①执行 top 命令查看Java进程PID top②执行top -Hp PID 命令查看具体的线程情况 top -Hp 3058输入上…

OSI模型_TCP/IP模型_五层模型

文章目录 OSI模型_TCP/IP模型_五层模型模型对比模型层级对比关键区别对比 OSI模型OSI模型概述举例说明流程图示 TCP/IP 四层模型模型结构举例说明流程图示 TCP/IP 五层模型模型的结构举例说明流程图示 OSI模型_TCP/IP模型_五层模型 学OSI&#xff0c;用TCP/IP&#xff0c;分析选…

R语言——字符串

参考资料&#xff1a;学习R 文本数据存储在字符向量中。重要的是&#xff0c;字符向量中的每个元素都是字符串&#xff0c;而非单独的字符。 文本的基本单位是字符向量&#xff0c;着意味着大部分字符串处理函数也能用于字符向量。 1、创建和打印字符串 字符向量可用c函数创建…

如何区别在Spring Boot 2 和 Spring Boot 3 中使用 Knife4j:集成与配置指南

在现代的 Web 开发中&#xff0c;API 文档是不可或缺的一部分。Knife4j 是基于 Swagger 的增强工具&#xff0c;它不仅提供了更友好的 API 文档界面&#xff0c;还支持更多实用的功能&#xff0c;如离线文档导出、全局参数配置等。本文将详细介绍如何在 Spring Boot 2 和 Sprin…

C++可变参数

可变参数C风格的可变参数C风格可变参数的使用 C11可变参数模板递归展开参数包参数列表展开折叠表达式 STL中的emplace插入接口 可变参数 C风格的可变参数 可变参数是一种语言特性&#xff0c;可以在函数声明中使用省略号...来表示函数接受可变数量的参数。 例如典型的printf…

数据库的操作,以及sql之DML

首先&#xff0c;创建表以及插入数据 create table t_text(id int primary key auto_increment,name varchar(20) unique not null,gender char(5) not null check(gender in ("男","女")),deed varchar(255) not null default "事例不详"); in…

vue2前端日志数据存储(indexedD)自动清理3天前的数据

前言&#xff1a;关于Dexie.js这个前端本地数据库&#xff0c;如何使用IndexedDB来存储数据&#xff0c;并且设置到期自动清理的机制。首先&#xff0c;我需要回忆一下Dexie.js的基本用法&#xff0c;以及IndexedDB的特性。IndexedDB是浏览器中的一种非关系型数据库&#xff0c…