项目名称:四川麻将
统一简称:scmj
参考资料:https://v.qq.com/x/page/o0522mo58vj.html
带开房间表示代理创建一个新的房间,让其它玩家加入。为什么要代开房呢?首先可以帮助没有房卡的朋友开房来提升尚未买卡的玩家积极性,其次代理开房可以更高的对玩家进行管理以防止玩家自己创房。
代开流程
客户端与服务器之间的交互
客户端请求登录服获取大厅服务器信息,校验成功后,登录服返回大厅服务器的IP端口、当前游戏版本号、游戏下载地址...
客户端获取登录服返回的应用版本号后,和客户端本地版本号进行比对,若不匹配则下载最新的更新包,若匹配则继续执行后续流程。
若客户端使用微信登录则登录服使用微信接口进行认证授权,同时获取微信用户的个人信息并更新到数据库。同时返回微信个人信息以及认证结果。
客户端获得认证后向大厅服务器发起登录请求,大厅服收到用户认证令牌token
,经过校验成功后,返回玩家账号信息以及账号房卡数量。
大厅服服务房间的创建和玩家的进入,大厅服创建房间或进入房间成功后,客户端会获得分配给自己的房间服IP、端口、登录令牌。
客户端使用登录令牌与房间服建立连接并执行后续打牌逻辑,牌局结果后客户端断开与房间服务器的连接并跳转到大厅场景。
服务器功能划分
环境搭建
检查本机NodeJS版本
$ npm -v
6.4.1
$ node -v
v10.15.1
创建目录并进入
$ mkdir scmj&& cd scmj
$ mkdir server && cd server
使用NPM初始化项目
$ npm init
使用NPM安装核心组件
$ npm i express
$ npm i mysql
$ npm i socket.io
$ npm i fibers
$ npm i moment
$ npm i log4js
创建核心目录与文件
$ mkdir login hall game utils docs
$ touch config.js
为项目添加自定义启动命令
$ vim package.json
{
"name": "scmj",
"version": "1.0.0",
"description": "scmj",
"scripts": {
"login": "node ./login/app.js ../config.js",
"hall": "node ./hall/hall.js ../config.js",
"game": "node ./gmae/app.js ../config.js"
},
"author": "junchow",
"license": "ISC",
"dependencies": {
"fibers": "^3.1.1",
"log4js": "^4.0.2",
"mysql": "^2.16.0",
"socket.io": "^2.2.0"
}
}
在登录服、大厅服、游戏服三个文件夹下均以app.js
为 入口。
添加自定义脚本命令后可依次直接启动不同的服务
$ npm run login
$ npm run hall
$ npm run game
连接数据库
$ vim config.js
添加数据库连接配置
// 数据库配置
exports.mysql = function()
{
return
{
"host":"127.0.0.1",
"port":3306,
"user":"root",
"password":"root",
"database":"scmj"
}
}
连接数据库
$ vim utils/db.js
var mysql = require("mysql");
// 创建数据库连接池并初始化
var pool = null;
exports.init = function(config)
{
pool = mysql.connectPool({
host:config.host,
port:config.port,
user:config.user,
password:config.password,
database:config.database
});
}
登录服
根据config
配置文件建立HTTP服务器,并维护RESTful风格接口。
$ cd login && touch app.js api.js server.js
入口文件
$ vim app.js
// 初始化数据库连接
var db = require("../utils/db");
var config = require(process.argv[2]);
db.init(config.mysql());
// 获取登录服配置
var cfg = config.login();
// 开启服务器
var server = require("./server");
server.start(cfg);
// 开启客户端API
var api = require("./api");
api.start(cfg);
在config
配置文件中添加登录服配置
$ vim config.js
// 登录服配置
exports.login = function()
{
return {
port:9000
};
}
编写登录服接口
$ vim server.js
start 启动登录服并初始化参数
var express = require("express");
var app = express();
// 开启服务
var cfg = null;
export.start = function(config)
{
cfg = config;
app.listen(config.port);
console.log("login server is listening on port "+config.port);
}
在命令行中使用nmp run login
开启登录服进程,查看打印输出,判断是否启动成功。
$ npm run login
> [email protected] login D:\nodejs\scmj
> node ./login/app.js ../config.js
login server is listening on port 9000
跨域请求访问处理
var fibers = require("fibers");
//设置跨域访问
app.all("*", function(req, res, next){
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By", "3.2.1");
res.header("Content-Type", "application/json;charset=utf-8");
fibers(function(){
next();
}).run();
});
RESTful接口返回参数格式
//成功返回
function success(res, data)
{
var ret = {};
ret.code = 0;
ret.message = "success";
if(data != undefined){
ret.data = data;
}
console.log(ret);
res.send(JSON.stringify(ret));
}
//失败返回
function error(res, data){
var ret = {};
ret.code = 1;
ret.message = "error";
if(data!=undefined || data!=""){
ret.message = data;
}
console.log(ret);
res.send(JSON.stringify(ret));
}
version 获取客户端最新版本
配置文件添加客户版本
$ vim config.js
//登录服配置
exports.login = function(){
return {
//登录服端口
port:9000,
//客户端应用最新版本
app_version:'1.0.0',
}
}
添加接口地址
vim login/server.js
//获取客户端最新版本
app.get("/version", function(req, res){
var ret = {};
var version = cfg.app_version;
if(version == undefined || version == ""){
return error(res, "no data");
}
ret.app_version = version;
return success(res, ret);
});
测试接口
$ curl 127.0.0.1/version
{"code":0,"message":"success","data":{"app_version":"1.0.0"}}
server 获取服务器信息
配置文件config.js
添加需要返回给客户端的信息
$ vim config.js
//登录服配置
exports.login = function(){
return {
//登录服端口
port:9000,
//大厅服IP
hall_ip:"127.0.0.1",
//大厅服端口
hall_port:9001,
//客户端应用最新版本
app_version:'1.0.0',
//客户端应用下载地址
app_url:"http://fir.im/1f21",
//是否停服更新
is_down:1,
//停服更新公告
notice:"停服更新公告",
}
}
添加RESTful接口
$ vim server.js
//获取登录服信息
app.get("/server", function(req, res){
var ret = {};
ret.hall_ip = cfg.hall_ip;
ret.hall_port = cfg.hall_port;
ret.app_version = cfg.app_version;
ret.app_url = cfg.app_url;
ret.is_down = cfg.is_down;
ret.notice = cfg.notice;
success(res, ret);
});
接口测试
$ curl 127.0.0.1/server
{"code":0,"message":"success","data":{"hall_ip":"127.0.0.1","hall_port":9001,"app_version":"1.0.0","app_url":"http://fir.im/2f17","is_down":1,"notice":"停服更新公告"}}
封装常用加密解密方法
$ vim utils/crypto.js
var crypto = require("crypto");
//MD5加密
exports.md5 = function(data)
{
var md5 = crypto.createHash("md5");
md5.update(data);
return md5.digest("hex");
}
//BASE4编码
exports.base64encode = function(data)
{
return new Buffer(data).toString("base64");
}
//BASE64解码
exports.base64decode = function(data)
{
return new Buffer(data, "base64").toString();
}
//JSON序列化
exports.json_encode = functon(data)
{
return JSON.stringify(data);
}
guest 游客登录
配置文件config.js
添加配置
$ vim config.js
//登录服配置
exports.login = function(){
return {
//登录服端口
port:9000,
//大厅服IP
hall_ip:"127.0.0.1",
//大厅服端口
hall_port:9001,
//客户端应用最新版本
app_version:'1.0.0',
//客户端应用下载地址
app_url:"http://fir.im/2f17",
//是否停服更新
is_down:1,
//停服更新公告
notice:"停服更新公告",
//加密私钥
prikey:"^&*#$%()@"
}
}
添加RESTful接口
$ vim server.js
//游客访问
app.get("/guest", function(req, res){
var ret = {};
//获取客户端参数
var client_ip = req.ip;
var account = req.query.account;
//生成签名
var sign = crypto.md5(account + client_ip, cfg.prikey);
//返回数据
ret.account = account;
ret.hall_ip = cfg.hall_ip;
ret.hall_port = cfg.hall_port;
ret.sign = sign;
return success(res, ret);
});
接口测试
{"code":0,"message":"success","data":{"account":"alice","hall_ip":"127.0.0.1","hall_port":9001,"sign":"3f6128303c0d450ad5c383f0b766816a"}}
$ vim package.json
{
"name": "scmj",
"version": "1.0.0",
"description": "scmj",
"scripts": {
"login": "node ./login/app.js ../config.js",
"hall": "node ./hall/app.js ../config.js",
"game": "node ./gmae/app.js ../config.js"
},
"author": "junchow",
"license": "ISC",
"dependencies": {
"fibers": "^3.1.1",
"koa": "^2.7.0",
"koa-logger": "^3.2.0",
"koa-router": "^7.4.0",
"koa2-cors": "^2.0.6",
"moment": "^2.24.0",
"mysql": "^2.17.1",
"socket.io": "^2.2.0"
}
}
简要分析
koa
HTTP服务框架,提供HTTP的Request请求与Response对象,是Express的升级版。koa-router
Koa框架的路由组件,用来做HTTP服务接口时使用。koa2-cors
Koa框架跨域访问组件,由于不同服务之间使用不同端口势必相互访问时需要涉及到HTTP的跨域问题。预计项目组织结构,开发阶段逐步提取,目前为尚未完成。
bin
存放启动脚本与配置文件,根据不同的类型启动不同的环境,如开发版、测试版...config
配置文件目录,将配置文件分离,分别保存。middleware
中间件目录lib
常用类库目录model
数据模型文件,完成数据库与模型的映射关系。app
应用目录$ vim config.js
//数据库配置
exports.mysql = function(){
return {
host:"127.0.0.1",
port:3306,
user:"root",
password:"root",
database:"scmj"
}
};
目前配置文件未做分离,为测试方便放在根目录下config.js
文件中,后期分离优化。
$ vim login/app.js
//获取配置
const arg = process.argv[2];
const config = require(arg);
//初始化数据库连接
const db = require("../utils/db");
db.connect(config.mysql());
//启用服务器
const server = require("./server");
server.start(config.login());
目前登录业务也尚未纳入项目目录结构中,每个服务暂时以文件夹的方式存储,login
文件下保存的是登录服务相关的业务代码,入口文件为app.js
。
简要说明下登录服务入口完成的功能有三项
启动命令
$ cd login
$ node app.js ../config.js
目前为测试使用此命令,后期优化统一配置。
流程解析
使用node app.js ../config.js
时注意第二个参数是根目录下的config.js
文件,此文件为全局配置,文件采用exports
模块化导出对象的方式,在入口文件app.js
中,使用process.argv[2]
获取此文件,并通过require
的方式加载配置文件,此时会得到一个配置对象。
此处需要说明下,数据库操作已经进行分离提取优化,目前原始阶段,随着开发会一步步提取其中的实体以及模型等,最好的方式是采用ORM的方式进行操作。
$ vim ./utils/db.js
const mysql = require("mysql");
//初始化数据库
var pool = null;
const init = (cfg) => {
let config = {};
config.host = cfg.host;
config.port = cfg.port;
config.user = cfg.user;
config.password = cfg.password;
config.database = cfg.database;
pool = mysql.createPool(config);
};
//连接数据库
const connect = (cfg) => {
init(cfg);
return new Promise((resolve, reject) => {
pool.getConnection((error, connection) => {
if(error != null){
reject({error:true});
} else{
resolve({error:false, connection:connection});
}
});
});
};
exports.connect = connect;
针对代码将要分析下,这里数据库使用的是MySQL,首先需要引入MySQL组件,这个在package.json
文件中已经注明,当然希望的方式是MySQL能够支持异步操作,这个继续在研究。
这里的db
类中只做了两件事,完成了数据库连接的建立和配置的初始化,代码很粗糙,先打通流程然后再一步步优化。这里需要注意的是需要使用数据库连接池,至于为什么不言而喻了。
对于下一步是针对增删改查操作的封装,随着业务代码的实现一步步再添加,走到哪里是哪儿,想太多总是会发现能力边界,先实现再优化,逐步求精。
app.js
入口文件的第三个核心操作是HTTP服务与接口路由的配置
const server = require("./server");
server.start(config.login());
这里首先加入login
文件夹下的server.js
文件,此文件是登录服务的核心业务所在,希望下一步分离是能让路由和业务代码进行分离。
$ vim login/server.js
/**
* 登录服
* */
const Koa = require("koa");
const app = new Koa();
//跨域设置
const cors = require("koa2-cors");
//调用路由中间件
const KoaRouter = require("koa-router");
const router = new KoaRouter();
//log request url
app.use(async (ctx, next) => {
console.log(`process ${ctx.request.method} ${ctx.request.url}`);
await next();
});
//配置
let cfg = null;
//客户端版本更新
router.get("/version", async (ctx, next) => {
let body = {};
body.error = 0;
body.message = "success";
if(cfg !== null){
let app_version = cfg.app_version;
if(app_version === undefined || app_version === ""){
body.error = 100;
body.message = "no configuration";
}else{
body.app_version = app_version;
}
}
ctx.body = body;
});
//应用设置
app.use(cors());
app.use(router.routes());
app.use(router.allowedMethods());
//启动服务器:初始化参数并监听端口
exports.start = (config) => {
cfg = config;
//监听端口
app.listen(3000, () => {
console.log(`login server is listening on ${config.login_ip}:${config.login_port}`);
});
};
简要梳理下核心点,server.js
作为登录服务的业务核心,首先需要明确几点:
目前使用的是HTTP服务因此选择了Koa框架,Koa框架在Express框架的基础上做了进一步的分离和精简,这个是很不错的,至于HTTP服务是否合适,当然是不合适的,由于目前只涉及到登录服务,因此采用HTTP服务,后期的游戏的业务逻辑光HTTP服务是完全不够的,目前主要是做学习研究使用。
使用HTTP的接口最好是采用RESTful风格标准的,还是先GET、POST实现后优化。这里使用的组件是Koa的Router。
涉及到HTTP接口必然绕不过跨域文件,所以这里采用koa2-cors
用来解决跨域访问文件,这里的跨域主要是因为使用了不同的端口所造成的。
最后有一种很重要的东西没有加进入是哪就是中间件,至于中间件的作用在Express中是很重要的一部分,在业务代码完善的过程中再一步步添加。
小结:
上面的代码主要的打通的流程就是加载配置、读取配置、建立数据库连接、搭建HTTP服务环境、路由配置、跨域访问。最终得到的结果是通过访问127.0.0.1:3000/version
的接口可以正常得到返回值。
接下来的工作是随着上业务代码,对数据库操作进行封装以及缓存的添加,是重点要做的事情。还有一点是业务流程的梳理。代码凌乱也很零散,不间断更新完善,大体看看就行,不必深究。
未完待续...