Node.js 游戏服务器开发

mac2024-05-22  26

项目名称:四川麻将 统一简称:scmj 参考资料:https://v.qq.com/x/page/o0522mo58vj.html

基本概念

代开房间

带开房间表示代理创建一个新的房间,让其它玩家加入。为什么要代开房呢?首先可以帮助没有房卡的朋友开房来提升尚未买卡的玩家积极性,其次代理开房可以更高的对玩家进行管理以防止玩家自己创房。

代开流程

客户端新增代开选项,代理创房的消息内附带代开标识。大厅服中区分不同类型的房间并分别处理房间服添加对代开的数据读写客户端获取代开房列表房间服获取代开房列表消息处理

服务器架构

 

 

服务器架构

客户端与服务器之间的交互

客户端请求登录服获取大厅服地址

客户端请求登录服获取大厅服务器信息,校验成功后,登录服返回大厅服务器的IP端口、当前游戏版本号、游戏下载地址...

客户端应用版本检测与更新

客户端获取登录服返回的应用版本号后,和客户端本地版本号进行比对,若不匹配则下载最新的更新包,若匹配则继续执行后续流程。

客户端使用第三方登录或游客登录

若客户端使用微信登录则登录服使用微信接口进行认证授权,同时获取微信用户的个人信息并更新到数据库。同时返回微信个人信息以及认证结果。

客户端登录大厅服务器

客户端获得认证后向大厅服务器发起登录请求,大厅服收到用户认证令牌token,经过校验成功后,返回玩家账号信息以及账号房卡数量。

玩家在大厅内创建房间或进入房间

大厅服服务房间的创建和玩家的进入,大厅服创建房间或进入房间成功后,客户端会获得分配给自己的房间服IP、端口、登录令牌。

玩家登录游戏房间服务器进行游戏

客户端使用登录令牌与房间服建立连接并执行后续打牌逻辑,牌局结果后客户端断开与房间服务器的连接并跳转到大厅场景。

服务器功能划分

登录服 应用版本更新检测、创建用户并验证身份、第三方登录、大厅服地址维护大厅服 桥接用户和房间、为房间分配服务器以实现负载均衡、用户登录大厅会获取个人账户数据、大厅服务向房间服务器发起创建或进入房间请求。 大厅服主要负责玩家进入游戏的接入、发送公告等功能。当玩家登录完毕以及游戏结束时都将会进入大厅服务器。房间服 处理创建房间、进入房间...核心游戏业务逻辑。游戏服承载着游戏对外提供的服务。对于房间类游戏而言,其功能包括房间的创建、进入房间、离开房间、开始游戏、结束游戏。由于不同游戏对应得逻辑不通,若需要代码公用则可以将房间的操作分离出来作为一个公共库。只有游戏开始和游戏结束时游戏逻辑不通。

 

游戏服

登录服V1

环境搭建

检查本机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 > scmj@1.0.0 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"}}

登录服v2

组件列表 $ 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类中只做了两件事,完成了数据库连接的建立和配置的初始化,代码很粗糙,先打通流程然后再一步步优化。这里需要注意的是需要使用数据库连接池,至于为什么不言而喻了。

对于下一步是针对增删改查操作的封装,随着业务代码的实现一步步再添加,走到哪里是哪儿,想太多总是会发现能力边界,先实现再优化,逐步求精。

HTTP服务与路由配置

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的接口可以正常得到返回值。

接下来的工作是随着上业务代码,对数据库操作进行封装以及缓存的添加,是重点要做的事情。还有一点是业务流程的梳理。代码凌乱也很零散,不间断更新完善,大体看看就行,不必深究。

未完待续...

作者:JunChow520 链接:https://www.jianshu.com/p/c4bbee1c27da 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最新回复(0)