C++ 在线五子棋对战项目网页版开发详解
介绍基于 C++、WebSocketpp 和 MySQL 开发的在线五子棋对战项目。涵盖 WebSocket 通信原理、数据库设计、Session 管理、房间匹配逻辑及前后端联调流程。解决了 MySQL 8.0 密码验证兼容性问题,实现了用户登录、大厅匹配、房间对战等功能。

介绍基于 C++、WebSocketpp 和 MySQL 开发的在线五子棋对战项目。涵盖 WebSocket 通信原理、数据库设计、Session 管理、房间匹配逻辑及前后端联调流程。解决了 MySQL 8.0 密码验证兼容性问题,实现了用户登录、大厅匹配、房间对战等功能。

WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。
WebSocketpp 协议由 HTTP 协议切换过来。客户端发送协议切换请求,服务端响应同意切换,形成 WebSocket 通信连接。
搭建流程:
代码示例:
#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", "text/html");
conn->set_status(websocketpp::http::status_code::ok);
}
void wsopen_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {
std::cout << "websocket 握手成功!!\n";
}
void wsclose_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {
std::cout << "websocket 连接断开!!\n";
}
void wsmsg_callback(wsserver_t *srv, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {
wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);
std::string rsp = "client say: " + msg->get_payload();
conn->send(rsp, websocketpp::frame::opcode::text);
}
int main() {
wsserver_t wssrv;
wssrv.set_access_channels(websocketpp::log::alevel::none);
wssrv.init_asio();
wssrv.set_reuse_addr(true);
wssrv.set_http_handler(std::bind(http_callback, &wssrv, std::placeholders::_1));
wssrv.set_open_handler(std::bind(wsopen_callback, &wssrv, std::placeholders::_1));
wssrv.set_close_handler(std::bind(wsclose_callback, &wssrv, std::placeholders::_1));
wssrv.set_message_handler(std::bind(wsmsg_callback, &wssrv, std::placeholders::_1, std::placeholders::_2));
wssrv.listen(8085);
wssrv.start_accept();
wssrv.run();
return 0;
}
操作流程:
代码示例:
#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;
}



使用宏定义实现不同级别的日志输出。
#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
封装数据库连接、执行、销毁操作。
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 ;
}
};
用于序列化和反序列化。
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;
}
};
用于分割字符串提取内容。
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();
}
};
用于读取文件内容。
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;
}
};
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
);
封装数据库操作,提供用户注册、登录、信息查询、胜负更新等功能。
#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
维护游戏大厅和游戏房间的用户 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
包含棋盘状态、玩家信息、胜负判断逻辑。
#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;
}
};
管理房间的创建、查找和销毁。
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 ;
}
};
Session 用于维持用户登录状态,配合 Cookie 使用。WebSocketpp 支持定时器功能。
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; }
};
管理会话的生命周期,包括超时自动销毁。
#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);
}
}
};
基于队列的匹配机制,根据分数将用户分为普通、高手、大神三个队列。
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); }
};
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;
}
};
区分静态资源请求和动态资源请求。
统一数据格式,包含 optype, result, reason 等字段。
整合各模块,提供统一的入口。
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();
}
};
区分静态资源和动态业务请求。
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);
}
解决 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, "注册用户成功");
}
<!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>
注意:MySQL 8.0 移除了 password() 函数,需改用 MD5 哈希算法。
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 , "登录成功");
}
<!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>
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);
}
<!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>
Session 自动创建和销毁,从登录页跳转至游戏大厅成功。
同第 26 节,后端逻辑一致。
<!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>
成功显示用户信息。
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); }
}
同第 30 节代码。
注意不能使用同一个浏览器进行测试,因为 Cookie 和 Session 会互相影响。
<!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>
走棋操作涉及坐标计算、胜负判断及广播。聊天动作包含敏感词过滤。
核心技术栈:C++、WebSocketpp、MySQL、JSON、HTML/CSS/JS。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online