跳到主要内容
C++ 在线五子棋对战项目网页版开发详解 | 极客日志
C++ 大前端 算法
C++ 在线五子棋对战项目网页版开发详解 综述由AI生成 介绍基于 C++、WebSocketpp 和 MySQL 开发的在线五子棋对战项目。涵盖 WebSocket 通信原理、数据库设计、Session 管理、房间匹配逻辑及前后端联调流程。解决了 MySQL 8.0 密码验证兼容性问题,实现了用户登录、大厅匹配、房间对战等功能。
安卓系统 发布于 2026/3/27 更新于 2026/5/27 29 浏览C++ 在线五子棋对战项目网页版开发详解
1. WebSocket 通信基础
1.1. WebSocket 简介
WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。
传统的 Web 程序属于 "一问一答" 形式,客户端发送 HTTP 请求,服务器返回 HTTP 响应。服务器处于被动方。
即时聊天或五子棋游戏依赖 "消息推送",需要服务器主动推送消息。原生 HTTP 协议需通过 "轮询" 实现,成本高且不及时。
WebSocket 更接近 TCP 级别,连接建立后客户端或服务器均可主动向对方发送数据。
1.2. 协议切换过程
WebSocketpp 协议由 HTTP 协议切换过来。客户端发送协议切换请求,服务端响应同意切换,形成 WebSocket 通信连接。
1.3. websocketpp 库常用接口与搭建流程
搭建流程:
实例化 server 对象
设置日志输出等级
初始化 asio 框架调度器
设置业务处理回调函数
设置服务器监听端口
开始获取新建连接
启动服务器
代码示例:
#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
typedef websocketpp::server<websocketpp::config::asio> wsserver_t ;
void print (const std::string &str) {
std::cout << str << std::endl;
}
void http_callback (wsserver_t *srv, websocketpp::connection_hdl hdl) {
wsserver_t ::connection_ptr conn = srv->get_con_from_hdl (hdl);
std::string body = "<html><body><h1>Hello World</h1></body></html>" ;
conn->set_body (body);
conn->append_header ("Content-Type" , );
conn-> (websocketpp::http::status_code::ok);
}
{
std::cout << ;
}
{
std::cout << ;
}
{
::connection_ptr conn = srv-> (hdl);
std::string rsp = + msg-> ();
conn-> (rsp, websocketpp::frame::opcode::text);
}
{
wssrv;
wssrv. (websocketpp::log::alevel::none);
wssrv. ();
wssrv. ( );
wssrv. (std:: (http_callback, &wssrv, std::placeholders::_1));
wssrv. (std:: (wsopen_callback, &wssrv, std::placeholders::_1));
wssrv. (std:: (wsclose_callback, &wssrv, std::placeholders::_1));
wssrv. (std:: (wsmsg_callback, &wssrv, std::placeholders::_1, std::placeholders::_2));
wssrv. ( );
wssrv. ();
wssrv. ();
;
}
"text/html"
set_status
void wsopen_callback (wsserver_t *srv, websocketpp::connection_hdl hdl)
"websocket 握手成功!!\n"
void wsclose_callback (wsserver_t *srv, websocketpp::connection_hdl hdl)
"websocket 连接断开!!\n"
void wsmsg_callback (wsserver_t *srv, websocketpp::connection_hdl hdl, wsserver_t ::message_ptr msg)
wsserver_t
get_con_from_hdl
"client say: "
get_payload
send
int main ()
wsserver_t
set_access_channels
init_asio
set_reuse_addr
true
set_http_handler
bind
set_open_handler
bind
set_close_handler
bind
set_message_handler
bind
listen
8085
start_accept
run
return
0
2. MySQL 数据库操作
2.1. mysqlclient 库接口认识
初始化 mysql 操作句柄
连接 mysql 服务器
设置客户端字符集
选择要操作的数据库
执行 SQL 语句(增删改无需反馈结果,查询需保存结果)
获取查询结果集中的条数
遍历获取每一条数据
释放结果集和句柄
#include <stdio.h>
#include <string.h>
#include <mysql/mysql.h>
#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASS "password"
#define DBNAME "gobang"
int main () {
MYSQL *mysql = mysql_init (NULL );
if (mysql == NULL ) { printf ("mysql init failed!\n" ); return -1 ; }
if (mysql_real_connect (mysql, HOST, USER, PASS, DBNAME, PORT, NULL , 0 ) == NULL ) {
printf ("connect mysql server failed : %s\n" , mysql_error (mysql));
mysql_close (mysql); return -1 ;
}
if (mysql_set_character_set (mysql, "utf8" ) != 0 ) {
printf ("set client character failed : %s\n" , mysql_error (mysql));
mysql_close (mysql); return -1 ;
}
char *sql = "select * from stu;" ;
int ret = mysql_query (mysql, sql);
if (ret != 0 ) { printf ("mysql query failed : %s\n" , mysql_error (mysql)); mysql_close (mysql); return -1 ; }
MYSQL_RES *res = mysql_store_result (mysql);
if (res == NULL ) { mysql_close (mysql); return -1 ; }
int num_row = mysql_num_rows (res);
for (int i = 0 ; i < num_row; i++) {
MYSQL_ROW row = mysql_fetch_row (res);
}
mysql_free_result (res);
mysql_close (mysql);
return 0 ;
}
3. 项目模块划分
4. 项目流程图
4.1. 玩家用户角度流程图
4.2. 服务器流程结构图
5. 实用工具模块
5.1. 日志实现 #ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include <stdio.h>
#include <time.h>
#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL INF
#define LOG(level, format, ...) do{\
if (DEFAULT_LOG_LEVEL > level) break;\
time_t t = time(NULL);\
struct tm *lt = localtime(&t);\
char buf[32] = {0};\
strftime(buf, 31, "%H:%M:%S" , lt);\
fprintf(stdout, "[%s %s:%d] " format "\n" , buf, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)
#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
#endif
5.2. MySQL 工具类 class mysql_util {
public :
static MYSQL *mysql_create (const std::string &host, const std::string &username,
const std::string &password, const std::string &dbname, uint16_t port = 3306 ) {
MYSQL *mysql = mysql_init (NULL );
if (mysql == NULL ) { ELOG ("mysql init failed!" ); return NULL ; }
if (mysql_real_connect (mysql, host.c_str (), username.c_str (), password.c_str (), dbname.c_str (), port, NULL , 0 ) == NULL ) {
ELOG ("connect mysql server failed : %s" , mysql_error (mysql));
mysql_close (mysql); return NULL ;
}
if (mysql_set_character_set (mysql, "utf8" ) != 0 ) {
ELOG ("set client character failed : %s" , mysql_error (mysql));
mysql_close (mysql); return NULL ;
}
return mysql;
}
static bool mysql_exec (MYSQL *mysql, const std::string &sql) {
int ret = mysql_query (mysql, sql.c_str ());
if (ret != 0 ) { ELOG ("%s\n" , sql.c_str ()); ELOG ("mysql query failed : %s\n" , mysql_error (mysql)); return false ; }
return true ;
}
static void mysql_destroy (MYSQL *mysql) {
if (mysql != NULL ) { mysql_close (mysql); } return ;
}
};
5.3. JSON 工具类 class json_util {
public :
static bool serialize (const Json::Value &root, std::string &str) {
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter>sw (swb.newStreamWriter ());
std::stringstream ss;
int ret = sw->write (root, &ss);
if (ret != 0 ) { ELOG ("json serialize failed!!" ); return false ; }
str = ss.str (); return true ;
}
static bool unserialize (const std::string &str, Json::Value &root) {
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr (crb.newCharReader()) ;
std::string err;
bool ret = cr->parse (str.c_str (), str.c_str () + str.size (), &root, &err);
if (ret == false ) { ELOG ("json unserialize failed: %s" , err.c_str ()); return false ; }
return true ;
}
};
5.4. String 工具类 class string_util {
public :
static int split (const std::string &src, const std::string &sep, std::vector<std::string> &res) {
size_t pos, idx = 0 ;
while (idx < src.size ()) {
pos = src.find (sep, idx);
if (pos == std::string::npos) { res.push_back (src.substr (idx)); break ; }
if (pos == idx) { idx += sep.size (); continue ; }
res.push_back (src.substr (idx, pos - idx));
idx = pos + sep.size ();
}
return res.size ();
}
};
5.5. File 工具类 class file_util {
public :
static bool read (const std::string &filename, std::string &body) {
std::ifstream ifs (filename, std::ios::binary) ;
if (ifs.is_open () == false ) { ELOG ("%s file open failed!!" , filename.c_str ()); return false ; }
size_t fsize = 0 ;
ifs.seekg (0 , std::ios::end);
fsize = ifs.tellg ();
ifs.seekg (0 , std::ios::beg);
body.resize (fsize);
ifs.read (&body[0 ], fsize);
if (ifs.good () == false ) { ELOG ("read %s file content failed!" , filename.c_str ()); ifs.close (); return false ; }
ifs.close (); return true ;
}
};
6. 用户信息表设计 drop database if exists gobang;
create database if not exists gobang;
use gobang;
create table if not exists user (
id int primary key auto_increment,
username varchar (32 ) unique key not null ,
password varchar (128 ) not null ,
score int ,
total_count int ,
win_count int
);
7. 数据管理模块类设计
7.1. 整体思路 封装数据库操作,提供用户注册、登录、信息查询、胜负更新等功能。
7.2. 代码实现 #ifndef __M_DB_H__
#define __M_DB_H__
#include "util.hpp"
#include <mutex>
#include <cassert>
class user_table {
private :
MYSQL *_mysql;
std::mutex _mutex;
public :
user_table (const std::string &host, const std::string &username, const std::string &password,
const std::string &dbname, uint16_t port = 3306 ) {
_mysql = mysql_util::mysql_create (host, username, password, dbname, port);
assert (_mysql != NULL );
}
~user_table () { mysql_util::mysql_destroy (_mysql); _mysql = NULL ; }
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 ()) { DLOG ("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 ) { DLOG ("insert user info failed!!\n" ); return false ; }
return true ;
}
bool login (Json::Value &user) {
if (user["password" ].isNull () || user["username" ].isNull ()) { DLOG ("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::unique_lock<std::mutex> lock (_mutex) ;
bool ret = mysql_util::mysql_exec (_mysql, sql);
if (ret == false ) { DLOG ("user login failed!!\n" ); return false ; }
res = mysql_store_result (_mysql);
if (res == NULL ) { DLOG ("have no login user info!!" ); return false ; }
}
int row_num = mysql_num_rows (res);
if (row_num != 1 ) { DLOG ("the user information queried is not unique!!" ); return false ; }
MYSQL_ROW row = mysql_fetch_row (res);
user["id" ] = (Json::UInt64)std::stol (row[0 ]);
user["score" ] = (Json::UInt64)std::stol (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_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 ) { DLOG ("get user by name failed!!\n" ); return false ; }
res = mysql_store_result (_mysql);
if (res == NULL ) { DLOG ("have no user info!!" ); return false ; }
}
int row_num = mysql_num_rows (res);
if (row_num != 1 ) { DLOG ("the user information queried is not unique!!" ); return false ; }
MYSQL_ROW row = mysql_fetch_row (res);
user["id" ] = (Json::UInt64)std::stol (row[0 ]);
user["username" ] = name;
user["score" ] = (Json::UInt64)std::stol (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 (uint64_t id, Json::Value &user) {
#define USER_BY_ID "select username, score, total_count, win_count from user where id=%ld;"
char sql[4096 ] = {0 };
sprintf (sql, USER_BY_ID, id);
MYSQL_RES *res = NULL ;
{
std::unique_lock<std::mutex> lock (_mutex) ;
bool ret = mysql_util::mysql_exec (_mysql, sql);
if (ret == false ) { DLOG ("get user by id failed!!\n" ); return false ; }
res = mysql_store_result (_mysql);
if (res == NULL ) { DLOG ("have no user info!!" ); return false ; }
}
int row_num = mysql_num_rows (res);
if (row_num != 1 ) { DLOG ("the user information queried is not unique!!" ); return false ; }
MYSQL_ROW row = mysql_fetch_row (res);
user["id" ] = (Json::UInt64)id;
user["username" ] = row[0 ];
user["score" ] = (Json::UInt64)std::stol (row[1 ]);
user["total_count" ] = std::stoi (row[2 ]);
user["win_count" ] = std::stoi (row[3 ]);
mysql_free_result (res);
return true ;
}
bool win (uint64_t id) {
#define USER_WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%ld;"
char sql[4096 ] = {0 };
sprintf (sql, USER_WIN, id);
bool ret = mysql_util::mysql_exec (_mysql, sql);
if (ret == false ) { DLOG ("update win user info failed!!\n" ); return false ; }
return true ;
}
bool lose (uint64_t id) {
#define USER_LOSE "update user set score=score-30, total_count=total_count+1 where id=%ld;"
char sql[4096 ] = {0 };
sprintf (sql, USER_LOSE, id);
bool ret = mysql_util::mysql_exec (_mysql, sql);
if (ret == false ) { DLOG ("update lose user info failed!!\n" ); return false ; }
return true ;
}
};
#endif
8. 在线用户管理设计思想 维护游戏大厅和游戏房间的用户 ID 与通信连接的关系。
#ifndef __M_ONLINE_H__
#define __M_ONLINE_H__
#include "util.hpp"
#include <mutex>
#include <unordered_map>
class online_manager {
private :
std::mutex _mutex;
std::unordered_map<uint64_t , wsserver_t ::connection_ptr> _hall_user;
std::unordered_map<uint64_t , wsserver_t ::connection_ptr> _room_user;
public :
void enter_game_hall (uint64_t uid, wsserver_t ::connection_ptr &conn) {
std::unique_lock<std::mutex> lock (_mutex) ;
_hall_user.insert (std::make_pair (uid, conn));
}
void enter_game_room (uint64_t uid, wsserver_t ::connection_ptr &conn) {
std::unique_lock<std::mutex> lock (_mutex) ;
_room_user.insert (std::make_pair (uid, conn));
}
void exit_game_hall (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
_hall_user.erase (uid);
}
void exit_game_room (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
_room_user.erase (uid);
}
bool is_in_game_hall (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _hall_user.find (uid);
if (it == _hall_user.end ()) { return false ; }
return true ;
}
bool is_in_game_room (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _room_user.find (uid);
if (it == _room_user.end ()) { return false ; }
return true ;
}
wsserver_t ::connection_ptr get_conn_from_hall (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _hall_user.find (uid);
if (it == _hall_user.end ()) { return wsserver_t ::connection_ptr (); }
return it->second;
}
wsserver_t ::connection_ptr get_conn_from_room (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _room_user.find (uid);
if (it == _room_user.end ()) { return wsserver_t ::connection_ptr (); }
return it->second;
}
};
#endif
9. 游戏房间的设计思想 #ifndef __M_ROOM_H__
#define __M_ROOM_H__
#include "util.hpp"
#include "logger.hpp"
#include "online.hpp"
#include "db.hpp"
#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2
typedef enum { GAME_START, GAME_OVER } room_statu;
class room {
private :
uint64_t _room_id;
room_statu _statu;
int _player_count;
uint64_t _white_id;
uint64_t _black_id;
user_table *_tb_user;
online_manager *_online_user;
std::vector<std::vector<int >> _board;
private :
bool five (int row, int col, int row_off, int col_off, int color) {
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 );
}
uint64_t check_win (int row, int col, int color) {
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)) {
return color == CHESS_WHITE ? _white_id : _black_id;
}
return 0 ;
}
public :
room (uint64_t room_id, user_table *tb_user, online_manager *online_user):
_room_id(room_id), _statu(GAME_START), _player_count(0 ), _tb_user(tb_user), _online_user(online_user),
_board(BOARD_ROW, std::vector <int >(BOARD_COL, 0 )){
DLOG ("%lu 房间创建成功!!" , _room_id);
}
~room () { DLOG ("%lu 房间销毁成功!!" , _room_id); }
uint64_t id () { return _room_id; }
room_statu statu () { return _statu; }
int player_count () { return _player_count; }
void add_white_user (uint64_t uid) { _white_id = uid; _player_count++; }
void add_black_user (uint64_t uid) { _black_id = uid; _player_count++; }
uint64_t get_white_user () { return _white_id; }
uint64_t get_black_user () { return _black_id; }
Json::Value handle_chess (Json::Value &req) {
Json::Value json_resp = req;
int chess_row = req["row" ].asInt ();
int chess_col = req["col" ].asInt ();
uint64_t cur_uid = req["uid" ].asUInt64 ();
if (_online_user->is_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->is_in_game_room (_black_id) == false ) {
json_resp["result" ] = true ; json_resp["reason" ] = "运气真好!对方掉线,不战而胜!" ;
json_resp["winner" ] = (Json::UInt64)_white_id; return json_resp;
}
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;
uint64_t 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 (Json::Value &req) {
Json::Value json_resp = req;
std::string msg = req["message" ].asString ();
size_t pos = msg.find ("垃圾" );
if (pos != std::string::npos) {
json_resp["result" ] = false ; json_resp["reason" ] = "消息中包含敏感词,不能发送!" ; return json_resp;
}
json_resp["result" ] = true ; return json_resp;
}
void handle_exit (uint64_t uid) {
Json::Value json_resp;
if (_statu == GAME_START) {
uint64_t winner_id = (Json::UInt64)(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" ] = (Json::UInt64)uid; json_resp["row" ] = -1 ; json_resp["col" ] = -1 ;
json_resp["winner" ] = (Json::UInt64)winner_id;
uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
_tb_user->win (winner_id); _tb_user->lose (loser_id); _statu = GAME_OVER;
broadcast (json_resp);
}
_player_count--; return ;
}
void handle_request (Json::Value &req) {
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);
}
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); _statu = 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);
DLOG ("房间 - 广播动作:%s" , body.c_str ());
return broadcast (json_resp);
}
void broadcast (Json::Value &rsp) {
std::string body;
json_util::serialize (rsp, body);
wsserver_t ::connection_ptr wconn = _online_user->get_conn_from_room (_white_id);
if (wconn.get () != nullptr ) { wconn->send (body); } else { DLOG ("房间 - 白棋玩家连接获取失败" ); }
wsserver_t ::connection_ptr bconn = _online_user->get_conn_from_room (_black_id);
if (bconn.get () != nullptr ) { bconn->send (body); } else { DLOG ("房间 - 黑棋玩家连接获取失败" ); }
return ;
}
};
10. 房间管理的设计思想 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;
std::unordered_map<uint64_t , uint64_t > _users;
public :
room_manager (user_table *ut, online_manager *om): _next_rid(1 ), _tb_user(ut), _online_user(om) {
DLOG ("房间管理模块初始化完毕!" );
}
~room_manager () { DLOG ("房间管理模块即将销毁!" ); }
room_ptr create_room (uint64_t uid1, uint64_t uid2) {
if (_online_user->is_in_game_hall (uid1) == false ) { DLOG ("用户:%lu 不在大厅中,创建房间失败!" , uid1); return room_ptr (); }
if (_online_user->is_in_game_hall (uid2) == false ) { DLOG ("用户:%lu 不在大厅中,创建房间失败!" , uid2); return room_ptr (); }
std::unique_lock<std::mutex> lock (_mutex) ;
room_ptr rp (new room(_next_rid, _tb_user, _online_user)) ;
rp->add_white_user (uid1); rp->add_black_user (uid2);
_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++;
return rp;
}
room_ptr get_room_by_rid (uint64_t rid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto it = _rooms.find (rid);
if (it == _rooms.end ()) { return room_ptr (); }
return it->second;
}
room_ptr get_room_by_uid (uint64_t uid) {
std::unique_lock<std::mutex> lock (_mutex) ;
auto uit = _users.find (uid);
if (uit == _users.end ()) { return room_ptr (); }
uint64_t rid = uit->second;
auto rit = _rooms.find (rid);
if (rit == _rooms.end ()) { return room_ptr (); }
return rit->second;
}
void remove_room (uint64_t rid) {
room_ptr rp = get_room_by_rid (rid);
if (rp.get () == nullptr ) { return ; }
uint64_t uid1 = rp->get_white_user ();
uint64_t uid2 = rp->get_black_user ();
std::unique_lock<std::mutex> lock (_mutex) ;
_users.erase (uid1); _users.erase (uid2);
_rooms.erase (rid);
}
void remove_room_user (uint64_t uid) {
room_ptr rp = get_room_by_uid (uid);
if (rp.get () == nullptr ) { return ; }
rp->handle_exit (uid);
if (rp->player_count () == 0 ) { remove_room (rp->id ()); }
return ;
}
};
11. Session 基础了解 Session 用于维持用户登录状态,配合 Cookie 使用。WebSocketpp 支持定时器功能。
12. Session 类设计 typedef enum {UNLOGIN, LOGIN} ss_statu;
class session {
private :
uint64_t _ssid;
uint64_t _uid;
ss_statu _statu;
wsserver_t ::timer_ptr _tp;
public :
session (uint64_t ssid): _ssid(ssid){ DLOG ("SESSION %p 被创建!!" , this ); }
~session () { DLOG ("SESSION %p 被释放!!" , this ); }
uint64_t ssid () { return _ssid; }
void set_statu (ss_statu statu) { _statu = statu; }
void set_user (uint64_t uid) { _uid = uid; }
uint64_t get_user () { return _uid; }
bool is_login () { return (_statu == LOGIN); }
void set_timer (const wsserver_t ::timer_ptr &tp) { _tp = tp;}
wsserver_t ::timer_ptr& get_timer () { return _tp; }
};
13. Session 管理类设计 #define SESSION_TIMEOUT 30000
#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;
wsserver_t *_server;
public :
session_manager (wsserver_t *srv): _next_ssid(1 ), _server(srv){ DLOG ("session 管理器初始化完毕!" ); }
~session_manager () { DLOG ("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);
_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);
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);
}
void set_session_expire_time (uint64_t ssid, int ms) {
session_ptr ssp = get_session_by_ssid (ssid);
if (ssp.get () == nullptr ) { return ; }
wsserver_t ::timer_ptr tp = ssp->get_timer ();
if (tp.get () == nullptr && ms == SESSION_FOREVER) { return ; }
else if (tp.get () == nullptr && ms != SESSION_FOREVER) {
wsserver_t ::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) {
tp->cancel ();
ssp->set_timer (wsserver_t ::timer_ptr ());
_server->set_timer (0 , std::bind (&session_manager::append_session, this , ssp));
}
else if (tp.get () != nullptr && ms != SESSION_FOREVER) {
tp->cancel ();
ssp->set_timer (wsserver_t ::timer_ptr ());
_server->set_timer (0 , std::bind (&session_manager::append_session, this , ssp));
wsserver_t ::timer_ptr tmp_tp = _server->set_timer (ms, std::bind (&session_manager::remove_session, this , ssp->ssid ()));
ssp->set_timer (tmp_tp);
}
}
};
14. 游戏对战匹配设计思想 基于队列的匹配机制,根据分数将用户分为普通、高手、大神三个队列。
14.1. 匹配队列设计 template <class T > class match_queue {
private :
std::list<T> _list;
std::mutex _mutex;
std::condition_variable _cond;
public :
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 () == true ) { return false ; } data = _list.front (); _list.pop_front (); return true ; }
void remove (T &data) { std::unique_lock<std::mutex> lock (_mutex) ; _list.remove (data); }
};
15. 游戏对战匹配管理设计 class matcher {
private :
match_queue<uint64_t > _q_normal;
match_queue<uint64_t > _q_high;
match_queue<uint64_t > _q_super;
std::thread _th_normal;
std::thread _th_high;
std::thread _th_super;
room_manager *_rm;
user_table *_ut;
online_manager *_om;
private :
void handle_match (match_queue<uint64_t > &mq) {
while (1 ) {
while (mq.size () < 2 ) { mq.wait (); }
uint64_t uid1, uid2;
bool ret = mq.pop (uid1);
if (ret == false ) { continue ; }
ret = mq.pop (uid2);
if (ret == false ) { this ->add (uid1); continue ; }
wsserver_t ::connection_ptr conn1 = _om->get_conn_from_hall (uid1);
if (conn1. get () == nullptr ) { this ->add (uid2); continue ; }
wsserver_t ::connection_ptr conn2 = _om->get_conn_from_hall (uid2);
if (conn2. get () == nullptr ) { this ->add (uid1); continue ; }
room_ptr rp = _rm->create_room (uid1, uid2);
if (rp.get () == nullptr ) { this ->add (uid1); this ->add (uid2); continue ; }
Json::Value resp;
resp["optype" ] = "match_success" ; resp["result" ] = true ;
std::string body;
json_util::serialize (resp, body);
conn1->send (body); conn2->send (body);
}
}
void th_normal_entry () { return handle_match (_q_normal); }
void th_high_entry () { return handle_match (_q_high); }
void th_super_entry () { return handle_match (_q_super); }
public :
matcher (room_manager *rm, user_table *ut, online_manager *om):
_rm(rm), _ut(ut), _om(om),
_th_normal(std::thread (&matcher::th_normal_entry, this )),
_th_high(std::thread (&matcher::th_high_entry, this )),
_th_super(std::thread (&matcher::th_super_entry, this )){
DLOG ("游戏匹配模块初始化完毕...." );
}
bool add (uint64_t uid) {
Json::Value user;
bool ret = _ut->select_by_id (uid, user);
if (ret == false ) { DLOG ("获取玩家:%d 信息失败!!" , uid); return false ; }
int score = user["score" ].asInt ();
if (score < 2000 ) { _q_normal.push (uid); }
else if (score >= 2000 && score < 3000 ) { _q_high.push (uid); }
else { _q_super.push (uid); }
return true ;
}
bool del (uint64_t uid) {
Json::Value user;
bool ret = _ut->select_by_id (uid, user);
if (ret == false ) { DLOG ("获取玩家:%d 信息失败!!" , uid); return false ; }
int score = user["score" ].asInt ();
if (score < 2000 ) { _q_normal.remove (uid); }
else if (score >= 2000 && score < 3000 ) { _q_high.remove (uid); }
else { _q_super.remove (uid); }
return true ;
}
};
16. 服务器业务请求流程
17. 服务器网络通信接口设计 统一数据格式,包含 optype, result, reason 等字段。
18. 服务器类设计 void http_callback (websocketpp::connection_hdl hdl) {
wsserver_t ::connection_ptr conn = _wssrv.get_con_from_hdl (hdl);
websocketpp::http::parser::request req = conn->get_request ();
std::string method = req.get_method ();
std::string uri = req.get_uri ();
std::string pathname = _web_root + uri;
std::string body;
file_util::read (pathname,body);
conn->set_status (websocketpp::http::status_code::ok);
conn->set_body (body);
}
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 ();
}
};
19. 服务器业务请求处理分类
20. 服务器业务静态资源请求处理 void file_handler (wsserver_t ::connection_ptr &conn) {
websocketpp::http::parser::request req = conn->get_request ();
std::string uri = req.get_uri ();
std::string realpath = _web_root + uri;
if (realpath.back () == '/' ) { realpath += "login.html" ; }
Json::Value resp_json;
std::string body;
bool ret = file_util::read (realpath, body);
if (ret == false ) {
body += "<html><head><meta charset='UTF-8'/></head><body><h1> Not Found </h1></body></html>" ;
conn->set_status (websocketpp::http::status_code::not_found);
conn->set_body (body);
return ;
}
conn->set_body (body);
conn->set_status (websocketpp::http::status_code::ok);
}
21. 服务器业务用户注册后台处理 解决 MySQL 8.0 密码验证兼容性问题(移除 password() 函数,改用 MD5)。
void http_resp (wsserver_t ::connection_ptr &conn, bool result, websocketpp::http::status_code::value code, const std::string &reason) {
Json::Value resp_json;
resp_json["result" ] = result;
resp_json["reason" ] = reason;
std::string resp_body;
json_util::serialize (resp_json, resp_body);
conn->set_status (code);
conn->set_body (resp_body);
conn->append_header ("Content-Type" , "application/json" );
return ;
}
void reg (wsserver_t ::connection_ptr &conn) {
websocketpp::http::parser::request req = conn->get_request ();
std::string req_body = conn->get_request_body ();
Json::Value login_info;
bool ret = json_util::unserialize (req_body, login_info);
if (ret == false ) { DLOG ("反序列化注册信息失败" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "请求的正文格式错误" ); }
if (login_info["username" ].isNull () || login_info["password" ].isNull ()) {
DLOG ("用户名密码不完整" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "请输入用户名/密码" );
}
ret = _ut.insert (login_info);
if (ret == false ) { DLOG ("向数据库插入数据失败" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "用户名已经被占用!" ); }
return http_resp (conn, true , websocketpp::http::status_code::ok, "注册用户成功" );
}
22. 服务器业务用户注册前端处理 <!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > 注册</title >
<link rel ="stylesheet" href ="./css/common.css" >
<link rel ="stylesheet" href ="./css/login.css" >
</head >
<body >
<div > 网络五子棋对战游戏</div >
<div >
<h3 > 注册</h3 >
<div > <span > 用户名</span > <input type ="text" name ="username" > </div >
<div > <span > 密码</span > <input type ="password" name ="password" > </div >
<div > <button onclick ="reg()" > 提交</button > </div >
</div >
<script src ="js/jquery.min.js" > </script >
<script >
function reg ( ) {
var reg_info = { username : document .getElementById ("user_name" ).value , password : document .getElementById ("password" ).value };
$.ajax ({ url : "/reg" , type : "post" , data : JSON .stringify (reg_info),
success : function (res ) {
if (res.result == false ) {
document .getElementById ("user_name" ).value = "" ;
document .getElementById ("password" ).value = "" ;
alert (res.reason );
}else {
alert (res.reason );
window .location .assign ("/login.html" );
}
},
error : function (xhr ) {
document .getElementById ("user_name" ).value = "" ;
document .getElementById ( ). = ;
( . (xhr));
}
})
}
</script >
</body >
</html >
23. 服务器业务用户注册联调测试 注意:MySQL 8.0 移除了 password() 函数,需改用 MD5 哈希算法。
24. 服务器业务用户登录后台处理 void login (wsserver_t ::connection_ptr &conn) {
std::string req_body = conn->get_request_body ();
Json::Value login_info;
bool ret = json_util::unserialize (req_body, login_info);
if (ret == false ) { DLOG ("反序列化登录信息失败" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "请求的正文格式错误" ); }
if (login_info["username" ].isNull () || login_info["password" ].isNull ()) {
DLOG ("用户名密码不完整" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "请输入用户名/密码" );
}
ret = _ut.login (login_info);
if (ret == false ) { DLOG ("用户名密码错误" ); return http_resp (conn, false , websocketpp::http::status_code::bad_request, "用户名密码错误" ); }
uint64_t uid = login_info["id" ].asUInt64 ();
session_ptr ssp = _sm.create_session (uid, LOGIN);
if (ssp.get () == nullptr ) { DLOG ("创建会话失败" ); return http_resp (conn, false , websocketpp::http::status_code::internal_server_error , "创建会话失败" ); }
_sm.set_session_expire_time (ssp->ssid (), SESSION_TIMEOUT);
std::string cookie_ssid = "SSID=" + std::to_string (ssp->ssid ());
conn->append_header ("Set-Cookie" , cookie_ssid);
return http_resp (conn, true , websocketpp::http::status_code::ok , "登录成功" );
}
25. 服务器业务用户登录前端处理 <!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > 登录</title >
<link rel ="stylesheet" href ="./css/common.css" >
<link rel ="stylesheet" href ="./css/login.css" >
</head >
<body >
<div > 网络五子棋对战游戏</div >
<div >
<h3 > 登录</h3 >
<div > <span > 用户名</span > <input type ="text" > </div >
<div > <span > 密码</span > <input type ="password" > </div >
<div > <button onclick ="login()" > 提交</button > </div >
</div >
<script src ="./js/jquery.min.js" > </script >
<script >
function login ( ) {
var login_info = { username : document .getElementById ("user_name" ).value , password : document .getElementById ("password" ).value };
$.ajax ({ url : "/login" , type : "post" , data : JSON .stringify (login_info),
success : function (result ) {
alert ("登录成功" );
window .location .assign ("/game_hall.html" );
},
error : function (xhr ) {
alert (JSON .stringify (xhr));
document .getElementById ("user_name" ).value = "" ;
document .getElementById ("password" ).value = "" ;
}
})
}
</script >
</body >
</html >
26. 服务器业务用户信息获取后台处理 bool get_cookie_val (const std::string &cookie_str, const std::string &key, std::string &val) {
std::string sep = "; " ;
std::vector<std::string> cookie_arr;
string_util::split (cookie_str, sep, cookie_arr);
for (auto str : cookie_arr) {
std::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 ;
}
void info (wsserver_t ::connection_ptr &conn) {
Json::Value err_resp;
std::string cookie_str = conn->get_request_header ("Cookie" );
if (cookie_str.empty ()) {
return http_resp (conn, true , websocketpp::http::status_code::bad_request, "找不到 cookie 信息,请重新登录" );
}
std::string ssid_str;
bool ret = get_cookie_val (cookie_str, "SSID" , ssid_str);
if (ret == false ) {
return http_resp (conn, true , websocketpp::http::status_code::bad_request, "找不到 ssid 信息,请重新登录" );
}
session_ptr ssp = _sm.get_session_by_ssid (std::stol (ssid_str));
if (ssp.get () == nullptr ) {
return http_resp (conn, true , websocketpp::http::status_code::bad_request, "登录过期,请重新登录" );
}
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);
_sm.set_session_expire_time (ssp->ssid (), SESSION_TIMEOUT);
}
27. 服务器业务用户信息获取前端处理 <!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > 登录</title >
<link rel ="stylesheet" href ="./css/common.css" >
<link rel ="stylesheet" href ="./css/login.css" >
</head >
<body >
<div > 网络五子棋对战游戏</div >
<div >
<h3 > 登录</h3 >
<div > <span > 用户名</span > <input type ="text" > </div >
<div > <span > 密码</span > <input type ="password" > </div >
<div > <button onclick ="login()" > 提交</button > </div >
</div >
<script src ="./js/jquery.min.js" > </script >
<script >
function login ( ) {
var login_info = { username : document .getElementById ("user_name" ).value , password : document .getElementById ("password" ).value };
$.ajax ({ url : "/login" , type : "post" , data : JSON .stringify (login_info),
success : function (result ) {
alert ("登录成功" );
window .location .assign ("/game_hall.html" );
},
error : function (xhr ) {
alert (JSON .stringify (xhr));
document .getElementById ("user_name" ).value = "" ;
document .getElementById ("password" ).value = "" ;
}
})
}
</script >
</body >
</html >
28. 服务器业务用户登录联调测试 Session 自动创建和销毁,从登录页跳转至游戏大厅成功。
29. 用户信息获取功能请求的处理
30. 服务器业务用户信息获取前端处理 <!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > 游戏大厅</title >
<link rel ="stylesheet" href ="./css/common.css" >
<link rel ="stylesheet" href ="./css/game_hall.css" >
</head >
<body >
<div > 网络五子棋对战游戏</div >
<div >
<div > </div >
<div > 开始匹配</div >
</div >
</div >
<script src ="./js/jquery.min.js" > </script >
<script >
var ws_url = "ws://" + location.host + "/hall" ;
var ws_hdl = null ;
window .onbeforeunload = function ( ) { ws_hdl.close (); }
var button_flag = "stop" ;
var be = document .getElementById ("match-button" );
be.onclick = function ( ) {
if (button_flag == "stop" ) {
var req_json = { optype : "match_start" }
ws_hdl.send (JSON .stringify (req_json));
}else {
var req_json = { optype : "match_stop" }
ws_hdl.send (JSON .stringify (req_json));
}
}
function get_user_info ( ) {
$.ajax ({ url : "/info" , type : "get" ,
success : function (res ) {
var info_html = "<p>" + "用户:" + res.username + " 积分:" + res.score + "</br>" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "</p>" ;
var screen_div = document .getElementById ("screen" );
screen_div.innerHTML = info_html;
ws_hdl = new WebSocket (ws_url);
ws_hdl.onopen = ws_onopen;
ws_hdl.onclose = ws_onclose;
ws_hdl.onerror = ws_onerror;
ws_hdl.onmessage = ws_onmessage;
},
error : function (xhr ) {
alert (JSON .stringify (xhr));
location.replace ("/login.html" );
}
})
}
function ws_onopen ( ) { console .log ("websocket onopen" ); }
function ws_onclose ( ) { console .log ("websocket onopen" ); }
function ws_onerror ( ) { console .log ("websocket onopen" ); }
function ws_onmessage (evt ) {
var rsp_json = JSON .parse (evt.data );
if (rsp_json.result == false ) { alert (evt.data ); location.replace ("/login.html" ); return ; }
if (rsp_json["optype" ] == "hall_ready" ) { alert ("游戏大厅连接建立成功!" ); }
else if (rsp_json["optype" ] == "match_success" ) { alert ("对战匹配成功,进入游戏房间!" ); location.replace ("/game_room.html" ); }
else if (rsp_json["optype" ] == "match_start" ) { console .log ("玩家已经加入匹配队列" ); button_flag = "start" ; be.innerHTML = "匹配中....点击按钮停止匹配!" ; return ; }
else if (rsp_json["optype" ] == "match_stop" ){ console .log ("玩家已经移除匹配队列" ); button_flag = "stop" ; be.innerHTML = "开始匹配" ; return ; }
else { alert (evt.data ); location.replace ("/login.html" ); return ; }
}
get_user_info ();
</script >
</body >
</html >
31. 服务器业务用户信息获取前后端联调
32. 服务器业务长连接分类
33. 服务器业务游戏大厅长连接建立成功处理 void wsopen_game_hall (wsserver_t ::connection_ptr conn) {
Json::Value resp_json;
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { return ; }
if (_om.is_in_game_hall (ssp->get_user ()) || _om.is_in_game_room (ssp->get_user ())) {
resp_json["optype" ] = "hall_ready" ; resp_json["reason" ] = "玩家重复登录!" ; resp_json["result" ] = false ;
return ws_resp (conn, resp_json);
}
_om.enter_game_hall (ssp->get_user (), conn);
resp_json["optype" ] = "hall_ready" ; resp_json["result" ] = true ;
ws_resp (conn, resp_json);
_sm.set_session_expire_time (ssp->ssid (), SESSION_FOREVER);
}
void wsopen_game_room (wsserver_t ::connection_ptr conn) {
Json::Value resp_json;
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { return ; }
if (_om.is_in_game_hall (ssp->get_user ()) || _om.is_in_game_room (ssp->get_user ())) {
resp_json["optype" ] = "room_ready" ; resp_json["reason" ] = "玩家重复登录!" ; resp_json["result" ] = false ;
return ws_resp (conn, resp_json);
}
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);
}
_om.enter_game_room (ssp->get_user (), conn);
_sm.set_session_expire_time (ssp->ssid (), SESSION_FOREVER);
resp_json["optype" ] = "room_ready" ; resp_json["result" ] = true ;
resp_json["room_id" ] = (Json::UInt64)rp->id ();
resp_json["uid" ] = (Json::UInt64)ssp->get_user ();
resp_json["white_id" ] = (Json::UInt64)rp->get_white_user ();
resp_json["black_id" ] = (Json::UInt64)rp->get_black_user ();
return ws_resp (conn, resp_json);
}
void wsopen_callback (websocketpp::connection_hdl hdl) {
wsserver_t ::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 (wsserver_t ::connection_ptr conn) {
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { return ; }
_om.exit_game_hall (ssp->get_user ());
_sm.set_session_expire_time (ssp->ssid (), SESSION_TIMEOUT);
}
void wsclose_game_room (wsserver_t ::connection_ptr conn) {
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { return ; }
_om.exit_game_room (ssp->get_user ());
_sm.set_session_expire_time (ssp->ssid (), SESSION_TIMEOUT);
_rm.remove_room_user (ssp->get_user ());
}
void wsclose_callback (websocketpp::connection_hdl hdl) {
wsserver_t ::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 (wsserver_t ::connection_ptr conn, wsserver_t ::message_ptr msg) {
Json::Value resp_json;
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { return ; }
std::string req_body = msg->get_payload ();
Json::Value req_json;
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); }
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 (wsserver_t ::connection_ptr conn, wsserver_t ::message_ptr msg) {
Json::Value resp_json;
session_ptr ssp = get_session_by_cookie (conn);
if (ssp.get () == nullptr ) { DLOG ("房间 - 没有找到会话信息" ); return ; }
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 ;
DLOG ("房间 - 没有找到玩家房间信息" ); return ws_resp (conn, resp_json);
}
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 ;
DLOG ("房间 - 反序列化请求失败" ); return ws_resp (conn, resp_json);
}
DLOG ("房间:收到房间请求,开始处理...." );
return rp->handle_request (req_json);
}
void wsmsg_callback (websocketpp::connection_hdl hdl, wsserver_t ::message_ptr msg) {
wsserver_t ::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); }
}
34. 服务器业务游戏大厅前端事件处理
35. 服务器业务游戏大厅所有功能联调测试 注意不能使用同一个浏览器进行测试,因为 Cookie 和 Session 会互相影响。
36. 服务器业务游戏房间前端长连接建立 <!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > 游戏房间</title >
<link rel ="stylesheet" ="css/common.css" >
<link rel ="stylesheet" ="css/game_room.css" >
</head >
<body >
<div > 网络五子棋对战游戏</div >
<div >
<div >
<canvas > </canvas >
<div > 等待玩家连接中...</div >
</div >
<div >
<div > <p > 你好!</p > </br > <p > 你好!</p > </br > <p > leihoua~</p > </br > </div >
<div > <input type ="text" > <button > 发送</button > </div >
</div >
</div >
<script >
let chessBoard = [];
let BOARD_ROW_AND_COL = 15 ;
let chess = document .getElementById ('chess' );
let context = chess.getContext ('2d' );
var ws_url = "ws://" + location.host + "/room" ;
var ws_hdl = new WebSocket (ws_url);
var room_info = null ;
var is_me;
function initGame ( ) {
initBoard ();
context.strokeStyle = "#BFBFBF" ;
let logo = new Image ();
logo.src = "image/sky.jpeg" ;
logo.onload = function ( ) {
context.drawImage (logo, 0 , 0 , 450 , 450 );
drawChessBoard ();
}
}
function initBoard ( ) {
for (let i = 0 ; i < BOARD_ROW_AND_COL ; i++) {
chessBoard[i] = [];
for (let j = 0 ; j < BOARD_ROW_AND_COL ; j++) { chessBoard[i][j] = 0 ; }
}
}
( ) {
( i = ; i < ; i++) {
context. ( + i * , ); context. ( + i * , ); context. ();
context. ( , + i * ); context. ( , + i * ); context. ();
}
}
( ) {
(i < || j < ) ;
context. ();
context. ( + i * , + j * , , , * . );
context. ();
gradient = context. ( + i * + , + j * - , , + i * + , + j * - , );
(!isWhite) { gradient. ( , ); gradient. ( , ); }
{ gradient. ( , ); gradient. ( , ); }
context. = gradient;
context. ();
}
chess. = ( ) {
(!is_me) { ( ); ; }
x = e. ; y = e. ;
col = . (x / ); row = . (y / );
(chessBoard[row][col] != ) { ( ); ; }
(row, col);
}
( ) {
chess_info = { optype : , : room_info. , : room_info. , : r, : c };
ws_hdl. ( . (chess_info));
}
. = ( ) { ws_hdl. (); }
ws_hdl. = ( ) { . ( ); }
ws_hdl. = ( ) { . ( ); }
ws_hdl. = ( ) { . ( ); }
( ) {
screen_div = . ( );
(me) { screen_div. = ; }
{ screen_div. = ; }
}
ws_hdl. = ( ) {
info = . (evt. );
(info. == ) {
room_info = info;
is_me = room_info. == room_info. ? : ;
(is_me);
();
} (info. == ){
(info. == ) { (info. ); ; }
is_me = info. == room_info. ? : ;
isWhite = info. == room_info. ? : ;
(info. != - && info. != - ){ (info. , info. , isWhite); chessBoard[info. ][info. ] = ; }
(info. == ) { ; }
screen_div = . ( );
(room_info. == info. ) { screen_div. = info. ; }
{ screen_div. = ; }
chess_area_div = . ( );
button_div = . ( );
button_div. = ;
button_div. = ( ) { ws_hdl. (); location. ( ); }
chess_area_div. (button_div);
} (info. == ) {
(info. == ) { (info. ); ; }
msg_div = . ( );
msg_div. = info. ;
(info. == room_info. ) { msg_div. ( , ); }
{ msg_div. ( , ); }
br_div = . ( );
msg_show_div = . ( );
msg_show_div. (msg_div);
msg_show_div. (br_div);
. ( ). = ;
}
}
cb_div = . ( );
cb_div. = ( ) {
send_msg = { optype : , room_id : room_info. , uid : room_info. , message : . ( ). };
ws_hdl. ( . (send_msg));
}
</script >
</body >
</html >
37. 走棋操作与聊天动作 走棋操作涉及坐标计算、胜负判断及广播。聊天动作包含敏感词过滤。
项目总结 核心技术栈:C++、WebSocketpp、MySQL、JSON、HTML/CSS/JS。
问题
MySQL 8.0 密码验证函数变更问题。
Session 生命周期管理细节。
浏览器缓存与 Cookie 隔离问题。
"password"
value
""
alert
JSON
stringify
function
drawChessBoard
for
let
0
BOARD_ROW_AND_COL
moveTo
15
30
15
lineTo
15
30
430
stroke
moveTo
15
15
30
lineTo
435
15
30
stroke
function
oneStep
i, j, isWhite
if
0
0
return
beginPath
arc
15
30
15
30
13
0
2
Math
PI
closePath
var
createRadialGradient
15
30
2
15
30
2
13
15
30
2
15
30
2
0
if
addColorStop
0
"#0A0A0A"
addColorStop
1
"#636766"
else
addColorStop
0
"#D1D1D1"
addColorStop
1
"#F9F9F9"
fillStyle
fill
onclick
function
e
if
alert
"等待对方走棋...."
return
let
offsetX
let
offsetY
let
Math
floor
30
let
Math
floor
30
if
0
alert
"当前位置已有棋子!"
return
send_chess
function
send_chess
r, c
var
"put_chess"
room_id
room_id
uid
uid
row
col
send
JSON
stringify
window
onbeforeunload
function
close
onopen
function
console
log
"房间长连接建立成功"
onclose
function
console
log
"房间长连接断开"
onerror
function
console
log
"房间长连接出错"
function
set_screen
me
var
document
getElementById
"screen"
if
innerHTML
"轮到己方走棋..."
else
innerHTML
"轮到对方走棋..."
onmessage
function
evt
var
JSON
parse
data
if
optype
"room_ready"
uid
white_id
true
false
set_screen
initGame
else
if
optype
"put_chess"
if
result
false
alert
reason
return
uid
uid
false
true
uid
white_id
true
false
if
row
1
col
1
oneStep
col
row
row
col
1
if
winner
0
return
var
document
getElementById
"screen"
if
uid
winner
innerHTML
reason
else
innerHTML
"你输了"
var
document
getElementById
"chess_area"
var
document
createElement
"div"
innerHTML
"返回大厅"
onclick
function
close
replace
"/game_hall.html"
appendChild
else
if
optype
"chat"
if
result
false
alert
reason
return
var
document
createElement
"p"
innerHTML
message
if
uid
uid
setAttribute
"id"
"self_msg"
else
setAttribute
"id"
"peer_msg"
var
document
createElement
"br"
var
document
getElementById
"chat_show"
appendChild
appendChild
document
getElementById
"chat_input"
value
""
var
document
getElementById
"chat_button"
onclick
function
var
"chat"
room_id
uid
document
getElementById
"chat_input"
value
send
JSON
stringify
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online