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

网站制作简单协议杭州 网站开发公司

网站制作简单协议,杭州 网站开发公司,网站建设具体详细过程,电销系统哪个好目录 数据管理模块#xff08;数据库设计#xff09; 前端界面模块 业务处理模块 会话管理模块网络通信模块(session,cookie) 在线管理模块 房间管理模块 用户匹配模块 项目扩展 数据管理模块#xff08;数据库设计#xff09; 数据库中有可能存在很多张表#xf…目录 数据管理模块数据库设计 前端界面模块 业务处理模块 会话管理模块网络通信模块(session,cookie) 在线管理模块 房间管理模块 用户匹配模块 项目扩展 数据管理模块数据库设计 数据库中有可能存在很多张表每张表中管理的数据⼜有不同要进⾏的数据操作也各不相同因此我们可以为每⼀张表中的数据操作都设计⼀个类通过类实例化的对象来访问这张数据库表中的数据这样的话当我们要访问哪张表的时候使⽤哪个类实例化的对象即可。 创建user_table类该类的作⽤是负责通过 MySQL 接⼝管理用户数据。主要提供了四个⽅法 • select_by_name:根据用户名查找用户信息用于实现登录功能 • insert:新增用户用户实现注册功能 • login:登录验证并获取完整的用户信息 • win:⽤于给获胜玩家修改分数 • lose:用户给失败玩家修改分数 #ifndef __M_DB_H__ #define __M_DB_H__ #include util.hpp #include mutex #include cassert class user_table{private:MYSQL *_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);// sprintf(void *buf, char *format, ...)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 passwordpassword(%s);char sql[4096] {0};sprintf(sql, LOGIN_USER, user[username].asCString(), user[password].asCString());MYSQL_RES *res NULL;{std::unique_lockstd::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_lockstd::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%d;char sql[4096] {0};sprintf(sql, USER_BY_ID, id);MYSQL_RES *res NULL;{std::unique_lockstd::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;}//胜利时天梯分数增加30分战⽃场次增加1胜利场次增加1bool win(uint64_t id) { #define USER_WIN update user set scorescore30, total_counttotal_count1, win_countwin_count1 where id%d;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;}//失败时天梯分数减少30战⽃场次增加1其他不变bool lose(uint64_t id) { #define USER_LOSE update user set scorescore-30, total_counttotal_count1 where id%d;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 前端界面模块 登录⻚⾯:login.html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title登录/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classlogin-container!-- 登录界⾯的对话框 --div classlogin-dialog!-- 提⽰信息 --h3登录/h3!-- 这个表⽰⼀⾏ --div classrowspan⽤⼾名/spaninput typetext iduser_name/div!-- 这是另⼀⾏ --div classrowspan密码/spaninput typepassword idpassword/div!-- 提交按钮 --div classrowbutton idsubmit onclicklogin()提交/button /div/div/divscript src./js/jquery.min.js/scriptscript//1. 给按钮添加点击事件调⽤登录请求函数//2. 封装登录请求函数function login() {// 1. 获取输⼊框中的⽤⼾名和密码并组织json对象var login_info {username: document.getElementById(user_name).value,password: document.getElementById(password).value};// 2. 通过ajax向后台发送登录验证请求$.ajax({url: /login,type: post,data: JSON.stringify(login_info),success: function(result) {// 3. 如果验证通过则跳转游戏⼤厅⻚⾯alert(登录成功);window.location.assign(/game_hall.html);},error: function(xhr) {// 4. 如果验证失败则提⽰错误信息并清空输⼊框alert(JSON.stringify(xhr));document.getElementById(user_name).value ;document.getElementById(password).value ;}})}/script /body /html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title登录/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classlogin-container!-- 登录界⾯的对话框 --div classlogin-dialog!-- 提⽰信息 --h3登录/h3!-- 这个表⽰⼀⾏ --div classrowspan⽤⼾名/spaninput typetext iduser_name/div!-- 这是另⼀⾏ --div classrowspan密码/spaninput typepassword idpassword/div!-- 提交按钮 --div classrowbutton idsubmit提交/button/div/div/divscript src./js/jquery.min.js/scriptscript// 获取⽤⼾在前端输⼊的⽤⼾名和密码let usernameInput document.getElementById(user_name);let passwordInput document.getElementById(password);let submitButton document.getElementById(submit);// 点击提交按钮的回调函数submitButton.onclick function() {// 通过 ajax 向服务器发起登录请求 实现登录功能$.ajax({// 构造请求type: post,url: /login,data: {username: usernameInput.value,password: passwordInput.value,},success: function(body) {alert(登录成功!);// 重定向跳转到 游戏⼤厅⻚⾯location.assign(/game_hall.html);},error: function(body) {alert(JSON.stringify(body));}});}/script /body /html 注册⻚⾯:register.html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title注册/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classlogin-container!-- 登录界⾯的对话框 --div classlogin-dialog!-- 提⽰信息 --h3注册/h3!-- 这个表⽰⼀⾏ --div classrowspan⽤⼾名/spaninput typetext iduser_name nameusername/div!-- 这是另⼀⾏ --div classrowspan密码/spaninput typepassword idpassword namepassword/div!-- 提交按钮 --div classrowbutton idsubmit提交/button/div/div/div script srcjs/jquery.min.js/script /body /html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title注册/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/login.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classlogin-container!-- 登录界⾯的对话框 --div classlogin-dialog!-- 提⽰信息 --h3注册/h3!-- 这个表⽰⼀⾏ --div classrowspan⽤⼾名/spaninput typetext iduser_name nameusername/div!-- 这是另⼀⾏ --div classrowspan密码/spaninput typepassword idpassword namepassword/div!-- 提交按钮 --div classrowbutton idsubmit onclickreg()提交/button/div/div/div script srcjs/jquery.min.js/scriptscript//1. 给按钮添加点击事件调⽤注册函数//2. 封装实现注册函数function reg() {// 1. 获取两个输⼊框空间中的数据组织成为⼀个json串var reg_info {username: document.getElementById(user_name).value,password: document.getElementById(password).value};console.log(JSON.stringify(reg_info));// 2. 通过ajax向后台发送⽤⼾注册请求$.ajax({url : /reg,type : post,data : JSON.stringify(reg_info),success : function(res) {if (res.result false) {// 4. 如果请求失败则清空两个输⼊框内容并提⽰错误原因 document.getElementById(user_name).value ;document.getElementById(password).value ;alert(res.reason);}else {// 3. 如果请求成功则跳转的登录⻚⾯alert(res.reason);window.location.assign(/login.html);}},error : function(xhr) {document.getElementById(user_name).value ;document.getElementById(password).value ;alert(JSON.stringify(xhr));}})}/script /body /html 游戏⼤厅⻚⾯game_hall.html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏⼤厅/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/game_hall.css /head bodydiv classnav⽹络五⼦棋对战游戏/div!-- 整个⻚⾯的容器元素 --div classcontainer!-- 这个 div 在 container 中是处于垂直⽔平居中这样的位置的 --div!-- 展⽰⽤⼾信息 --div idscreen玩家: ⼩⽩ 分数: 1860/br⽐赛场次: 23 获胜场次: 18/div!-- 匹配按钮 --div idmatch-button开始匹配/div/div/divscript src./js/jquery.min.js/script /body /html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏⼤厅/titlelink relstylesheet href./css/common.csslink relstylesheet href./css/game_hall.css /head bodydiv classnav⽹络五⼦棋对战游戏/div!-- 整个⻚⾯的容器元素 --div classcontainer!-- 这个 div 在 container 中是处于垂直⽔平居中这样的位置的 --div!-- 展⽰⽤⼾信息 --div idscreen/div!-- 匹配按钮 --div idmatch-button开始匹配/div/div/divscript src./js/jquery.min.js/scriptscriptvar 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 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 游戏房间⻚⾯game_room.html 在游戏房间⻚⾯中关于棋盘的绘制部分已经直接提供。  !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏房间/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_room.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classcontainerdiv idchess_area!-- 棋盘区域, 需要基于 canvas 进⾏实现 --canvas idchess width450px height450px/canvas!-- 显⽰区域 --div idscreen 等待玩家连接中... /div/divdiv idchat_area width400px height300pxdiv idchat_showp idself_msg你好/p/brp idpeer_msg你好/p/br/divdiv idmsg_showinput typetext idchat_inputbutton idchat_button发送/button/div/div/divscriptlet chessBoard [];let BOARD_ROW_AND_COL 15;let chess document.getElementById(chess);//获取chess控件区域2d画布let context chess.getContext(2d);function initGame() {initBoard();// 背景图⽚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;}}}// 绘制棋盘⽹格线function drawChessBoard() {context.strokeStyle #BFBFBF;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();//createLinearGradient() ⽅法创建放射状/圆形渐变对象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) {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);}initGame();/script /body /html !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title游戏房间/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_room.css /head bodydiv classnav⽹络五⼦棋对战游戏/divdiv classcontainerdiv idchess_area!-- 棋盘区域, 需要基于 canvas 进⾏实现 --canvas idchess width450px height450px/canvas!-- 显⽰区域 --div idscreen 等待玩家连接中... /div/divdiv idchat_area width400px height300pxdiv idchat_showp idself_msg你好/p/brp idpeer_msg你好/p/brp idpeer_msgleihoua~/p/br/divdiv idmsg_showinput typetext idchat_inputbutton idchat_button发送/button/div/div/divscriptlet 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.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;}}}// 绘制棋盘⽹格线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. 获取下棋位置判断当前下棋操作是否正常// 1. 当前是否轮到⾃⼰⾛棋了// 2. 当前位置是否已经被占⽤// 2. 向服务器发送⾛棋请求if (!is_me) {alert(等待对⽅⾛棋 ....);return;}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);//向服务器发送⾛棋请求收到响应后再绘制棋⼦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);//2. ⾛棋操作// 3. 收到⾛棋消息进⾏棋⼦绘制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);//设置棋盘信息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 ;}}//3. 聊天动作// 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 我们必须使⽤两个浏览器或者⼀个浏览器的⽆痕模式打开两个标签⻚避免cookie和session相互影响导致检测到多开。  业务处理模块 服务器模块是对当前所实现的所有模块的⼀个整合并进⾏服务器搭建的⼀个模块最终封装实现出⼀个gobang_server的服务器模块类向外提供搭建五⼦棋对战服务器的接口。通过实例化的对象可以简便的完成服务器的搭建。 通信接口设计Restful⻛格 静态资源请求   静态资源⻚⾯在后台服务器上就是个html/css/js⽂件静态资源请求的处理其实就是将⽂件中的内容发送给客⼾端 1. 注册⻚⾯请求 请求GET /register.html HTTP/1.1 响应 HTTP/1.1 200 OK Content-Length: xxx Content-Type: text/html register.html⽂件的内容数据 2. 登录⻚⾯请求 请求GET /login.html HTTP/1.1 3. ⼤厅⻚⾯请求 请求GET /game_hall.html HTTP/1.1 4. 房间⻚⾯请求 请求GET /game_room.html HTTP/1.1 注册用户 POST /reg HTTP/1.1 Content-Type: application/json Content-Length: 32 {username:xiaobai, password:123123} #成功时的响应 HTTP/1.1 200 OK Content-Type: application/json Content-Length: 15 {result:true} #失败时的响应 HTTP/1.1 400 Bad Request Content-Type: application/json Content-Length: 43 {result:false, reason: ⽤⼾名已经被占⽤} 用户登录 POST /login HTTP/1.1 Content-Type: application/json Content-Length: 32 {username:xiaobai, password:123123} #成功时的响应 HTTP/1.1 200 OK Content-Type: application/json Content-Length: 15 {result:true} #失败时的响应 HTTP/1.1 400 Bad Request Content-Type: application/json Content-Length: 43 {result:false, reason: ⽤⼾名或密码错误} 获取客户端信息 GET /userinfo HTTP/1.1 Content-Type: application/json Content-Length: 0 #成功时的响应 HTTP/1.1 200 OK Content-Type: application/json Content-Length: 58 {id:1, username:xiaobai, score:1000, total_count:4, win_count:2} #失败时的响应 HTTP/1.1 401 Unauthorized Content-Type: application/json Content-Length: 43 {result:false, reason: ⽤⼾还未登录} websocket⻓连接协议切换请求进⼊游戏⼤厅 /* ws://localhost:9000/match */ GET /match HTTP/1.1 Connection: Upgrade Upgrade: WebSocket ...... HTTP/1.1 101 Switching ...... {optype: hall_ready,uid: 1 } 开始对战匹配 {optype: match_start } /*后台正确处理后回复*/ {optype: match_start, //表⽰成功加⼊匹配队列 result: true } /*后台处理出错回复*/ {optype: match_startresult: false,reason: 具体原因 .... } /*匹配成功了给客⼾端的回复*/ {optype: match_success, //表⽰成匹配成功 result: true } 停止匹配   {optype: match_stop } /*后台正确处理后回复*/ {optype: match_stopresult: true } /*后台处理出错回复*/ {optype: match_stopresult: false,reason: 具体原因 .... } websocket⻓连接协议切换请求进⼊游戏房间   /* ws://localhost:9000/game */ GET /game HTTP/1.1 Connection: Upgrade Upgrade: WebSocket ...... HTTP/1.1 101 Switching ...... /*协议切换成功 房间已经建⽴*/ {optype: room_ready,room_id: 222, //房间IDself_id: 1, //⾃⾝IDwhite_id: 1, //⽩棋IDblack_id: 2, //⿊棋ID } 走棋 {optype: put_chess, // put_chess表⽰当前请求是下棋操作 room_id: 222, // room_id 表⽰当前动作属于哪个房间 uid: 1, // 当前的下棋操作是哪个⽤⼾发起的row: 3, // 当前下棋位置的⾏号 col: 2 // 当前下棋位置的列号} {optype: put_chess,result: falsereason: ⾛棋失败具体原因 .... } {optype: put_chess,result: true,reason: 对⽅掉线不战⽽胜 / 对⽅/⼰⽅五星连珠战⽆敌/虽败犹荣, room_id: 222,uid: 1,row: 3,col: 2,winner: 0 // 0-未分胜负 !0-已分胜负 (uid是谁谁就赢了) } 聊天 {optype: chat,room_id: 222,uid: 1,message: 赶紧点 } {optype: chat,result: falsereason: 聊天失败具体原因....⽐如有敏感词 } {optype: chat,result: true,room_id: 222,uid: 1,message: 赶紧点 } 服务器模块实现   #ifndef __M_SRV_H__ #define __M_SRV_H__ #include db.hpp #include matcher.hpp #include online.hpp #include room.hpp #include session.hpp #include util.hpp #define WWWROOT ./wwwroot/ class gobang_server{private:std::string _web_root;//静态资源根⽬录 ./wwwroot/ /register.html - ./wwwroot/register.htmlwsserver_t _wssrv;user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;private:void file_handler(wsserver_t::connection_ptr conn) {//静态资源请求的处理//1. 获取到请求uri-资源路径了解客⼾端请求的⻚⾯⽂件名称websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();//2. 组合出⽂件的实际路径 相对根⽬录 uristd::string realpath _web_root uri;//3. 如果请求的是个⽬录增加⼀个后缀 login.html, / - /login.htmlif (realpath.back() /) {realpath login.html;}//4. 读取⽂件内容Json::Value resp_json;std::string body;bool ret file_util::read(realpath, body);// 1. ⽂件不存在读取⽂件内容失败返回404if (ret false) {body html;body head;body meta charsetUTF-8/;body /head;body body;body h1 Not Found /h1;body /body;conn-set_status(websocketpp::http::status_code::not_found);conn-set_body(body);return;}//5. 设置响应正⽂conn-set_body(body);conn-set_status(websocketpp::http::status_code::ok);}void http_resp(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();//1. 获取到请求正⽂std::string req_body conn-get_request_body();//2. 对正⽂进⾏json反序列化得到⽤⼾名和密码Json::Value login_info;bool ret json_util::unserialize(req_body, login_info);if (ret false) {DLOG(反序列化注册信息失败);return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请求的正⽂格式错误);}//3. 进⾏数据库的⽤⼾新增操作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, ⽤⼾名已经被占⽤!);}// 如果成功了则返回200return http_resp(conn, true, websocketpp::http::status_code::ok, 注册⽤⼾成功);}void login(wsserver_t::connection_ptr conn) {//⽤⼾登录功能请求的处理//1. 获取请求正⽂并进⾏json反序列化得到⽤⼾名和密码std::string req_body conn-get_request_body();Json::Value login_info;bool ret json_util::unserialize(req_body, login_info);if (ret false) {DLOG(反序列化登录信息失败);return http_resp(conn, false, websocketpp::http::status_code::bad_request, 请求的正⽂格式错误);}//2. 校验正⽂完整性进⾏数据库的⽤⼾信息验证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) {// 1. 如果验证失败则返回400DLOG(⽤⼾名密码错误);return http_resp(conn, false, websocketpp::http::status_code::bad_request, ⽤⼾名密码错误);}//3. 如果验证成功给客⼾端创建sessionuint64_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);//4. 设置响应头部Set-Cookie,将sessionid通过cookie返回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 , 登录成功);}bool get_cookie_val(const std::string cookie_str, const std::string key, std::string val) {// Cookie: SSIDXXX; path/; //1. 以 ; 作为间隔对字符串进⾏分割得到各个单个的cookie信息std::string sep ; ;std::vectorstd::string cookie_arr;string_util::split(cookie_str, sep, cookie_arr);for (auto str : cookie_arr) {//2. 对单个cookie字符串以 为间隔进⾏分割得到key和valstd::vectorstd::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;// 1. 获取请求信息中的Cookie从Cookie中获取ssidstd::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()) {//如果没有cookie返回错误没有cookie信息让客⼾端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, 找不到cookie信息请重新登录); }// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret get_cookie_val(cookie_str, SSID, ssid_str);if (ret false) {//cookie中没有ssid返回错误没有ssid信息让客⼾端重新登录 return http_resp(conn, true, websocketpp::http::status_code::bad_request, 找不到ssid信息请重新登录); }// 2. 在session管理中查找对应的会话信息session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() nullptr) {//没有找到session则认为登录已经过期需要重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, 登录过期请重新登录);}// 3. 从数据库中取出⽤⼾信息进⾏序列化发送给客⼾端uint64_t uid ssp-get_user();Json::Value user_info;ret _ut.select_by_id(uid, user_info);if (ret false) {//获取⽤⼾信息失败返回错误找不到⽤⼾信息return http_resp(conn, true, websocketpp::http::status_code::bad_request, 找不到⽤⼾信息请重新登录); }std::string body;json_util::serialize(user_info, body);conn-set_body(body);conn-append_header(Content-Type, application/json);conn-set_status(websocketpp::http::status_code::ok);// 4. 刷新session的过期时间_sm.set_session_expire_time(ssp-ssid(), SESSION_TIMEOUT);}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();if (method POST uri /reg) {return reg(conn);}else if (method POST uri /login) {return login(conn);}else if (method GET uri /info) {return info(conn);}else {return file_handler(conn);}}void ws_resp(wsserver_t::connection_ptr conn, Json::Value resp) {std::string body;json_util::serialize(resp, body);conn-send(body);}session_ptr get_session_by_cookie(wsserver_t::connection_ptr conn) {Json::Value err_resp;// 1. 获取请求信息中的Cookie从Cookie中获取ssidstd::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()) {//如果没有cookie返回错误没有cookie信息让客⼾端重新登录err_resp[optype] hall_ready;err_resp[reason] 没有找到cookie信息需要重新登录;err_resp[result] false;ws_resp(conn, err_resp);return session_ptr();}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret get_cookie_val(cookie_str, SSID, ssid_str);if (ret false) {//cookie中没有ssid返回错误没有ssid信息让客⼾端重新登录err_resp[optype] hall_ready;err_resp[reason] 没有找到SSID信息需要重新登录;err_resp[result] false;ws_resp(conn, err_resp);return session_ptr();}// 2. 在session管理中查找对应的会话信息session_ptr ssp _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() nullptr) {//没有找到session则认为登录已经过期需要重新登录err_resp[optype] hall_ready;err_resp[reason] 没有找到session信息需要重新登录;err_resp[result] false;ws_resp(conn, err_resp);return session_ptr();}return ssp;}void wsopen_game_hall(wsserver_t::connection_ptr conn) {//游戏⼤厅⻓连接建⽴成功Json::Value resp_json;//1. 登录验证--判断当前客⼾端是否已经成功登录session_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {return;}//2. 判断当前客⼾端是否是重复登录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);}//3. 将当前客⼾端以及连接加⼊到游戏⼤厅_om.enter_game_hall(ssp-get_user(), conn);//4. 给客⼾端响应游戏⼤厅连接建⽴成功resp_json[optype] hall_ready;resp_json[result] true;ws_resp(conn, resp_json);//5. 记得将session设置为永久存在_sm.set_session_expire_time(ssp-ssid(), SESSION_FOREVER);}void wsopen_game_room(wsserver_t::connection_ptr conn) {Json::Value resp_json;//1. 获取当前客⼾端的sessionsession_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {return;}//2. 当前⽤⼾是否已经在在线⽤⼾管理的游戏房间或者游戏⼤厅中---在线⽤⼾管理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);}//3. 判断当前⽤⼾是否已经创建好了房间 --- 房间管理room_ptr rp _rm.get_room_by_uid(ssp-get_user());if (rp.get() nullptr) {resp_json[optype] room_ready;resp_json[reason] 没有找到玩家的房间信息;resp_json[result] false;return ws_resp(conn, resp_json);}//4. 将当前⽤⼾添加到在线⽤⼾管理的游戏房间中_om.enter_game_room(ssp-get_user(), conn);//5. 将session重新设置为永久存在_sm.set_session_expire_time(ssp-ssid(), SESSION_FOREVER);//6. 回复房间准备完毕resp_json[optype] room_ready;resp_json[result] true;resp_json[room_id] (Json::UInt64)rp-id();resp_json[uid] (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) {//websocket⻓连接建⽴成功之后的处理函数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) {//游戏⼤厅⻓连接断开的处理//1. 登录验证--判断当前客⼾端是否已经成功登录session_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {return;}//1. 将玩家从游戏⼤厅中移除_om.exit_game_hall(ssp-get_user());//2. 将session恢复⽣命周期的管理设置定时销毁_sm.set_session_expire_time(ssp-ssid(), SESSION_TIMEOUT);}void wsclose_game_room(wsserver_t::connection_ptr conn) {//获取会话信息识别客⼾端session_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {return;}//1. 将玩家从在线⽤⼾管理中移除_om.exit_game_room(ssp-get_user());//2. 将session回复⽣命周期的管理设置定时销毁_sm.set_session_expire_time(ssp-ssid(), SESSION_TIMEOUT);//3. 将玩家从游戏房间中移除房间中所有⽤⼾退出了就会销毁房间_rm.remove_room_user(ssp-get_user());}void wsclose_callback(websocketpp::connection_hdl hdl) {//websocket连接断开前的处理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;std::string resp_body;//1. ⾝份验证当前客⼾端到底是哪个玩家session_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {return;}//2. 获取请求信息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);}//3. 对于请求进⾏处理if (!req_json[optype].isNull() req_json[optype].asString() match_start){// 开始对战匹配通过匹配模块将⽤⼾添加到匹配队列中_mm.add(ssp-get_user());resp_json[optype] match_start;resp_json[result] true;return ws_resp(conn, resp_json);}else if (!req_json[optype].isNull() req_json[optype].asString() match_stop) {// 停⽌对战匹配通过匹配模块将⽤⼾从匹配队列中移除_mm.del(ssp-get_user());resp_json[optype] match_stop;resp_json[result] true;return ws_resp(conn, resp_json);}resp_json[optype] unknow;resp_json[reason] 请求类型未知;resp_json[result] false;return ws_resp(conn, resp_json);}void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {Json::Value resp_json;//1. 获取客⼾端session识别客⼾端⾝份session_ptr ssp get_session_by_cookie(conn);if (ssp.get() nullptr) {DLOG(房间-没有找到会话信息);return;}//2. 获取客⼾端房间信息room_ptr rp _rm.get_room_by_uid(ssp-get_user());if (rp.get() nullptr) {resp_json[optype] unknow;resp_json[reason] 没有找到玩家的房间信息;resp_json[result] false;DLOG(房间-没有找到玩家房间信息);return ws_resp(conn, resp_json);}//3. 对消息进⾏反序列化Json::Value req_json;std::string req_body msg-get_payload();bool ret json_util::unserialize(req_body, req_json);if (ret false) {resp_json[optype] unknow;resp_json[reason] 请求解析失败;resp_json[result] false;DLOG(房间-反序列化请求失败);return ws_resp(conn, resp_json);}DLOG(房间收到房间请求开始处理 ....);//4. 通过房间模块进⾏消息请求的处理return rp-handle_request(req_json);}void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {//websocket⻓连接通信处理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);}}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();} }; #endif 会话管理模块网络通信模块session,cookie) session类设计实现 • 这⾥我们简单的设计⼀个session类但是session对象不能⼀直存在这样是⼀种资源泄漏因此需要使⽤定时器对每个创建的session对象进⾏定时销毁⼀个客户端连接断开后⼀段时间内都没有重新连接则销毁session。 • _ssid使⽤时间戳填充。实际上我们通常使⽤唯⼀id⽣成器⽣成⼀个唯⼀的id • _user保存当前用户的信息 • timer_ptr_tp保存当前session对应的定时销毁任务 typedef enum {UNLOGIN, LOGIN} ss_statu; class session {private:uint64_t _ssid;//标识符uint64_t _uid;//session对应的⽤⼾IDss_statu _statu;//⽤⼾状态未登录已登录wsserver_t::timer_ptr _tp;//session关联的定时器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; } }; session管理设计实现 session的管理主要包含以下⼏个点 1. 创建⼀个新的session 2. 通过ssid获取session 3. 通过ssid判断session是否存在 4. 销毁session 5. 为session设置过期时间过期后session被销毁 #ifndef __M_SS_H__ #define __M_SS_H__ #include util.hpp #include unordered_map #include websocketpp/server.hpp #include websocketpp/config/asio_no_tls.hpp #define SESSION_TIMEOUT 30000 #define SESSION_FOREVER -1 using session_ptr std::shared_ptrsession; class session_manager {private:uint64_t _next_ssid;std::mutex _mutex;std::unordered_mapuint64_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_lockstd::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_lockstd::mutex lock(_mutex);_session.insert(std::make_pair(ssp-ssid(), ssp));}session_ptr get_session_by_ssid(uint64_t ssid) {std::unique_lockstd::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_lockstd::mutex lock(_mutex);_session.erase(ssid);}void set_session_expire_time(uint64_t ssid, int ms) {//依赖于websocketpp的定时器来完成session⽣命周期的管理。// 登录之后创建sessionsession需要在指定时间⽆通信后删除 // 但是进⼊游戏⼤厅或者游戏房间这个session就应该永久存在 // 等到退出游戏⼤厅或者游戏房间这个session应该被重新设置为临时在⻓时间⽆通信后被删除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) {// 1. 在session永久存在的情况下设置永久存在return ;}else if (tp.get() nullptr ms ! SESSION_FOREVER) {// 2. 在session永久存在的情况下设置指定时间之后被删除的定时任务 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) {// 3. 在session设置了定时删除的情况下将session设置为永久存在 // 删除定时任务--- stready_timer删除定时任务会导致任务直接被执⾏ tp-cancel();//因为这个取消定时任务并不是⽴即取消的//因此重新给session管理器中添加⼀个session信息, 且添加的时候需要使⽤定时器⽽不是⽴即添加ssp-set_timer(wsserver_t::timer_ptr());//将session关联的定时器设置为空_server-set_timer(0, std::bind(session_manager::append_session, this, ssp));}else if (tp.get() ! nullptr ms ! SESSION_FOREVER) {// 4. 在session设置了定时删除的情况下将session重置删除时间。 tp-cancel();//因为这个取消定时任务并不是⽴即取消的ssp-set_timer(wsserver_t::timer_ptr());_server-set_timer(0, std::bind(session_manager::append_session, this, ssp));//重新给session添加定时销毁任务wsserver_t::timer_ptr tmp_tp _server-set_timer(ms, std::bind(session_manager::remove_session, this, ssp- ssid()));//重新设置session关联的定时器ssp-set_timer(tmp_tp);}} }; #endif 在线管理模块 在线用户管理是对于当前游戏⼤厅和游戏房间中的用户进⾏管理主要是建⽴起用户与Socket连接的映射关系这个模块具有两个功能 1. 能够让程序中根据用户信息进⽽找到能够与用户客户端进⾏通信的Socket连接进⽽实现与客户端的通信。 2. 判断⼀个用户是否在线或者判断用户是否已经掉线 class online_manager {private:/*游戏⼤厅的客⼾端连接管理*/std::unordered_mapuint64_t, websocket_server::connection_ptr _game_hall;/*游戏房间的客⼾端连接管理*/std::unordered_mapuint64_t, websocket_server::connection_ptr _game_room;std::mutex _mutex;public:/*进⼊游戏⼤厅--游戏⼤厅连接建⽴成功后调⽤*/void enter_game_hall(uint64_t uid, const websocket_server::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);_game_hall.insert(std::make_pair(uid, conn));}/*退出游戏⼤厅--游戏⼤厅连接断开后调⽤*/void exit_game_hall(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);_game_hall.erase(uid);}/*进⼊游戏房间--游戏房间连接建⽴成功后调⽤*/void enter_game_room(uint64_t uid, const websocket_server::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);_game_room.insert(std::make_pair(uid, conn));}/*退出游戏房间--游戏房间连接断开后调⽤*/void exit_game_room(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);_game_room.erase(uid);}/*判断⽤⼾是否在游戏⼤厅*/bool in_game_hall(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);auto it _game_hall.find(uid);if (it _game_hall.end()) {return false;}return true;}/*判断⽤⼾是否在游戏房间*/bool in_game_room(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);auto it _game_room.find(uid);if (it _game_room.end()) {return false;}return true;}/*从游戏⼤厅中获取指定⽤⼾关联的Socket连接*/bool get_conn_from_game_hall(uint64_t uid, websocket_server::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);auto it _game_hall.find(uid);if (it _game_hall.end()) {return false;}conn it-second;return true;}/*从游戏房间中获取指定⽤⼾关联的Socket连接*/bool get_conn_from_game_room(uint64_t uid, websocket_server::connection_ptr conn) {std::unique_lockstd::mutex lock(_mutex);auto it _game_room.find(uid);if (it _game_room.end()) {return false;}conn it-second;return true;} }; 房间管理模块 房间类实现 ⾸先需要设计⼀个房间类能够实现房间的实例化房间类主要是对匹配成对的玩家建⽴⼀个小范围的关联关系⼀个房间中任意⼀个用户发⽣的任何动作都会被⼴播给房间中的其他用户。 ⽽房间中的动作主要包含两类 1. 棋局对战 2. 实时聊天 #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::vectorstd::vectorint _board;private:bool five(int row, int col, int row_off, int col_off, int color) {//row和col是下棋位置 row_off和col_off是偏移量也是⽅向int count 1;int search_row row row_off;int search_col col col_off;while(search_row 0 search_row BOARD_ROW search_col 0 search_col BOARD_COL _board[search_row][search_col] color) {//同⾊棋⼦数量count;//检索位置继续向后偏移search_row row_off;search_col col_off;}search_row row - row_off;search_col col - col_off;while(search_row 0 search_row BOARD_ROW search_col 0 search_col BOARD_COL _board[search_row][search_col] color) {//同⾊棋⼦数量count;//检索位置继续向后偏移search_row - row_off;search_col - col_off;}return (count 5);}uint64_t check_win(int row, int col, int color) {// 从下棋位置的四个不同⽅向上检测是否出现了5个及以上相同颜⾊的棋⼦横⾏纵列正斜反斜if (five(row, col, 0, 1, color) || five(row, col, 1, 0, color) ||five(row, col, -1, 1, color)||five(row, col, -1, -1, color)) {//任意⼀个⽅向上出现了true也就是五星连珠则设置返回值return color CHESS_WHITE ? _white_id : _black_id;}return 0;}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::vectorint(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;// 2. 判断房间中两个玩家是否都在线任意⼀个不在线就是另⼀⽅胜利。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;}// 3. 获取⾛棋位置判断当前⾛棋是否合理位置是否已经被占⽤if (_board[chess_row][chess_col] ! 0) {json_resp[result] false;json_resp[reason] 当前位置已经有了其他棋⼦;return json_resp;}int cur_color cur_uid _white_id ? CHESS_WHITE : CHESS_BLACK;_board[chess_row][chess_col] cur_color;// 4. 判断是否有玩家胜利从当前⾛棋位置开始判断是否存在五星连珠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) {//1. 校验房间号是否匹配Json::Value json_resp;uint64_t room_id req[room_id].asUInt64();if (room_id ! _room_id) {json_resp[optype] req[optype].asString();json_resp[result] false;json_resp[reason] 房间号不匹配;return broadcast(json_resp);}//2. 根据不同的请求类型调⽤不同的处理函数if (req[optype].asString() put_chess) {json_resp handle_chess(req);if (json_resp[winner].asUInt64() ! 0) {uint64_t winner_id json_resp[winner].asUInt64();uint64_t loser_id winner_id _white_id ? _black_id : _white_id;_tb_user-win(winner_id);_tb_user-lose(loser_id);_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) {//1. 对要响应的信息进⾏序列化将Json::Value中的数据序列化成为json格式字符串std::string body;json_util::serialize(rsp, body);//2. 获取房间中所有⽤⼾的通信连接//3. 发送响应信息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;} }; 房间管理类实现 实现对所有的游戏房间进⾏管理。  #ifndef __M_ROOM_H__ #define __M_ROOM_H__ #include util.hpp #include logger.hpp #include online.hpp #include db.hpp using room_ptr std::shared_ptrroom; class room_manager{private:uint64_t _next_rid;std::mutex _mutex;user_table *_tb_user;online_manager *_online_user;std::unordered_mapuint64_t, room_ptr _rooms;std::unordered_mapuint64_t, uint64_t _users;public:/*初始化房间ID计数器*/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) {//两个⽤⼾在游戏⼤厅中进⾏对战匹配匹配成功后创建房间//1. 校验两个⽤⼾是否都还在游戏⼤厅中只有都在才需要创建房间。 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();}//2. 创建房间将⽤⼾信息添加到房间中std::unique_lockstd::mutex lock(_mutex);room_ptr rp(new room(_next_rid, _tb_user, _online_user));rp-add_white_user(uid1);rp-add_black_user(uid2);//3. 将房间信息管理起来_rooms.insert(std::make_pair(_next_rid, rp));_users.insert(std::make_pair(uid1, _next_rid));_users.insert(std::make_pair(uid2, _next_rid));_next_rid;//4. 返回房间信息return rp;}/*通过房间ID获取房间信息*/room_ptr get_room_by_rid(uint64_t rid) {std::unique_lockstd::mutex lock(_mutex);auto it _rooms.find(rid);if (it _rooms.end()) {return room_ptr();}return it-second;}/*通过⽤⼾ID获取房间信息*/room_ptr get_room_by_uid(uint64_t uid) {std::unique_lockstd::mutex lock(_mutex);//1. 通过⽤⼾ID获取房间IDauto uit _users.find(uid);if (uit _users.end()) {return room_ptr();}uint64_t rid uit-second;//2. 通过房间ID获取房间信息auto rit _rooms.find(rid);if (rit _rooms.end()) {return room_ptr();}return rit-second;}/*通过房间ID销毁房间*/void remove_room(uint64_t rid) {//因为房间信息是通过shared_ptr在_rooms中进⾏管理因此只要将shared_ptr从_rooms中移除//则shared_ptr计数器0外界没有对房间信息进⾏操作保存的情况下就会释放 //1. 通过房间ID获取房间信息room_ptr rp get_room_by_rid(rid);if (rp.get() nullptr) {return;}//2. 通过房间信息获取房间中所有⽤⼾的IDuint64_t uid1 rp-get_white_user();uint64_t uid2 rp-get_black_user();//3. 移除房间管理中的⽤⼾信息std::unique_lockstd::mutex lock(_mutex);_users.erase(uid1);_users.erase(uid2);//4. 移除房间管理信息_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 ;} }; #endif 用户匹配模块 匹配队列实现 五子棋对战的玩家匹配是根据自己的天梯分数进⾏匹配的而服务器中将玩家天梯分数分为三个档次 1. 青铜天梯分数小于2000分 2. 白银天梯分数介于2000~3000分之间 3. 黄金天梯分数⼤于3000分 ⽽实现玩家匹配的思想⾮常简单为不同的档次设计的各自匹配队列当⼀个队列中的玩家数量⼤于等于2的时候则意味着同⼀档次中有2个及以上的⼈要进⾏实战匹配则出队队列中的前两个用户相当于队⾸2个玩家匹配成功这时候为其创建房间并将两个用户信息加⼊房间中。 template class T class match_queue {private:/*⽤链表⽽不直接使⽤queue是因为我们有中间删除数据的需要*/std::listT _list;/*实现线程安全*/std::mutex _mutex;/*这个条件变量主要为了阻塞消费者后边使⽤的时候队列中元素个数2则阻塞*/ std::condition_variable _cond;public:/*获取元素个数*/int size() { std::unique_lockstd::mutex lock(_mutex);return _list.size(); }/*判断是否为空*/bool empty() {std::unique_lockstd::mutex lock(_mutex);return _list.empty();}/*阻塞线程*/void wait() {std::unique_lockstd::mutex lock(_mutex);_cond.wait(lock);}/*⼊队数据并唤醒线程*/void push(const T data) {std::unique_lockstd::mutex lock(_mutex);_list.push_back(data);_cond.notify_all();}/*出队数据*/bool pop(T data) {std::unique_lockstd::mutex lock(_mutex);if (_list.empty() true) {return false;}data _list.front();_list.pop_front();return true;}/*移除指定的数据*/void remove(T data) {std::unique_lockstd::mutex lock(_mutex);_list.remove(data);} }; 玩家匹配管理模块设计实现 #ifndef __M_MATCHER_H__ #define __M_MATCHER_H__ #include util.hpp #include online.hpp #include db.hpp #include room.hpp #include list #include mutex #include condition_variable template class T class match_queue {private:/*⽤链表⽽不直接使⽤queue是因为我们有中间删除数据的需要*/std::listT _list;/*实现线程安全*/std::mutex _mutex;/*这个条件变量主要为了阻塞消费者后边使⽤的时候队列中元素个数2则阻塞*/ std::condition_variable _cond;public:/*获取元素个数*/int size() { std::unique_lockstd::mutex lock(_mutex);return _list.size(); }/*判断是否为空*/bool empty() {std::unique_lockstd::mutex lock(_mutex);return _list.empty();}/*阻塞线程*/void wait() {std::unique_lockstd::mutex lock(_mutex);_cond.wait(lock);}/*⼊队数据并唤醒线程*/void push(const T data) {std::unique_lockstd::mutex lock(_mutex);_list.push_back(data);_cond.notify_all();}/*出队数据*/bool pop(T data) {std::unique_lockstd::mutex lock(_mutex);if (_list.empty() true) {return false;}data _list.front();_list.pop_front();return true;}/*移除指定的数据*/void remove(T data) {std::unique_lockstd::mutex lock(_mutex);_list.remove(data);} }; class matcher {private:/*普通选⼿匹配队列*/match_queueuint64_t _q_normal;/*⾼⼿匹配队列*/match_queueuint64_t _q_high;/*⼤神匹配队列*/match_queueuint64_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_queueuint64_t mq) {while(1) {//1. 判断队列⼈数是否⼤于22则阻塞等待while (mq.size() 2) {mq.wait();} //2. ⾛下来代表⼈数够了出队两个玩家uint64_t uid1, uid2;bool ret mq.pop(uid1);if (ret false) { continue; }ret mq.pop(uid2);if (ret false) { this-add(uid1); continue; }//3. 校验两个玩家是否在线如果有⼈掉线则要吧另⼀个⼈重新添加⼊队列 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;}//4. 为两个玩家创建房间并将玩家加⼊房间中 room_ptr rp _rm-create_room(uid1, uid2); if (rp.get() nullptr) {this-add(uid1);this-add(uid2);continue;}//5. 对两个玩家进⾏响应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) {//根据玩家的天梯分数来判定玩家档次添加到不同的匹配队列// 1. 根据⽤⼾ID获取玩家信息Json::Value user;bool ret _ut-select_by_id(uid, user);if (ret false) {DLOG(获取玩家:%d 信息失败, uid);return false;}int score user[score].asInt();// 2. 添加到指定的队列中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();// 2. 添加到指定的队列中if (score 2000) {_q_normal.remove(uid);}else if (score 2000 score 3000) {_q_high.remove(uid);}else {_q_super.remove(uid);}return true;} }; #endif 项目扩展 实现局时/步时 • 局时:⼀局游戏中玩家能思考的总时间 • 步时:⼀步落⼦过程中玩家能思考的时间 保存棋谱录像回放 • 服务器可以把每⼀局对局、玩家轮流落⼦的位置都记录下来 • 玩家可以在游戏⼤厅⻚⾯选定某个曾经的⽐赛在⻚⾯上回放出对局的过程 观战功能• 在游戏⼤厅显⽰当前所有的对局房间 • 玩家可以选中某个房间以观众的形式加⼊到房间中实时的看到选⼿的对局情况 虚拟对⼿⼈机对战 • 如果当前⻓时间匹配不到选⼿则⾃动分配⼀个AI对⼿实现⼈机对战
http://wiki.neutronadmin.com/news/15949/

相关文章:

  • 购物网站如何推广wordpress建站原理
  • 火的网站建设明细报价表电子商务c2c模式
  • 网站建设样板西安网络公司网站建设
  • 中国交通建设工程监督管理局网站wordpress如何换成经典编辑器
  • 为什么找不到做网站的软件广告公司网站建设
  • 非交互式网站建网站有哪些文件夹
  • 北京沙河教做网站的黔西网站建设
  • 大连做公司网站哪家好做旅游宣传不错的网站
  • 贵阳做网站的服装设计素材网站
  • 松原手机网站开发公司电话专业的广州手机网站
  • 长沙建站找有为太极环境遵网站被做暗链报告
  • 关于建设集团公司网站的报告青岛网站建设培训
  • 公众号怎么做网站多种语言的网站
  • 上海电商网站设计河源网站搭建费用
  • 校园网站规划与建设心得高校建设思政教育网站案例
  • 如何做网站小编seo刷排名工具
  • 全能网站建设教程杭州建设网站 网站建设
  • 南昌网站网页设计中铁建设集团招标网站
  • 回力网站建设初衷微信小程序卖货平台
  • 商城网站建设价位seo运营招聘
  • 一对一直播网站开发微信网站建设平台
  • 中文域名网站建设做网站广告有哪些职位
  • 凡科网站产品导航怎么做莆田外贸网站建设
  • 衡阳微信网站开发上传网站步骤
  • 网站建设运营知乎现在的网站用什么程序做
  • 滨州做网站公司自己做的网站怎么链接火车头采集
  • 哪家建网站wordpress作者页制作
  • 网站营销推广公司免费模板网站都有什么用
  • 2手房产App网站开发app小程序网站开发
  • 大邑做网站永康好口碑关键词优化