项目——网络对战五子棋(Web-Gobang)
Posted It‘s so simple
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目——网络对战五子棋(Web-Gobang)相关的知识,希望对你有一定的参考价值。
目录
项目源码:https://github.com/mxzw/Project/tree/main/webgobang-project/src
1. 项目开发背景
小时候,闲来无事最喜欢和别人玩的游戏之一就是五子棋了,它规则简单,但却玩法多变,直到现在我还时不时的在手机上和别人在线对战。最近自己学习Linux方面的相关知识,学习了多线程和Socket编程,并对网络这块的数据流向有了基本的了解,加之刚好找到了一份纯前端的单机五子棋的源码,于是就想尝试能否在该前端的基础上,加上后端,使其变成联机版的网络在线对战五子棋。
由于本人对前端学习并不深入,所有前端的代码都是基于原来现有的前端代码进行的修改。
项目运行截图
用户登录:
用户注册:
下棋页面:
胜利页面:
2.项目需求 & 开发环境
项目需求
- 支持用户登录、用户注册功能
- 支持在线匹配对战功能
- 支持点击重开按钮重新进行匹配功能
- 支持对局信息持久化功能
开发环境
gcc/g++:7.3.1版本,使用C++语言进行开发,并采用mysql数据库进行相应数据的存储。
项目所用到的相关知识点
多线程,互斥锁和条件变量,httplib,JSON,AJAX,MySQL数据库
3.项目设计 & 项目难点
项目设计
首先,本项目大体的数据流向是前端通过AJAX方法将JSON格式的数据传送到后端,后端通过HttpLib库中的方法获取到前端当中的JSON数据,然后对该数据进行相应逻辑的处理后,将结果返回给前端,前端根据结果显示相应的内容。
项目架构图如下:
项目难点:
- 难点1: 当用户点击开始匹配按钮时,创建房间并为用户分配相应的对手。
- 难点2:在对战过程中,用户只能下一种颜色的棋子(黑棋/白棋),并且只能在属于自己的回合进行下棋。
- 难点3:在对战过程中,对手下的棋子的位置能够及时的同步在自己的棋盘上。
4.功能实现 & 难点解决
由项目设计,我们可以得到本项目大概的框架
- HTTP模块:httplib.h
- 用户管理模块和房间管理模块:room_player.hpp
- 会话模块:Session.hpp
- 数据库管理模块:ManagerDB.hpp
- 工具模块:tools.hpp
- 项目驱动模块:webgobang.hpp、webgobang.cpp
4.1. 用户登录 / 注册
当用户在网页上点击登录按钮的时候,前端会对用户输入的邮箱和密码进行获取,将数据转为JSON格式(序列化),并通过AJAX,采用POST的http请求方法将该数据发送到后端去,后端校验该数据之后,将其持久化到数据库中,并且根据输入的邮箱和密码生成相应的MD5码作为当前用户的cookie信息返回回去。
登录页面后端代码如下:
http_svr_.Post("/login", [=](const Request& req, Response& res) {
//1.校验用户(登录页面)所提交的邮箱和密码(与数据库进行比较)
cout << req.body << endl;
Json::Reader r;
Json::Value v; //存储string :string 的键值对
r.parse(req.body, v); // 用于将请求正文中的内容反序列化(从连续的二进制转为对应键值对)
//cout <<"v[\\"email\\"]" <<v["email"] << endl;
//cout <<"v[\\"password\\"]" <<v["password"] << endl;
//
// 1.1 同数据库进行校验并得到对应user_id
int user_id = this->db_svr_->CheckUserExist(v);
//1.2 为了防止用户不经过登录,直接访问主页面,我们需要在这里加上一个session会话
// 设置MD5码,作为会话id 进行http响应
string tmp = "";
if (user_id > 0)
{
Session sess(v, user_id);
string session_id = sess.GetSessionID();
//回复的会话格式为JSESSIONID = xxxxx
tmp += "JSESSIONID=" + session_id;
all_sess_->SetSessionInfo(session_id, sess);
//将该用户放到用户管理类中进行管理
this->pm_->insertPlayer2Map(user_id);
}
//2.组织http响应格式使用校验的结果进行响应(使用Json对其进行响应,因为ajax中接收的数据类型为json)
Json::Value res_value;
// 在前端ajax请求中,要求接收的是一个Json对象,并且该对象中含有status的键值对
res_value["status"] = user_id >= 0 ? true : false;
//将该Json-Value对象序列化 并进行响应
res.body = Serializa(res_value);
res.set_header("Set-Cookie", tmp.c_str());
res.set_header("content-Type", "application/json");
});
生成MD5码
在头文件#include<openssl/md5.h>
中包含着生成MD5码的方法。
/*
* SumMd5 : 生成对应的MD5码(初始化session_id_)
* 即将real_str_中的内容调用MD5_xxx函数生成对应的md5码,存入session_id中
* 成功:true
* 失败:false
* int MD5_Init(MD5_CTX *c);初始化MD5码
* int MD5_Update(MD5_CTX *c, const void *data, unsigned long len);
* 通过传入的字符串更新MD5码
* int MD5_Final(unsigned char *md, MD5_CTX *c); 获取生成的MD5码
*/
bool SumMd5()
{
MD5_CTX c;
//1.初始化MD5码
MD5_Init(&c);
//2.更新和获取MD5码
if (MD5_Update(&c, real_str_.c_str(), real_str_.size()) != 1)
{
return false;
}
unsigned char md5[16] = { 0 };
if (MD5_Final(md5, &c) != 1)
{
return false;
}
//3.将生成的MD5码按照16进制的格式存入会话id中
char tmp[3] = { 0 };
char buf[32] = { 0 };
for (int i = 0; i < 16; ++i)
{
snprintf(tmp, sizeof(tmp) - 1, "%02x", md5[i]);
//cout << tmp;
strncat(buf, tmp, 2);
}
session_id_ = buf;
return true;
}
用户注册的大体逻辑也一样,这里则不再进行过多的解释。
4.2. 项目难点解决
4.2.1 难点一:当用户点击开始匹配按钮时,创建房间并为用户分配相应的对手。
解决:
这里采用vector作为匹配池,创建一个线程用来从匹配池中获取匹配玩家进行匹配,并且引入玩家状态(ONLINE、MATCHING、PLAYING)作为区分,当玩家点击开始匹配按钮时,玩家状态变为MATCHING,并且将其加入匹配池当中,唤醒匹配线程进行匹配,如果匹配线程发现当前匹配玩家数量 < 2,则不进行匹配,如果发送当前匹配玩家数量为奇数,则匹配池中最后一个玩家不进行匹配,等待下一次匹配,其余玩家两两配对。
匹配线程所做的事情:当从匹配池中获取到两个玩家信息后,则为这两名玩家创建一个房间,并将这两名玩家的状态变为PALYING状态,为其分配创建出的房间号和对应所执的黑棋/白棋。
匹配线程代码如下:
// 匹配线程入口函数
static void* MatchServer(void* arg)
{
pthread_detach(pthread_self());
WebGoBang* wg = (WebGoBang*)arg;
while (1)
{
pthread_mutex_lock(&wg->vec_lock_);
//从匹配池中进行匹配
//要操作的是vector,所以首先要加锁,
//然后还要判断当前正在匹配玩家的数量,如果小于2,则放入等待队列中
while (wg->match_pool_num_ < 2)
{
pthread_cond_wait(&wg->vec_cond_, &wg->vec_lock_);
}
//匹配池中的人数一定大于等于2
// 奇数:一定有一个玩家要轮空
// 偶数:两两进行匹配
vector<int>& iv = wg->match_pool_;
size_t size = iv.size();
int last_id = -1;
if (size % 2)
{
//人数是奇数
//默认让最后一个人轮空
last_id = iv[size - 1];
size -= 1;
}
for (int i = size - 1;i >= 0; i -= 2)
{
int player_one = iv[i];
int player_two = iv[i - 1];
//假设匹配成功
int room_id = wg->rm_->CreateRoom(player_one, player_two);
//然后再让当前这两个用户的状态变为PALYING
wg->pm_->SetUserStartStatus(player_one, PLAYING);
wg->pm_->SetUserStartStatus(player_two, PLAYING);
//为该用户分配房间号
wg->pm_->SetRoomID(player_one, room_id);
wg->pm_->SetRoomID(player_two, room_id);
//为用户分配黑棋/白棋
wg->pm_->SetUserChessName(player_one, "黑棋");
wg->pm_->SetUserChessName(player_two, "白棋");
cout << "player1:" << player_one << ", palyer2:"
<< player_two << ",roomId:" << room_id << endl;
}
if (last_id < -1)
wg->PushPlayer2MatchPool(last_id);
wg->MatchPoolClear();
pthread_mutex_unlock(&wg->vec_lock_);
}
return NULL;
}
这个时候,前端的逻辑就是当监听到用户点击开始匹配按钮之后,就向后端发送一个AJAX请求,用来进行通知后端进行匹配,当后端将当前用户放入匹配池中之后,将相应的结果返回给前端,前端在获得这个结果之后,则就意味着当前用户正在处于匹配状态(MATCHING),为了获知什么时候才会匹配上,则前端需要不停的向后端发送AJAX请求,获知当前匹配的结果,直到当前用户匹配上为止。这里的循环我们采用的是setInterval
函数,在该函数内部不停调用AJAX请求,并且每隔1秒发送一次。
js如下:
SetMatch: function() {
if (game.match_flag == = false)
return;
console.log("SetMatch Func , 开始匹配...");
$.ajax({
url: "/SetMatch",
type : "Get",
dataType : "JSON",
//async : false ==> 禁止异步请求 chrome 有可能会禁止 ajax 的同步请求
async : false,
success : function(data) {
if (data.status == = 0) {
console.log("SetMatch Func , 开始匹配状态设置成功...");
game.GetMatchResult()
}
else {
alert("匹配失败, 重新匹配...");
}
},
});
},
GetMatchResult: function() {
//循环的获取匹配结果
//向后台循环发送获取匹配结果, 直到匹配成功 -- 调用setInterval函数即可
let s = 0;
let _time = setInterval(() = > {
console.log("GetMatchResult, " + s++);
$.ajax({
url: "/Match",
type : "Get",
dataType : "JSON",
async : false,//同步
success : function(data) {
game.match_status = data.status;
if (data.status == = 1) {
/*
* data = {
* status : 匹配状态
* room_id : xxx,
* chess_name: xxxx
* }
* */
console.log("匹配成功,room_id is:" + data.room_id);
alert("匹配成功, 您执 " + data.chess_name
+ ", 黑棋先走");
game.room_id = data.room_id;
game.my_chess_name = data.chess_name;
/*
* 当匹配成功之后,持白棋的人,
* 需要循环去获取执黑色棋子的落子位置
*/
if (data.chess_name == = "白棋")
game.GetPeerStep();
game.match_flag = false;
//将颜色变为灰色
game.start_match.style.background = '#d0cdcd';
game.start_match.style.color = '#505050';
}
else if (data.status == = 0 && s >= 30)
{
//当没有匹配上,一直到循环结束,才弹框提示匹配人数太少
alert("当前匹配人数太少,请重新匹配...");
}
else if (data.status == = -1)
{
alert("当前会话失效,请重新登录....");
window.location.href = "index.html";
}
},
});
if (s >= 30 || game.match_status == = 1
|| game.match_status == = -1) {
//当调用次数超过30次时。就不再循环调用
// 或者当匹配上了
// 会着匹配失败
clearInterval(_time);
}
}, 1000); // 1000 -> 每隔1秒调用一次
},
这里也不是无限制的在进行调用,而是当调用次数超过30次,或者已经匹配上、匹配失败的情况下,都会停止对该函数的调用。
4.2.2 难点二:在对战过程中,用户只能下一种颜色的棋子(黑棋/白棋),并且只能在属于自己的回合进行下棋。
解决:
首先,我们要明确的一点就是当前用户能够下棋的条件是一定是已经匹配上了,并且当前处于自己的回合,才可以进行下棋。当两个用户匹配上之后,在创建房间的时候,已经规定了每个用户只能使用一种棋子的颜色(用户1:黑色,用户2:白色),并且决定了当前该哪个用户下棋(初始请求是第一个用户先下棋),因此,我们只需要判断当前房间中的是否该自己下棋即可。
因此,每次只需判断当前用户的user_id_和whose_turn_是否相同即可,相同则代表着是自己的回合,不同则代表着是对方的回合,不能进行下棋。
此时前端的逻辑就是当用户点击棋盘上的某一点的时候,就向后端发送一个AJAX请求,判断当前是否是自己的回合,如果是则允许落子,否则,不允许落子。如果当前是允许落子的,那么在棋盘上绘制对应棋子之前,会再发送一个AJAX请求到后端,后端会对当前房间的 whose_turn_ 进行修改,并且保存当前所走棋子的位置信息在vector中。
4.2.3 难点三:在对战过程中,对手下的棋子的位置能够及时同步在自己的棋盘上。
解决:
假设当前是用户1的回合,当用户1点击棋盘上的某个位置的时候,前端会给后端发送一个AJAX请求,后端在接收到该请求后,会对当前房间的 whose_turn_ 进行修改,并且保存当前所走棋子的位置信息在一个vector中,然后返回相应的结果,前端在接收到该结果之后,就去循环获取对方棋盘最近一次下棋的棋子位置,同理,调用
setInterval
函数进行循环获取,即向后端发送AJAX请求,后端将保存在vector中的最后位置的棋子位置信息进行返回,前端拿到该棋子位置信息之后,将该棋子绘制到当前用户的棋盘上,这样就实现了两个用户对战棋盘之间的同步。
这里还需注意的是初始情况下,即两个用户刚刚匹配上的时候,这个时候由于是黑棋先走,因此,黑棋用户在刚开始时是不需要获取对方棋盘上棋子的结果的,而相应的白棋是第二步走的,因此,白棋用户需要在刚开始时循环获取对方棋子的下棋结果。
以上是关于项目——网络对战五子棋(Web-Gobang)的主要内容,如果未能解决你的问题,请参考以下文章