websocket 长连接请求分为两大类,一类是建立连接处理,一类是断开连接处理,而这两类中又根据是在游戏大厅还是游戏房间来区分开,所以得设计多个模块,下面来实现它们!
还要注意一个点,就是 websocket 连接中进行响应的时候,用的是 connect 类中的 send() 接口给客户端响应;而我们之前的 http 响应都是通过 connect 类中的设置头部、正文等设置函数来进行响应的,它们是不同的!
Ⅰ. 整体框架
和 http 请求处理函数一样,因为有不同类型的请求,这里一共有两种:游戏大厅连接、游戏房间连接。
注意,open_callback() 函数是在 websocket 长连接自动建立之后触发的函数,并不是我们要去做长连接建立的工作的地方,建立连接的工作库里会帮我们实现,我们只需要负责完成建立之后的处理函数!
这三个 callback 函数基本是一致的,它们都只需要通过 uri 就能得到和判断是游戏房间还是游戏大厅的长连接,从而调用各自的子函数去实现长连接任务;而不同的是两个子函数的功能不同!
/* websocket连接建立成功处理函数 */
// websocket长连接建立成功之后的处理函数 -- 根据请求类型不同分为游戏大厅和游戏房间连接
void open_callback(websocketpp::connection_hdl hdl)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall")
return open_game_hall(conn); // 游戏大厅的长连接建立成功处理
else if(uri == "/room")
return open_game_room(conn); // 游戏房间的长连接建立成功处理
}
// 游戏大厅的长连接建立成功处理函数
void open_game_hall(wsserver_t::connection_ptr& conn)
{}
// 游戏大厅的长连接建立成功处理函数
void open_game_room(wsserver_t::connection_ptr& conn)
{}
/* websocket断开连接处理函数 */
// websocket长连接建立断开之前的处理函数 -- 分为游戏大厅和游戏房间连接
void close_callback(websocketpp::connection_hdl hdl)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall") // 游戏大厅的长连接断开前处理
return close_game_hall(conn);
else if(uri == "/room") // 游戏房间的长连接断开前处理
return close_game_room(conn);
}
// 游戏大厅长连接断开前的处理
void close_game_hall(wsserver_t::connection_ptr& conn)
{}
// 游戏房间长连接断开前的处理
void close_game_room(wsserver_t::connection_ptr& conn)
{}
/* websocket收发消息处理函数 */
// websocket长连接通信处理
void message_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msgptr)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall")
return message_in_hall(conn, msgptr); // 游戏大厅的收发信息处理
else if(uri == "/room")
return message_in_room(conn, msgptr); // 游戏房间的收发信息处理
}
// 游戏大厅的通信处理
void message_in_hall(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{}
// 游戏房间的通信处理
void message_in_room(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{} 这里顺便给出下面会用到的几个接口,因为这部分的代码有很多是重复的,为了减少冗余,就提炼成几个接口函数:
// websocket响应处理通用函数
void websocket_response(wsserver_t::connection_ptr& conn,
bool result,
const std::string& optype,
const std::string& reason)
{
Json::Value resp_json;
resp_json["optype"] = optype;
if(!optype.empty()) resp_json["result"] = result;
if(!reason.empty()) resp_json["reason"] = reason;
std::string body;
json_util::serialize(resp_json, body);
conn->send(body);
}
// 获取session_ptr -- 顺便验证登录
session_ptr websocket_get_session(wsserver_t::connection_ptr& conn)
{
// 1. 获取请求信息中的Cookie,从Cookie中获取ssid
std::string cookie_str = conn->get_request_header("Cookie");
if (cookie_str.empty())
{
// 如果没有cookie,返回错误:没有cookie信息,让客户端重新登录
websocket_response(conn, false, "hall_ready", "没有找到cookie信息,需要重新登录");
return session_ptr();
}
// 2. 从cookie中取出ssid
std::string ssid_str;
bool ret = get_sessionID_from_cookie(cookie_str, "SSID", ssid_str);
if (ret == false)
{
// cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录
websocket_response(conn, false, "hall_ready", "没有找到SSID信息,需要重新登录");
return session_ptr();
}
// 3. 在session管理中查找对应的会话信息
session_ptr sp = _session.get_session_by_sesssionID(std::stol(ssid_str));
if (sp.get() == nullptr)
{
// 没有找到session,则认为登录已经过期,需要重新登录
websocket_response(conn, false, "hall_ready", "登录已经过期,需要重新登录");
return session_ptr();
}
return sp;
}
// 从cookie中获取sessionID函数
bool get_sessionID_from_cookie(const std::string& cookie_str, const std::string& key, std::string &val)
{
// Cookie: SSID=XXX; path=/;
//1. 以 ; 作为分隔符,对字符串进行分割,得到各个单个的cookie信息
std::string sep = "; ";
std::vector<std::string> cookie_arr;
string_util::split(cookie_str, sep, cookie_arr);
for (auto &str : cookie_arr) {
//2. 对单个cookie字符串,以 = 为分隔符进行分割,得到key和val
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;
}Ⅱ. 游戏大厅处理函数
游戏大厅我们要完成的就是三个函数,分别是 websocket连接建立成功处理函数、websocket断开连接处理函数、websocket收发消息处理函数,其实后面的游戏房间也是一样的完成这三个函数,但是逻辑业务是不太一样的!
1、连接建立成功处理
// 游戏大厅的长连接建立成功处理函数
void open_game_hall(wsserver_t::connection_ptr& conn)
{
// 1. 登录验证--判断当前客户端是否已经成功登录
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 判断当前客户端是否是重复登录
if (_online_user.isInHall(sp->getUserID()) || _online_user.isInRoom(sp->getUserID()))
return websocket_response(conn, false, "hall_ready", "玩家重复登录!");
// 3. 将当前客户端以及连接加入到游戏大厅
_online_user.enterHall(sp->getUserID(), conn);
//4. 给客户端响应游戏大厅连接建立成功
websocket_response(conn, true, "hall_ready", "游戏大厅连接建立成功");
// 5. 记得将session设置为永久存在
_session.set_session_expire_time(sp->getSessionID(), SESSION_FOREVER);
}2、断开连接处理函数
// 游戏大厅长连接断开前的处理
void close_game_hall(wsserver_t::connection_ptr& conn)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 将玩家从游戏大厅中移除
_online_user.exitHall(sp->getUserID());
// 3. 将session恢复生命周期的管理,设置定时销毁
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
}3、收发消息处理函数
// 游戏大厅的通信处理
void message_in_hall(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{
// 1. 获取session信息,在子函数中完成身份验证
session_ptr sp = websocket_get_session(conn);
if(sp.get() == nullptr)
return;
// 2. 获取消息的请求数据
std::string req_body = msgptr->get_payload();
Json::Value req_json;
bool ret = json_util::unserialize(req_body, req_json);
if(ret == false)
return websocket_response(conn, false, "", "请求信息解析失败");
// 3. 根据请求进行处理
if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start")
{
// 开始对战匹配:通过匹配模块,将用户添加到匹配队列中
_matcher.addUser(sp->getUserID());
return websocket_response(conn, true, "match_start", "开始匹配成功");
}
else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop")
{
// 停止对战匹配:通过匹配模块,将用户从匹配队列中移除
_matcher.delUser(sp->getUserID());
return websocket_response(conn, true, "match_stop", "停止匹配成功");
}
return websocket_response(conn, false, "unknow", "请求类型未知");
}Ⅲ. 游戏房间处理函数
1、连接建立成功处理
// 游戏房间的长连接建立成功处理函数
void open_game_room(wsserver_t::connection_ptr& conn)
{
// 1. 获取当前客户端的session
session_ptr sp = websocket_get_session(conn);
if(sp.get() == nullptr)
return;
// 2. 判断当前用户是否重复登陆---在线用户管理
if(_online_user.isInHall(sp->getUserID()) || _online_user.isInRoom(sp->getUserID()))
{
return websocket_response(conn, false, "room_ready", "玩家重复登录!");
}
// 3. 判断当前用户是否已经创建好了房间 --- 房间管理
room_ptr rp = _room.getRoom_ByUserID(sp->getUserID());
if(rp.get() == nullptr)
{
return websocket_response(conn, false, "room_ready", "没有找到玩家的房间信息");
}
// 4. 将当前用户添加到在线用户管理的游戏房间中
_online_user.enterRoom(sp->getUserID(), conn);
// 5. 将session重新设置为永久存在
_session.set_session_expire_time(sp->getSessionID(), SESSION_FOREVER);
// 6. 回复房间内玩家已经准备完毕
Json::Value resp_json;
resp_json["optype"] = "room_ready";
resp_json["result"] = true;
resp_json["room_id"] = (Json::UInt64)rp->getRoomID();
resp_json["uid"] = (Json::UInt64)sp->getUserID();
resp_json["white_id"] = (Json::UInt64)rp->getWhiteID();
resp_json["black_id"] = (Json::UInt64)rp->getBlackID();
std::string body;
json_util::serialize(resp_json, body);
conn->send(body);
}2、断开连接处理函数
// 游戏房间长连接断开前的处理
void close_game_room(wsserver_t::connection_ptr& conn)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 将玩家从在线用户管理中移除
_online_user.exitRoom(sp->getUserID());
// 3. 将session回复生命周期的管理,设置定时销毁
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
// 4. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间
_room.removeUser(sp->getUserID());
}3、收发消息处理函数
// 游戏房间的通信处理
void message_in_room(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 获取客户端房间信息
room_ptr rp = _room.getRoom_ByUserID(sp->getUserID());
if(rp.get() == nullptr)
{
DLOG("房间--没有找到玩家房间信息");
return websocket_response(conn, false, "unknow", "没有找到玩家的房间信息");
}
// 3. 对消息进行反序列化
Json::Value req_json;
std::string req_body = msgptr->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
if(ret == false)
{
DLOG("房间-反序列化请求失败");
return websocket_response(conn, false, "unknow", "请求解析失败");
}
DLOG("房间:收到房间请求,开始处理....");
// 4. 通过房间模块进行消息请求的处理
rp->request_handle(req_json);
}Ⅳ. 前端页面
1、游戏大厅页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏大厅</title>
<link rel="stylesheet" href="./css/common.css">
<link rel="stylesheet" href="./css/game_hall.css">
</head>
<body>
<div class="nav">网络五子棋对战游戏</div>
<!-- 整个页面的容器元素 -->
<div class="container">
<!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 -->
<div>
<!-- 展示用户信息 -->
<div id="screen"></div>
<!-- 匹配按钮 -->
<div id="match-button">开始匹配</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")
{
// 1. 没有进行匹配的状态下点击按钮,发送对战匹配请求
var req_json = {
optype: "match_start"
}
ws_hdl.send(JSON.stringify(req_json));
}
else
{
// 2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求
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 sl = 0;
if(res.win_count != 0){ sl = parseInt(res.win_count * 100 / res.total_count); }
var info_html = "<p>" + "用户: " + res.username + " 积分: " + res.score +
"</br>" + "比赛场次: " + res.total_count + " 获胜场次: " + res.win_count +
" 胜率: " + sl + "%" + "</p>";
var screen_div = document.getElementById("screen");
screen_div.innerHTML = info_html;
// 创建websocket连接
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")
{
console.log("游戏大厅连接建立成功!");
}
else if (rsp_json["optype"] == "match_success")
{
console.log("对战匹配成功,进入游戏房间!");
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>2、游戏房间页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏房间</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/game_room.css">
</head>
<body>
<div class="nav">网络五子棋对战游戏</div>
<div class="container">
<div id="chess_area">
<!-- 棋盘区域, 需要基于 canvas 进行实现 -->
<canvas id="chess" width="450px" height="450px"></canvas>
<!-- 显示区域 -->
<div id="screen"> 等待玩家连接中... </div>
</div>
<div id="chat_area" width="400px" height="300px">
<div id="chat_show">
<p id="self_msg">你好!</p></br>
<p id="peer_msg">你好!</p></br>
</div>
<div id="msg_show">
<input type="text" id="chat_input">
<button id="chat_button">发送</button>
</div>
</div>
</div>
<script>
let chessBoard = [];
let BOARD_ROW_AND_COL = 15;
let chess = document.getElementById('chess');
let context = chess.getContext('2d'); // 获取chess控件的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.jpg";
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;
}
}
}
// 绘制棋盘网格线函数
function drawChessBoard() {
for (let i = 0; i < BOARD_ROW_AND_COL; i++) {
context.moveTo(15 + i * 30, 15);
context.lineTo(15 + i * 30, 430); // 横向的线条
context.stroke();
context.moveTo(15, 15 + i * 30);
context.lineTo(435, 15 + i * 30); // 纵向的线条
context.stroke();
}
}
// 绘制棋子函数
function oneStep(i, j, isWhite) {
if (i < 0 || j < 0) return;
context.beginPath();
context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
context.closePath();
var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);
// 区分黑白子
if (!isWhite) {
gradient.addColorStop(0, "#0A0A0A");
gradient.addColorStop(1, "#636766");
} else {
gradient.addColorStop(0, "#D1D1D1");
gradient.addColorStop(1, "#F9F9F9");
}
context.fillStyle = gradient;
context.fill();
}
// 棋盘区域的点击事件
chess.onclick = function (e) {
// 1. 获取下棋位置,判断当前下棋操作是否正常
// a. 当前是否轮到自己走棋了
if (!is_me) {
alert("等待对方走棋....");
return;
}
// b. 当前位置是否已经被占用
let x = e.offsetX;
let y = e.offsetY;
// 注意, 横坐标是列, 纵坐标是行
// 这里是为了让点击操作能够对应到网格线上
let col = Math.floor(x / 30);
let row = Math.floor(y / 30);
if (chessBoard[row][col] != 0) {
alert("当前位置已有棋子!");
return;
}
// oneStep(col, row, true);
// 2. 向服务器发送走棋请求,收到响应后,再绘制棋子
send_chess(row, col);
}
// 发送下棋请求函数
function send_chess(r, c) {
var chess_info = {
optype : "put_chess",
room_id: room_info.room_id,
uid: room_info.uid,
row: r,
col: c
};
ws_hdl.send(JSON.stringify(chess_info));
console.log("click:" + JSON.stringify(chess_info));
}
window.onbeforeunload = function() {
ws_hdl.close();
}
ws_hdl.onopen = function() {
console.log("房间长连接建立成功");
}
ws_hdl.onclose = function() {
console.log("房间长连接断开");
}
ws_hdl.onerror = function() {
console.log("房间长连接出错");
}
// 设置横幅文字函数
function set_screen(me) {
var screen_div = document.getElementById("screen");
if (me)
{
screen_div.innerHTML = "轮到我方走棋...";
}
else
{
screen_div.innerHTML = "轮到对方走棋...";
}
}
ws_hdl.onmessage = function(evt)
{
// 1. 在收到room_ready之后进行房间的初始化
// 1. 将房间信息保存起来
var info = JSON.parse(evt.data);
console.log(JSON.stringify(info));
if (info.optype == "room_ready")
{
room_info = info;
is_me = room_info.uid == room_info.white_id ? true : false;
set_screen(is_me);
initGame();
}
else if (info.optype == "put_chess") // 走棋操作,收到走棋消息,进行棋子绘制
{
// 先判断是否请求正确
console.log("put_chess" + evt.data);
if (info.result == false)
{
alert(info.reason);
return;
}
// 当前走棋的用户id,与我自己的用户id相同,就是我自己走棋,走棋之后,就轮到对方了
is_me = info.uid == room_info.uid ? false : true;
// 绘制棋子的颜色,应该根据当前下棋角色的颜色确定
isWhite = info.uid == room_info.white_id ? true : false;
// 绘制棋子
if (info.row != -1 && info.col != -1)
{
oneStep(info.col, info.row, isWhite);
// 设置棋盘信息,1表示当前位置已走过
chessBoard[info.row][info.col] = 1;
}
// 是否有胜利者,还没胜利者则直接返回
if (info.winner == 0)
return;
// 如果有胜利者,则将双方的通知信息修改
var screen_div = document.getElementById("screen");
if (room_info.uid == info.winner)
screen_div.innerHTML = info.reason;
else
screen_div.innerHTML = "你输了";
// 并且在棋盘区域添加一个按钮,进行返回大厅
var chess_area_div = document.getElementById("chess_area");
var button_div = document.createElement("div");
button_div.innerHTML = "返回大厅";
button_div.onclick = function() {
ws_hdl.close();
location.replace("/game_hall.html");
}
chess_area_div.appendChild(button_div);
}
else if (info.optype == "chat")
{
// 收到一条消息,判断result,如果为true则渲染一条消息到显示框中
if(info.result == false)
{
alert(info.reason);
return;
}
var msg_div = document.createElement("p");
msg_div.innerHTML = info.message;
if (info.uid == room_info.uid)
{
msg_div.setAttribute("id", "self_msg");
}
else
{
msg_div.setAttribute("id", "peer_msg");
}
var br_div = document.createElement("br");
var msg_show_div = document.getElementById("chat_show");
msg_show_div.appendChild(msg_div);
msg_show_div.appendChild(br_div);
document.getElementById("chat_input").value = "";
}
}
// 聊天动作
// 1. 捕捉聊天输入框消息
// 2. 给发送按钮添加点击事件,点击俺就的时候,获取到输入框消息,发送给服务器
var cb_div = document.getElementById("chat_button");
cb_div.onclick = function() {
var send_msg = {
optype : "chat",
room_id : room_info.room_id,
uid : room_info.uid,
message : document.getElementById("chat_input").value
};
ws_hdl.send(JSON.stringify(send_msg));
}
</script>
</body>
</html>完整的makefile文件
.PHONY:gobang
gobang:gobang.cc logger.hpp util.hpp db.hpp online.hpp room.hpp session.hpp matcher.hpp server.hpp
g++ -o$@ $^ -std=c++11 -L/usr/lib64/mysql/ -lmysqlclient -ljsoncpp -lboost_system -lpthread
.PHONY:clean
clean:
rm -f gobang服务器完整代码
#ifndef __MY_SERVER_H__
#define __MY_SERVER_H__
#include "db.hpp"
#include "online.hpp"
#include "util.hpp"
#include "session.hpp"
#include "matcher.hpp"
#include "room.hpp"
#define WEBROOT "./wwwroot/"
class gobang_server
{
private:
std::string _webroot; // 静态资源目录,默认为./wwwroot/
wsserver_t _server; // websocket服务器实例
user_table _user_table; // 数据库用户信息实例
online_manager _online_user; // 在线用户管理实例
session_manager _session; // 会话管理实例
room_manager _room; // 房间管理实例
match_manager _matcher; // 匹配管理实例
public:
// 构造函数,完成成员变量的初始化以及服务器的设置
gobang_server(const std::string& host,
const std::string& user,
const std::string& passwd,
const std::string& dbname,
uint16_t port = 3306,
const std::string& webroot = WEBROOT)
: _webroot(webroot)
, _user_table(host, user, passwd, dbname, port)
, _session(&_server)
, _room(&_user_table, &_online_user)
, _matcher(&_online_user, &_user_table, &_room)
{
// 设置日志等级
_server.set_access_channels(websocketpp::log::alevel::none);
// 初始化asio调度器和地址重用
_server.init_asio();
_server.set_reuse_addr(true);
// 设置回调函数
_server.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));
_server.set_open_handler(std::bind(&gobang_server::open_callback, this, std::placeholders::_1));
_server.set_close_handler(std::bind(&gobang_server::close_callback, this, std::placeholders::_1));
_server.set_message_handler(std::bind(&gobang_server::message_callback, this, std::placeholders::_1, std::placeholders::_2));
}
// 启动服务器接口
void start(uint16_t port)
{
_server.listen(port); // 设置监听窗口
_server.start_accept(); // 开始获取新连接
_server.run(); // 启动服务器
}
private:
/* http请求处理函数 */
void http_callback(websocketpp::connection_hdl hdl)
{
// 1. 获取请求的方法和uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.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();
// 2. 根据不同的请求类型调用对应的处理函数
if(method == "POST" && uri == "/reg") // 注册请求
return register_handler(conn);
else if(method == "POST" && uri == "/login") // 登录请求
return login_handler(conn);
else if(method == "GET" && uri == "/info") // 获取用户信息请求
return information_handler(conn);
else
return static_resource_handler(conn); // 剩下的都认为是静态资源请求
}
// 静态资源请求处理函数
void static_resource_handler(wsserver_t::connection_ptr& conn)
{
// 1. 获取请求的uri资源路径
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 通过根目录和请求的uri组合成实际路径
std::string path = _webroot + uri;
// 3. 如果请求的是一个目录,那么就默认增加一个后缀login.html
if(path.back() == '/')
path += "index.html";
// 4. 读取文件内容,并且判断是否读写成功
std::string body;
bool ret = file_util::read(path, body);
if(ret == false)
{
DLOG("静态资源请求异常,%s, 大小: %d", path.c_str(), body.size());
// 读写失败则返回一个状态码为404的NOT FOUND页面
body.clear();
body += "<html>";
body += "<head>";
body += "<meta charset='UTF-8'/>";
body += "</head>";
body += "<body>";
body += "<h1> Not Found </h1>";
body += "</body>";
body += "</html>";
conn->set_status(websocketpp::http::status_code::not_found);
conn->set_body(body);
return;
}
// 5. 读写成功后设置响应正文,也就是将静态资源传回去
conn->set_status(websocketpp::http::status_code::ok);
conn->set_body(body);
DLOG("静态资源请求成功, %s, 大小: %d", path.c_str(), body.size());
}
// 注册功能请求的处理函数
void register_handler(wsserver_t::connection_ptr& conn)
{
// 1. 获取到请求正文 -- 为了获取用户名和密码
std::string req_body = conn->get_request_body();
// 2. 对正文进行反序列化,得到用户名和密码
Json::Value body;
bool ret = json_util::unserialize(req_body, body);
if(ret == false)
{
// 反序列化失败的话要返回错误响应
DLOG("反序列化注册信息失败");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
}
// 3. 判断用户名和密码是否填写完整
if(body["username"].isNull() || body["password"].isNull() || body["username"].asString().empty() || body["password"].asString().empty())
{
// 填写不完整的话要返回错误响应
DLOG("用户名密码不完整");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "请输入正确的用户名和密码");
}
// 4. 进行数据库的用户信息操作 -- 如果成功状态码返回200,失败返回400
ret = _user_table.sign_up(body);
if(ret == false)
{
// 写入用户信息错误的话要返回错误响应
DLOG("向数据库插入数据失败");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "该用户名已经被占用!");
}
http_response(conn, true, websocketpp::http::status_code::ok, "用户注册成功");
}
// 登录功能请求的处理函数
void login_handler(wsserver_t::connection_ptr& conn)
{
// 1. 获取请求正文,进行反序列化,得到用户名和密码
std::string req_body = conn->get_request_body();
Json::Value body;
bool ret = json_util::unserialize(req_body, body);
if(ret == false)
{
DLOG("反序列化登录信息失败");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
}
// 2. 验证用户名和密码是否都填写
if(body["username"].isNull() || body["password"].isNull() || body["username"].asString().empty() || body["password"].asString().empty())
{
DLOG("用户名密码不完整");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名和密码");
}
// 3. 进行数据库的用户信息验证,验证用户名和密码 -- 验证失败则返回400
ret = _user_table.login(body);
if(ret == false)
{
DLOG("用户不存在或者密码错误");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "请输入正确的用户名和密码");
}
// 4. 如果验证成功,则给客户端创建session,并且设置过期时间
session_ptr sp = _session.add_session(body["id"].asUInt64(), LOGIN);
if(sp.get() == nullptr)
{
// 创建会话失败的话返回的状态码是500
DLOG("创建会话失败");
return http_response(conn, false, websocketpp::http::status_code::internal_server_error, "创建会话失败");
}
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
// 5. 设置响应头部:Set-Cookie,将sessionID通过cookie响应返回
conn->append_header("Set-Cookie", "SSID=" + std::to_string(sp->getSessionID()));
http_response(conn, true, websocketpp::http::status_code::ok, "登录成功");
}
// 获取个人信息功能请求的处理函数
void information_handler(wsserver_t::connection_ptr& conn)
{
// 1. 获取请求信息中的cookie信息
std::string cookie_str = conn->get_request_header("Cookie");
if(cookie_str.empty())
{
// 如果没有cookie信息,则返回错误,让客户端重新登录
DLOG("找不到cookie信息,请重新登录");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录");
}
// 2. 从cookie中获取sessionID -- 封装一个接口并且调用
std::string sessionID;
bool ret = get_sessionID_from_cookie(cookie_str, "SSID", sessionID);
if(ret == false)
{
// cookie中没有会话id,则返回错误,让客户端重新登录
DLOG("找不到会话id,请重新登录");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "找不到会话id,请重新登录");
}
// 3. 在session管理对象中查找对应的session信息
session_ptr sp = _session.get_session_by_sesssionID(std::stol(sessionID));
if(sp.get() == nullptr)
{
// 在本地会话管理中找不到该会话,则认为登录已经过期,需要重新登录
DLOG("会话过期,请重新登录");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "会话过期,请重新登录");
}
// 4. 先获取用户id,然后从数据库中取出用户信息
uint64_t userid = sp->getUserID();
Json::Value userinfo;
ret = _user_table.select_by_id(userid, userinfo);
if(ret == false)
{
// 获取用户信息失败,返回错误:找不到用户信息
DLOG("找不到用户信息,请重新登录");
return http_response(conn, false, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录");
}
// 5. 将用户信息进行序列化后发送给客户端 -- 一般不会序列化失败,所以这里不判断
std::string body;
json_util::serialize(userinfo, body);
conn->set_body(body);
conn->set_status(websocketpp::http::status_code::ok);
conn->append_header("Content-Type", "application/json");
// 6. 刷新session过期时间,也就是重新设置过期时间
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
}
private:
/* websocket连接建立成功处理函数 */
// websocket长连接建立成功之后的处理函数 -- 根据请求类型不同分为游戏大厅和游戏房间连接
void open_callback(websocketpp::connection_hdl hdl)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall")
return open_game_hall(conn); // 游戏大厅的长连接建立成功处理
else if(uri == "/room")
return open_game_room(conn); // 游戏房间的长连接建立成功处理
}
// 游戏大厅的长连接建立成功处理函数
void open_game_hall(wsserver_t::connection_ptr& conn)
{
// 1. 登录验证--判断当前客户端是否已经成功登录
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 判断当前客户端是否是重复登录
if (_online_user.isInHall(sp->getUserID()) || _online_user.isInRoom(sp->getUserID()))
return websocket_response(conn, false, "hall_ready", "玩家重复登录!");
// 3. 将当前客户端以及连接加入到游戏大厅
_online_user.enterHall(sp->getUserID(), conn);
//4. 给客户端响应游戏大厅连接建立成功
websocket_response(conn, true, "hall_ready", "游戏大厅连接建立成功");
// 5. 记得将session设置为永久存在
_session.set_session_expire_time(sp->getSessionID(), SESSION_FOREVER);
}
// 游戏房间的长连接建立成功处理函数
void open_game_room(wsserver_t::connection_ptr& conn)
{
// 1. 获取当前客户端的session
session_ptr sp = websocket_get_session(conn);
if(sp.get() == nullptr)
return;
// 2. 判断当前用户是否重复登陆---在线用户管理
if(_online_user.isInHall(sp->getUserID()) || _online_user.isInRoom(sp->getUserID()))
{
return websocket_response(conn, false, "room_ready", "玩家重复登录!");
}
// 3. 判断当前用户是否已经创建好了房间 --- 房间管理
room_ptr rp = _room.getRoom_ByUserID(sp->getUserID());
if(rp.get() == nullptr)
{
return websocket_response(conn, false, "room_ready", "没有找到玩家的房间信息");
}
// 4. 将当前用户添加到在线用户管理的游戏房间中
_online_user.enterRoom(sp->getUserID(), conn);
// 5. 将session重新设置为永久存在
_session.set_session_expire_time(sp->getSessionID(), SESSION_FOREVER);
// 6. 回复房间内玩家已经准备完毕
Json::Value resp_json;
resp_json["optype"] = "room_ready";
resp_json["result"] = true;
resp_json["room_id"] = (Json::UInt64)rp->getRoomID();
resp_json["uid"] = (Json::UInt64)sp->getUserID();
resp_json["white_id"] = (Json::UInt64)rp->getWhiteID();
resp_json["black_id"] = (Json::UInt64)rp->getBlackID();
std::string body;
json_util::serialize(resp_json, body);
conn->send(body);
}
private:
/* websocket断开连接处理函数 */
// websocket长连接建立断开之前的处理函数 -- 分为游戏大厅和游戏房间连接
void close_callback(websocketpp::connection_hdl hdl)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall") // 游戏大厅的长连接断开前处理
return close_game_hall(conn);
else if(uri == "/room") // 游戏房间的长连接断开前处理
return close_game_room(conn);
}
// 游戏大厅长连接断开前的处理
void close_game_hall(wsserver_t::connection_ptr& conn)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 将玩家从游戏大厅中移除
_online_user.exitHall(sp->getUserID());
// 3. 将session恢复生命周期的管理,设置定时销毁
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
}
// 游戏房间长连接断开前的处理
void close_game_room(wsserver_t::connection_ptr& conn)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 将玩家从在线用户管理中移除
_online_user.exitRoom(sp->getUserID());
// 3. 将session回复生命周期的管理,设置定时销毁
_session.set_session_expire_time(sp->getSessionID(), SESSION_EXPIRE_TIME);
// 4. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间
_room.removeUser(sp->getUserID());
}
private:
/* websocket收发消息处理函数 */
// websocket长连接通信处理
void message_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msgptr)
{
// 1. 获取http请求的uri,用于区分不同的请求类型
wsserver_t::connection_ptr conn = _server.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
// 2. 根据不同的请求类型调用对应的处理函数
if(uri == "/hall")
return message_in_hall(conn, msgptr); // 游戏大厅的收发信息处理
else if(uri == "/room")
return message_in_room(conn, msgptr); // 游戏房间的收发信息处理
}
// 游戏大厅的通信处理
void message_in_hall(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{
// 1. 获取session信息,在子函数中完成身份验证
session_ptr sp = websocket_get_session(conn);
if(sp.get() == nullptr)
return;
// 2. 获取消息的请求数据
std::string req_body = msgptr->get_payload();
Json::Value req_json;
bool ret = json_util::unserialize(req_body, req_json);
if(ret == false)
return websocket_response(conn, false, "", "请求信息解析失败");
// 3. 根据请求进行处理
if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start")
{
// 开始对战匹配:通过匹配模块,将用户添加到匹配队列中
_matcher.addUser(sp->getUserID());
return websocket_response(conn, true, "match_start", "开始匹配成功");
}
else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop")
{
// 停止对战匹配:通过匹配模块,将用户从匹配队列中移除
_matcher.delUser(sp->getUserID());
return websocket_response(conn, true, "match_stop", "停止匹配成功");
}
return websocket_response(conn, false, "unknow", "请求类型未知");
}
// 游戏房间的通信处理
void message_in_room(wsserver_t::connection_ptr& conn, wsserver_t::message_ptr& msgptr)
{
// 1. 获取session信息,在子函数中顺便进行登录验证
session_ptr sp = websocket_get_session(conn);
if (sp.get() == nullptr)
return;
// 2. 获取客户端房间信息
room_ptr rp = _room.getRoom_ByUserID(sp->getUserID());
if(rp.get() == nullptr)
{
DLOG("房间--没有找到玩家房间信息");
return websocket_response(conn, false, "unknow", "没有找到玩家的房间信息");
}
// 3. 对消息进行反序列化
Json::Value req_json;
std::string req_body = msgptr->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
if(ret == false)
{
DLOG("房间-反序列化请求失败");
return websocket_response(conn, false, "unknow", "请求解析失败");
}
DLOG("房间:收到房间请求,开始处理....");
// 4. 通过房间模块进行消息请求的处理
rp->request_handle(req_json);
}
private:
/* 辅助处理函数 */
// http响应处理通用函数
void http_response(wsserver_t::connection_ptr& conn,
bool result,
websocketpp::http::status_code::value status,
const std::string& reason)
{
// 1. 填写响应信息
Json::Value response;
response["result"] = result;
response["reason"] = reason;
// 2. 进行序列化
std::string response_body;
json_util::serialize(response, response_body);
// 3. 设置http响应,类型为application/json
conn->set_body(response_body);
conn->set_status(status);
conn->append_header("Content-Type", "application/json");
}
// websocket响应处理通用函数
void websocket_response(wsserver_t::connection_ptr& conn,
bool result,
const std::string& optype,
const std::string& reason)
{
Json::Value resp_json;
resp_json["optype"] = optype;
if(!optype.empty()) resp_json["result"] = result;
if(!reason.empty()) resp_json["reason"] = reason;
std::string body;
json_util::serialize(resp_json, body);
conn->send(body);
}
// 获取session_ptr -- 顺便验证登录
session_ptr websocket_get_session(wsserver_t::connection_ptr& conn)
{
// 1. 获取请求信息中的Cookie,从Cookie中获取ssid
std::string cookie_str = conn->get_request_header("Cookie");
if (cookie_str.empty())
{
// 如果没有cookie,返回错误:没有cookie信息,让客户端重新登录
websocket_response(conn, false, "hall_ready", "没有找到cookie信息,需要重新登录");
return session_ptr();
}
// 2. 从cookie中取出ssid
std::string ssid_str;
bool ret = get_sessionID_from_cookie(cookie_str, "SSID", ssid_str);
if (ret == false)
{
// cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录
websocket_response(conn, false, "hall_ready", "没有找到SSID信息,需要重新登录");
return session_ptr();
}
// 3. 在session管理中查找对应的会话信息
session_ptr sp = _session.get_session_by_sesssionID(std::stol(ssid_str));
if (sp.get() == nullptr)
{
// 没有找到session,则认为登录已经过期,需要重新登录
websocket_response(conn, false, "hall_ready", "登录已经过期,需要重新登录");
return session_ptr();
}
return sp;
}
// 从cookie中获取sessionID函数
bool get_sessionID_from_cookie(const std::string& cookie_str, const std::string& key, std::string &val)
{
// Cookie: SSID=XXX; path=/;
//1. 以 ; 作为分隔符,对字符串进行分割,得到各个单个的cookie信息
std::string sep = "; ";
std::vector<std::string> cookie_arr;
string_util::split(cookie_str, sep, cookie_arr);
for (auto &str : cookie_arr) {
//2. 对单个cookie字符串,以 = 为分隔符进行分割,得到key和val
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;
}
};
#endif