Node.js 游戏服务器开发

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

基本概念

  • 代开房间

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

代开流程

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

服务器架构

4933701-2c6bfe210002a398.png
服务器架构

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

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

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

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

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

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

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

  1. 客户端登录大厅服务器

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

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

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

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

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

服务器功能划分

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

登录服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

  1. 组件列表
$ 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的跨域问题。
  1. 项目目录

预计项目组织结构,开发阶段逐步提取,目前为尚未完成。

  • bin 存放启动脚本与配置文件,根据不同的类型启动不同的环境,如开发版、测试版...
  • config 配置文件目录,将配置文件分离,分别保存。
  • middleware 中间件目录
  • lib 常用类库目录
  • model 数据模型文件,完成数据库与模型的映射关系。
  • app 应用目录
  1. 全局配置
$ vim config.js
//数据库配置
exports.mysql = function(){
    return {
        host:"127.0.0.1",
        port:3306,
        user:"root",
        password:"root",
        database:"scmj"
    }
};

目前配置文件未做分离,为测试方便放在根目录下config.js文件中,后期分离优化。

  1. 入口文件
$ 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

简要说明下登录服务入口完成的功能有三项

  1. 接收命令行参数中的全局配置文件
  2. 使用全局配置文件中的数据库配置对数据库进行连接,这里需要使用数据库连接池。
  3. 载入登录服务文件,并根据全局配置文件对其进行端口监听,以及路由访问设置。

启动命令

$ cd login
$ node app.js ../config.js

目前为测试使用此命令,后期优化统一配置。

流程解析

  1. 获取命令行参数

使用node app.js ../config.js时注意第二个参数是根目录下的config.js文件,此文件为全局配置,文件采用exports模块化导出对象的方式,在入口文件app.js中,使用process.argv[2]获取此文件,并通过require的方式加载配置文件,此时会得到一个配置对象。

  1. 加载数据库操作类

此处需要说明下,数据库操作已经进行分离提取优化,目前原始阶段,随着开发会一步步提取其中的实体以及模型等,最好的方式是采用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类中只做了两件事,完成了数据库连接的建立和配置的初始化,代码很粗糙,先打通流程然后再一步步优化。这里需要注意的是需要使用数据库连接池,至于为什么不言而喻了。

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

  1. 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作为登录服务的业务核心,首先需要明确几点:

  1. 目前使用的是HTTP服务因此选择了Koa框架,Koa框架在Express框架的基础上做了进一步的分离和精简,这个是很不错的,至于HTTP服务是否合适,当然是不合适的,由于目前只涉及到登录服务,因此采用HTTP服务,后期的游戏的业务逻辑光HTTP服务是完全不够的,目前主要是做学习研究使用。

  2. 使用HTTP的接口最好是采用RESTful风格标准的,还是先GET、POST实现后优化。这里使用的组件是Koa的Router。

  3. 涉及到HTTP接口必然绕不过跨域文件,所以这里采用koa2-cors用来解决跨域访问文件,这里的跨域主要是因为使用了不同的端口所造成的。

  4. 最后有一种很重要的东西没有加进入是哪就是中间件,至于中间件的作用在Express中是很重要的一部分,在业务代码完善的过程中再一步步添加。

小结:

上面的代码主要的打通的流程就是加载配置、读取配置、建立数据库连接、搭建HTTP服务环境、路由配置、跨域访问。最终得到的结果是通过访问127.0.0.1:3000/version的接口可以正常得到返回值。

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

未完待续...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值