Repository: shihuaping/gamex Branch: master Commit: 08e553880935 Files: 65 Total size: 66.8 KB Directory structure: gitextract_bpk30wdz/ ├── .idea/ │ └── vcs.xml ├── README.md ├── center_svr/ │ ├── README.md │ ├── center-handler.js │ ├── center-server.js │ ├── center.js │ ├── config/ │ │ ├── log4js.json │ │ └── sys-config.json │ ├── logger.js │ ├── redis-oper.js │ └── short-ID.js ├── game_svr/ │ ├── README.md │ ├── config/ │ │ ├── log4js.json │ │ └── sys-config.json │ ├── desk.js │ ├── game-handler.js │ ├── game-server.js │ ├── game.js │ ├── plugin/ │ │ ├── ddz-rule.js │ │ └── ddz.js │ ├── register-center.js │ ├── room-list.js │ └── room.js ├── gate_svr/ │ ├── README.md │ ├── client-handler.js │ ├── config/ │ │ ├── log4js.json │ │ └── sys-config.json │ ├── connections.js │ ├── gate.js │ ├── logger.js │ ├── redis-oper.js │ ├── register-center.js │ ├── server-handler.js │ ├── short-ID.js │ ├── sys-cache.js │ ├── tcp-client.js │ └── ws-server.js ├── hall_svr/ │ ├── config/ │ │ ├── log4js.json │ │ └── sys-config.json │ ├── db-oper.js │ ├── hall-handler.js │ ├── hall-server.js │ ├── hall.js │ ├── logger.js │ ├── register-center.js │ └── short-ID.js ├── lib/ │ ├── black-ip.js │ ├── const-define.js │ └── rds-key.js ├── login_svr/ │ ├── config/ │ │ ├── log4js.json │ │ └── sys-config.json │ ├── db-oper.js │ ├── logger.js │ ├── login-handler.js │ ├── login.js │ ├── register-center.js │ ├── server.js │ └── short-ID.js ├── package.json ├── protocol/ │ ├── client-protocol.md │ ├── cmd-define.js │ ├── packet.js │ └── server-server-protocol.md └── sql/ ├── gamedb.sql └── userdb.sql ================================================ FILE CONTENTS ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: README.md ================================================ # gamex 棋牌类游戏框架 技术基于node.js,写这个东西是因为最近接触了一部分棋牌的代码,过程实在是不那么让人愉快,代码规范性,可读性,稳定性,扩展性都不好。于是想用一个能够快速开发的工具来实现一个自己的框架。 center_svr 目前只做监控和服务器间的列表维护 gate_svr 用户请求转发到服务器 login_svr 用户帐号及登录授权 hall_svr 游戏信息及游戏服务器信息 game_svr 游戏服务器 除了center_svr以外,其它的服务器都可以水平扩展,可以多台分布式布署。center_svr是设计中的单点。 客户端与网关间应该只有一个连接,在开发阶段客户端可直连游戏服务器,不通过网关,降低调试复杂度。 对于多个同类服务器,使用随机访问的方式进行负载均衡。 服务器间通信协议都使用json,因为简单,好调试,易扩展。 服务间通信是自己模拟的rpc通信,需要严格的稳定性的话,要使用专门的rpc组件来实现。包括leader选举,广播等。目前的实现方式的问题在于,服务宕机了,不能及时通知到所有的服务。服务在调用时还需要自行判定被调者是否存活。 游戏服务器包含多个房间,每个房间参数可配置,每个房间里有一个桌子,每个桌子有玩家。由于node.js的天性,它是io处理强,cpu计算弱的。所以不要在游戏服务器中添加大量的计算类任务。 数据库使用SQL拼接的方式来写的。练手,练手。正确的做法是使用orm组件,避免大量重复的代码。 第三方游戏支付: 骏付通 爱贝 没想到这个项目有了这么多star……………………由于身体健康原因,项目停止了两年多了。 ================================================ FILE: center_svr/README.md ================================================ 中央服务器 功能:服务注册,服务查询 每一类服务器都有一个单独编号,编号是递增的。如果服务停掉了,这个编号可能会被下一个启动的服务使用。 每个服务都可以带有扩展信息,但是不建议使用,因为一旦添加了扩展信息就意味着中央服务器要修改,中央服务器修改后就需要重启。而中央服务器是单点,一重启就会导致客户端全部掉线。这个编号是给客户端用的,但同一个游戏有多个服务器的时候,用户连上网关以后,网关需要根据这个编号去找对应的服务器。否则就要保证服务端端口永不重复。 ================================================ FILE: center_svr/center-handler.js ================================================ //------------------------------------- // redis // type : server1 server2 server3 // // //------------------------------------- const rdsOper = require('./redis-oper'); const cmdDefine = require('../protocol/cmd-define'); const packet = require('../protocol/packet'); //serverInfo // { // name // type // ip // port // activeTime // serverNo // ext // } function registerServer(serverInfo) { rdsOper.saveServer(serverInfo); } function removeServer() { rdsOper.removeServer(serverInfo) } async function getServer(socket, serverInfo) { let serverList = await rdsOper.getServerList(serverInfo.type); } function process(socket, data) { let jObj = JSON.parse(data); console.log(jObj); let mainCmd = jObj.head.mcmd; let subCmd = jObj.head.scmd; switch (subCmd) { //更新服务状态 case cmdDefine.SUB_CENTER_UPDATE: let serverInfo = jObj.body; serverInfo.ip = socket.remoteAddress; serverInfo.activeTime = Math.floor(new Date().getTime()/1000); registerServer(serverInfo); break; //查询服务可用列表 case cmdDefine.SUB_CENTER_GET: let serverList = getServer(socket, serverInfo); let retObj = packet.getPacket(mainCmd,subCmd+100); retObj.body = serverList; socket.write(retObj); break; default: break } } exports.process = process; ================================================ FILE: center_svr/center-server.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); const process = require('process'); const centerHandler = require('./center-handler'); const server = net.createServer(); //attention: //1.to be more stable //all request should put into a queue function startServer() { server.on('listening', function () { logger.info("server is listening on port:%d", sysConfig.svrPort); }); server.on('connection', function (socket) { logger.info("connection income,ip:%s", socket.remoteAddress); socket.setTimeout(60 * 1000, function () { logger.error("ip:%s,idle timeout, disconncting, bye", socket.remoteAddress); socket.end('idle timeout, disconnecting, bye!'); socket.destroy(); }); socket.on('data', function (data) { logger.info("ip:%s, get data", socket.remoteAddress); console.debug(data); centerHandler.process(socket, data); }); socket.on('error', function (err) { logger.info("ip:%s,error", socket.remoteAddress); logger.error(err); socket.destroy(); }); socket.on('close', function (had_error) { logger.info("ip:%s,socket closed", socket.remoteAddress); if (!socket.destroyed) { logger.info("destroy socket"); socket.destroy(); } }) }); server.on('error', (err) => { logger.error(err); process.exit(1); }); server.listen(sysConfig.svrPort, sysConfig.svrHost, () => { logger.info('server bound on host:%s,port:%d', sysConfig.svrHost, sysConfig.svrPort); }); } exports.startServer = startServer; ================================================ FILE: center_svr/center.js ================================================ centerServer = require('./center-server'); centerServer.startServer(); ================================================ FILE: center_svr/config/log4js.json ================================================ { "appenders": { "logfile": { "type": "file", "filename": "log/center.log", "maxLogSize": 10485760, "numBackups": 3 }, "console": { "type": "console", "level": "debug" } }, "categories": { "default": { "appenders": [ "console", "logfile" ], "level": "debug" } } } ================================================ FILE: center_svr/config/sys-config.json ================================================ { "svrHost":"127.0.0.1", "svrPort":9200, "redisHost":"127.0.0.1", "redisPort":6379, "redisPassword":"" } ================================================ FILE: center_svr/logger.js ================================================ const config = require('./config/log4js.json'); const log4js = require('log4js'); log4js.configure(config); module.exports = log4js.getLogger(); ================================================ FILE: center_svr/redis-oper.js ================================================ // // 将服务信息保存在redis中,如果服务比较多,会造成冲突。每5秒一次心跳,极端情况下可能有服务永远注册不上。 // 仅有几个服务甚至十几个服务可以忽略上面的问题。有几十个服务就不要使用这种方式了,应该用rpc。 // const sysConfig = require('./config/sys-config.json'); const redis = require('redis'); const rdsKey = require('../lib/rds-key'); const logger = require('./logger'); const host = sysConfig.redisHost; const port = sysConfig.redisPort; const password = sysConfig.redisPassword; let rds = null; function getConnection() { if (rds) return rds; if (!!password && password.length > 0) { rds = redis.createClient({host: host, port: port, password: password}); } else { rds = redis.createClient({host: host, port: port}); } return rds; } getConnection(); //保存服务信息 function saveServer(serverInfo) { let keyName = rdsKey.KEY_SERVER_TYPE + serverInfo.type; rds.get(keyName, function (err, reply) { if (err) { logger.error(err); return; } jArray = JSON.parse(reply); if (!jArray) { jArray = []; } let serverIDList = []; for (let v of jArray) { serverIDList.push(v.serverNo); } serverIDList.sort(function (a,b) { if(a 1) { serverId = prevID + 1; break; } } // set server number if(maxServerId > 1000) { maxServerId = 1; } serverInfo.serverNo = maxServerId + 1; let found = false; for (let v of jArray) { if (v.ip === serverInfo.ip) { // do nothing found = true; break; } } if (!found) { jArray.push(serverInfo); let json = JSON.stringify(jArray); rds.set(keyName, json, 60*1000); } }); } //查询可用服务列表 function getServerList(type) { return new Promise(function (resolve, reject) { let keyName = rdsKey.KEY_SERVER_TYPE + type; rds.get(keyName, function (err, reply) { if (err) { reject(err); return; } resolve(reply); }) }); } //移除掉线的服务 async function removeServer(serverInfo) { let serverList = await getServerList(serverInfo.type); let jArray = JSON.parse(serverList); let idx = 0; for (let v of jArray) { if (v.ip === serverInfo.ip) { delete jArray[idx]; break; } idx++; } let keyName = rdsKey.KEY_SERVER_TYPE + serverInfo.type; let json = JSON.stringify(jArray); rds.set(keyName, json, 60*1000); } exports.getServerList = getServerList; exports.saveServer = saveServer; exports.removeServer = removeServer; ================================================ FILE: center_svr/short-ID.js ================================================ var idx = 1024; function getNextID() { if(idx > 4200000000) { idx = 1024; } idx++; return idx; } exports.getNextID = getNextID; ================================================ FILE: game_svr/README.md ================================================ 游戏服务器可以水平扩展。 游戏通过插件方式写在plugin目录下,同时将插件信息配置到config/sys-config中。 每个游戏一到多个游戏服务器,但不支持一个游戏服务器跑多个游戏。 每个游戏服务器有多个房间列表,房间相关配置信息在数据库中。玩家也可以自定义房间配置,覆盖默认的数据库中的配置。 游戏服务器启动后会自动往center服务器上报自己的信息。 游戏服务器会调用插件中的方法,如果插件不实现某个方法将会导致整个服务崩溃。 每个服务可开启的房间数是有上限的,现在限制为2000个房间。 ================================================ FILE: game_svr/config/log4js.json ================================================ { "appenders": { "logfile": { "type": "file", "filename": "log/game.log", "maxLogSize": 10485760, "numBackups": 3 }, "console": { "type": "console", "level": "debug" } }, "categories": { "default": { "appenders": [ "console", "logfile" ], "level": "debug" } } } ================================================ FILE: game_svr/config/sys-config.json ================================================ { "svrHost":"127.0.0.1", "svrPort":9000, "redisHost":"127.0.0.1", "redisPort":6379, "redisPassword":"", "centerSvrHost":"127.0.0.1", "centerSvrPort":9200, "plugin":"ddz.js" } ================================================ FILE: game_svr/desk.js ================================================ const sysConfig = require('./config/sys-config.json'); const cmdDefine = require('../protocol/cmd-define'); function init() { gameInst = require('./plugin/'+sysConfig.plugin); } function playerOpeCard(socket, packet) { const cardEvent = packet.body.cardEvent; switch (cardEvent.ope) { case cmdDefine.EVENT_OUT_CARD: gameInst.outCards(cardEvent); break; case cmdDefine.EVENT_PASS_CARD: gameInst.passCards(cardEvent); break; default: break; } } ================================================ FILE: game_svr/game-handler.js ================================================ const packet = require('../protocol/packet'); const cmdDefine = require('../protocol/cmd-define'); const room = require('./room'); function process(socket, data) { let jObj = JSON.parse(data); let mainCmd = jObj.head.mcmd; let subCmd = jObj.head.scmd; switch (subCmd) { case cmdDefine.SUB_GAME_CREATE_ROOM: //创建房间 room.createRoom(socket, jObj); break; case cmdDefine.SUB_GAME_ENTER_ROOM: //加入房间 room.enterRoom(socket, jObj); break; case cmdDefine.SUB_GAME_LEAVE_ROOM: //离开房间 room.leaveRoom(socket, jObj); break; case cmdDefine.SUB_GAME_DISMISS_ROOM: //解散房间 room.dismissRoom(socket, jObj); break; case cmdDefine.SUB_GAME_REENTER_ROOM: //断线重进房间 room.reenterRoom(socket, jObj); break; case cmdDefine.SUB_GAME_QUERY_PLAYER_INFO: //查询玩家游戏状态 room.queryPlayerInfo(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_SIT_DOWN: //坐下 room.playerSitDown(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_READY: //准备 room.playerReady(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_STAND_UP: //起立 room.playerStandUp(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_WATCH: //观战 room.playerWatch(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_OPE_CARD: //出牌,过牌,吃牌,碰,杠....等牌操作 room.playerOpeCard(socket, jObj); break; case cmdDefine.SUB_GAME_DESK_WIN: //胡牌 room.playerWin(socket, jObj); break; case cmdDefine.SUB_GAME_KICK_PLAYER: //房主踢人 room.kickPlayer(socket, jObj); break; default: logger.error("unknown mainCmd:%d,subCmd:%d", mainCmd, subCmd); break; } } exports.process = process; ================================================ FILE: game_svr/game-server.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); const process = require('process'); const gameHandler = require('./gane-handler'); const server = net.createServer(); function startServer() { server.on('listening', function () { logger.info("server is listening on port:%d", sysConfig.svrPort); }); server.on('connection', function (socket) { socket.setTimeout(60 * 1000, function () { logger.error("ip:%s,idle timeout, disconncting, bye", socket.remoteAddress); socket.end('idle timeout, disconnecting, bye!'); socket.destroy(); }); socket.on('data', function (data) { logger.info("ip:%s, get data", socket.remoteAddress); console.debug(data); gameHandler.process(socket, data); }); socket.on('error', function (err) { logger.info("ip:%s,error", socket.remoteAddress); logger.error(err); socket.destroy(); }); socket.on('close', function (had_error) { logger.info("ip:%s,socket closed", socket.remoteAddress); if (!socket.destroyed) { logger.info("destroy socket"); socket.destroy(); } }) }); server.on('error', (err) => { logger.error(err); process.exit(1); }); server.listen(sysConfig.svrPort, sysConfig.svrHost, () => { logger.info('server bound on host:%s,port:%d', sysConfig.svrHost, sysConfig.svrPort); }); } exports.startServer = startServer; ================================================ FILE: game_svr/game.js ================================================ const registerCenter = require('./register-center'); const gameServer = require('./game-server'); const sysConfig = require('./config/sys-config.json'); console.log(sysConfig); setInterval(registerCenter.registerSelf, 5*1000); gameServer.startServer(); ================================================ FILE: game_svr/plugin/ddz-rule.js ================================================ /* 牌列表说明: 每个牌的每个花色都有一个单独的ID,一共54个ID,ID按黑红梅方的顺序生成 a,2,3,....q,k 黑桃 [1,2,3,4,5,6,7,8,9,10,11,12,13] a,2,3,....q,k 红桃 [1,2,3,4,5,6,7,8,9,10,11,12,13] * 2 a,2,3,....q,k 梅花 [1,2,3,4,5,6,7,8,9,10,11,12,13] * 3 a,2,3,....1,k 方块 [1,2,3,4,5,6,7,8,9,10,11,12,13] * 4 小王 大王 */ const assert = require('assert'); // 花色定义 let SUIT_TYPE = { SPADE: 1, //黑桃 HEART: 2, //红桃 CLUB: 3, //梅花 DIAMOND: 4, //方块 JOKER: 5, //大小王 }; // 牌面显示定义 let CARD_CHAR = [ "null","A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "Kinglet", "King", ]; // 出牌类型 let CARD_TYPE = { ILL: 0, DAN: 1, //单张 DUI_ZI: 2, //对子 WANG_ZHA: 3, //王炸 ZHA_DAN: 4, //炸弹 SHUN_ZI: 5, //顺子 LIAN_DUI: 6, //连对 SAN: 7, //三张(不带) SAN_DAI_YI: 8, //三带一 SAN_DAI_YI_DUI: 9, //三带一对 FEI_JI: 10, //飞机(不带) FEI_JI_DAI_YI: 11, //飞机(带单) FEI_JI_DAI_DUI: 12, //飞机(带对子) SI_DAI_ER: 13, //四带二 SI_DAI_LIANG_DUI: 14, //四带两对 }; // 获取花色 // 3-q,k,a,2 每张牌有四个花色 function getSuitType(id) { let suitType = null; if (id >= 1 && id <= 13) suitType = SUIT_TYPE.SPADE; else if (id >= 14 && id <= 26) suitType = SUIT_TYPE.HEART; else if (id >= 27 && id <= 39) suitType = SUIT_TYPE.CLUB; else if (id >= 40 && id <= 52) suitType = SUIT_TYPE.DIAMOND; else if (id === 53 || id === 54) suitType = SUIT_TYPE.JOKER; return suitType } // 获取显示字 function getCardChar(id) { let displayChar = null; if (id >= 1 && id <= 52) displayChar = CARD_CHAR[(id - 1) % 13 + 1]; else if (id === 53) displayChar = CARD_CHAR[14]; else if (id === 54) displayChar = CARD_CHAR[15]; return displayChar; } // 获取牌等级 // 大王 > 小王 > 2 > A > K .... > 3 function getGrade(id) { let grade = 0; if (id === 54) { //大王 grade = 17; } else if (id === 53) { //小王 grade = 16; } else { let modResult = id % 13; if (modResult === 2) // 2 grade = 15; else if (modResult === 1) // A grade = 14; else if (modResult === 0) // K grade = 13; else if (modResult >= 3 && modResult < 13) // 3到Q grade = modResult; } return grade; } let AllCards = []; for (let i = 1; i <= 54; i++) { AllCards[i] = { id: i, grade: getGrade(i), // 等级 suit: getSuitType(i), // 花色 display: getCardChar(i), // 牌面 }; } console.log(AllCards); function sortCards(cards) { cards.sort(function (a, b) { if(AllCards[a].grade > AllCards[b].grade) return -1; else if(AllCards[a].grade === AllCards[b].grade) return 0; else return 1; }); } function getGrades(cards) { let t = {}; for (let v of cards) { let grade = AllCards[v].grade; if (! (grade in t)) { t[grade] = 0; } t[grade] = t[grade] + 1; } return t; } function dump(cards) { sortCards(cards); for (let v of cards) { console.log("花色:%d,牌面:%s, 权重:%d", AllCards[v].suit, AllCards[v].display, AllCards[v].grade); } } //单张 function isDan(cards) { return cards.length === 1; } assert(isDan([33])); assert(isDan([23, 5]) === false); //对子 function isDuiZi(cards) { return cards.length === 2 && AllCards[cards[0]].grade === AllCards[cards[1]].grade; } assert(isDuiZi([30, 43])); assert(isDuiZi([30, 52]) === false); //大小王炸弹 function isDuiWang(cards) { return (cards.length === 2) && (cards[0] + cards[1] === 107); } assert(isDuiWang([53, 54])); assert(isDuiWang([53, 52]) === false); //三张 function isSan(cards) { return cards.length === 3 && (AllCards[cards[0]].grade === AllCards[cards[1]].grade) && (AllCards[cards[1]].grade === AllCards[cards[2]].grade); } assert(isSan([30, 43, 17])); //4 4 4 assert(isSan([30, 43, 15]) === false);//4 4 2 //三带一 function isSanDaiYi(cards) { if (cards.length !== 4) { return false; } sortCards(cards); if (AllCards[cards[0]].grade !== AllCards[cards[1]].grade) { let t = cards[3]; cards[3] = cards[0]; cards[0] = t; } if (AllCards[cards[0]].grade === AllCards[cards[1]].grade) { if (AllCards[cards[0]].grade === AllCards[cards[2]].grade && AllCards[cards[0]].grade !== AllCards[cards[3]].grade) { return true; } } return false; } assert(isSanDaiYi([30, 43, 17, 5])); //4 4 4 3 assert(isSanDaiYi([30, 43, 17, 4]) === false); //4 4 4 4 //三带一对 function isSanDaiDui(cards) { if (cards.length !== 5) { return false; } sortCards(cards); if (AllCards[cards[0]].grade !== AllCards[cards[2]].grade) { let t = cards[3]; cards[3] = cards[0]; cards[0] = t; t = cards[1]; cards[1] = cards[4] ; cards[4] = t; } if (AllCards[cards[0]].grade === AllCards[cards[1]].grade) { if (AllCards[cards[0]].grade === AllCards[cards[2]].grade && AllCards[cards[2]].grade !== AllCards[cards[3]].grade && AllCards[cards[3]].grade === AllCards[cards[4]].grade) { return true; } } else { return false; } return false; } assert(isSanDaiDui([30, 43, 17, 5, 31]));// 4 4 4 5 5 assert(isSanDaiDui([30, 43, 17, 3, 31]) === false);//4 4 4 5 3 //四张(炸弹) function isSi(cards) { if (cards.length !== 4) { return false; } for (let i = 0; i < 3; i++) { if (AllCards[cards[i]].grade !== AllCards[cards[i + 1]].grade) { return false; } } return true; } assert(isSi([30, 43, 17, 4]));// 4 4 4 4 assert(isSi([30, 43, 18, 4]) === false); // 4 4 5 4 //四带二 function isSiDaiEr(cards) { if (cards.length !== 6) { return false; } let grades = getGrades(cards); for (let i in grades) { if (grades[i] === 4) { return true; } } return false; } assert(isSiDaiEr([30, 43, 17, 4, 3, 5]));// 4 4 4 4 3 5 assert(isSiDaiEr([30, 43, 17, 4, 3, 16]));// 4 4 4 4 3 3 //四带两对 function isSiDaiLiangDui(cards) { if (cards.length !== 8) { return false } let grades = getGrades(cards); let t1 = 0; let t2 = 0; let t3 = 0; for (let i in grades) { if (!t1) { t1 = grades[i] } else if (!t2) { t2 = grades[i]; } else if (!t3) { t3 = grades[i]; } } let arr = [t1, t2, t3]; arr.sort(function (a, b) { if( a > b) { return -1;} else if(a===b) {return 0;} else {return 1;} }); return (arr.length === 3) && (arr[0] === 4) && (arr[1] === 2) && (arr[2] === 2); } assert(isSiDaiLiangDui([30, 43, 17, 4, 3, 16, 5, 18]));// 4 4 4 4 3 3 5 5 assert(isSiDaiLiangDui([3, 16, 5, 18, 30, 43, 17, 4]));// 3 3 5 5 4 4 4 4 assert(isSiDaiLiangDui([30, 43, 17, 4, 3, 6, 5, 18]) === false); // 4 4 4 4 3 6 5 5 //顺子 function isShunZi(cards) { // 顺子必须超过五张 if (cards.length < 5) return false; sortCards(cards); //最大的牌不能超过A if (AllCards[cards[0]].grade > 14) return false; //如果不连续 for (let i=0;i 14) return false; //如果牌不连续 for (let i=0; i < cards.length / 2 - 1; i++) { //先判定是不是都是对子 if (AllCards[cards[i * 2]].grade !== AllCards[cards[i * 2 + 1]].grade) return false; //再判定连续对 if (i < cards.length / 2 - 1 && AllCards[cards[i * 2 + 1]].grade !== AllCards[cards[(i+1) * 2 ]].grade + 1) return false; } return true; } assert(isLianDui([35, 9, 21, 8, 20, 7]));// 9 9 8 8 7 7 assert(isLianDui([35, 9, 21, 8, 20, 7, 6, 19]));// 9 9 8 8 7 7 6 6 assert(isLianDui([35, 9, 21, 8, 20, 7, 33]) === false);// 9 9 8 8 7 7 7 //飞机(不带) function isFeiJi(cards) { if (cards.length !== 6 && cards.length !== 9) return false; let grades = getGrades(cards); // 如果不是三张,就判错 for (let i in grades) { if (grades[i] !== 3) return false; } //如果是三张组合,还要判定是不是连续的 let t1 = 0; let t2 = 0; let t3 = 0; for(k in grades) { if (grades[k] === 3) { if (!t1) t1 = Number(k); else if(!t2) t2 = Number(k); else if(!t3) t3 = Number(k); } } if (cards.length === 6 && t1 && t2 && (t1 === t2 + 1 || t2 === t1 + 1)) return true; if (cards.length === 9 && t1 && t2 && t3) { let arr = [t1, t2, t3]; arr.sort(function (a, b) { if (a > b) return -1; else if (a === b) return 0; else return 1; }); if (arr[0] === arr[1] + 1 && arr[1] === arr[2] + 1) return true; } return false; } assert(isFeiJi([35, 9, 22, 21, 8, 34]));// 9 9 9 8 8 8 assert(isFeiJi([35, 9, 22, 21, 8, 34, 7, 20, 33]));// 9 9 9 8 8 8 7 7 7 assert(isFeiJi([35, 9, 22, 20, 7, 33]) === false);// 9 9 9 7 7 7 //飞机(带单张) function isFeiJiDaiDan(cards) { if (cards.length !== 8 && cards.length !== 12) return false; let grades = getGrades(cards); let t1 = 0; let t2 = 0; let t3 = 0; for(let k in grades) { if (grades[k] === 3) { if (!t1) t1 = Number(k); else if(!t2) t2 = Number(k); else if(!t3) t3 = Number(k); } } //判定三张是否连续 if (cards.length === 8 && t1 && t2 && (t1 === t2 + 1 || t2 === t1 + 1)) return true; if (cards.length === 12 && t1 && t2 && t3) { let arr = [t1, t2, t3]; arr.sort(function (a,b) { if (a>b)return -1; else if(a===b) return 0; else return 1; }); if (arr[0] === arr[1] + 1 && arr[1] === arr[2] + 1) return true; } return false; } assert(isFeiJiDaiDan([35, 9, 22, 21, 8, 34, 3, 4]));// 9 9 9 8 8 8 3 4 assert(isFeiJiDaiDan([35, 9, 22, 21, 8, 34, 7, 20, 33, 3, 4, 5]));// 9 9 9 8 8 8 3 4 5 assert(isFeiJiDaiDan([35, 9, 22, 20, 7, 33, 3, 4]) === false);// 9 9 9 7 7 7 //飞机(带对子) function isFeiJiDaiDui(cards) { if (cards.length !== 10 && cards.length !== 15) return false; let grades = getGrades(cards); let t1 = 0; let t2 = 0; let t3 = 0; for(let k in grades) { if (grades[k] === 3) { if (!t1) t1 = Number(k); else if(!t2) t2 = Number(k); else if(!t3) t3 = Number(k); } else if(grades[k] !== 2) { return false; } } if (cards.length === 10 && t1 && t2 && (t1 === t2 + 1 || t2 === t1 + 1) ) return true; if (cards.length === 15 && t1 && t2 && t3) { let arr = [t1, t2, t3]; arr.sort(function (a, b) { if (a > b) return -1; else if (a === b) return 0; else return 1; }); if (arr[0] === arr[1] + 1 && arr[1] === arr[2] + 1) return true; } return false; } assert(isFeiJiDaiDui([35, 9, 22, 21, 8, 34, 7, 20, 33, 3, 16, 4, 17, 5, 18]));//9 9 9 8 8 8 7 7 7 3 3 4 4 5 5 assert(isFeiJiDaiDui([35, 9, 22, 21, 8, 34, 3, 4, 16, 17]));// 9 9 9 8 8 8 3 3 4 4 assert(isFeiJiDaiDui([35, 9, 22, 21, 8, 34, 3, 4, 16, 18]) === false); // 9 9 9 8 8 8 3 3 4 5 function getCardType(cards) { let c = cards.length; if (c === 1) { //单张 return CARD_TYPE.DAN; } else if (c === 2) { //两张只可能是王炸或一对 if (isDuiWang(cards)) { return CARD_TYPE.WANG_ZHA; } else if (isDuiZi(cards)) { return CARD_TYPE.DUI_ZI; } } else if (c === 3) { //三张只可能是单出三张 if (isSan(cards)) return CARD_TYPE.SAN; } else if (c === 4) { //四张只可能是炸弹或三带一 if (isSi(cards)) return CARD_TYPE.ZHA_DAN; else if (isS && aiYi(cards)) return CARD_TYPE.SAN_DAI_YI; } else if (c === 5) { //5张只可能是顺子或三带一对 if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isSanDaiDui(cards)) return CARD_TYPE.SAN_DAI_YI_DUI; } else if (c === 6) { //6张可能是顺子、飞机(不带)、四带二、连对 if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isFeiJi(cards)) return CARD_TYPE.FEI_JI; else if (isSiDaiEr(cards)) return CARD_TYPE.SI_DAI_ER; else if (isLianDui(cards)) return CARD_TYPE.LIAN_DUI; } else if (c === 7 || c === 11) { //7或11张只可能是顺子 if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; } else if (c === 8) { //8张可能是顺子、连对、飞机(带单) if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isFeiJiDaiDan(cards)) return CARD_TYPE.FEI_JI; else if (isLianDui(cards)) return CARD_TYPE.LIAN_DUI; } else if (c === 9) { //9张只可能是顺子、飞机(3头不带) if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isFeiJi(cards)) return CARD_TYPE.FEI_JI; } else if (c === 10) { //10张可能是顺子、飞机(2头带对子)、连对 if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isFeiJiDaiDui(cards)) return CARD_TYPE.FEI_JI; else if (isLi && ui(cards)) return CARD_TYPE.LIAN_DUI; } else if (c === 12) { //12张可能是顺子、飞机(3头带单张)、连对 if (isShunZi(cards)) return CARD_TYPE.SHUN_ZI; else if (isFeiJiDaiDan(cards)) return CARD_TYPE.FEI_JI; else if (isLi && ui(cards)) return CARD_TYPE.LIAN_DUI; } else if (c ===13) { //13张不合法,顺子带'2',不可能是飞机,不可能是4带 } else if (c === 14) { //14张只可能是连对 if (isLi && ui(cards)) return CARD_TYPE.LIAN_DUI; } else if (c === 15) { //15张只可能是飞机(3头带对子) if (isFeiJiDaiDui(cards)) return CARD_TYPE.FEI_JI; } return CARD_TYPE.ILL; } assert(getCardType([54]) === CARD_TYPE.DAN); assert(getCardType([54, 53]) === CARD_TYPE.WANG_ZHA); assert(getCardType([3, 3]) === CARD_TYPE.DUI_ZI); assert(getCardType([3, 3, 4, 4, 5, 5]) === CARD_TYPE.LIAN_DUI); assert(getCardType([3, 4, 5, 6, 7]) === CARD_TYPE.SHUN_ZI); assert(getCardType([13, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1]) === CARD_TYPE.SHUN_ZI); assert(getCardType([3, 3, 3, 3]) === CARD_TYPE.ZHA_DAN); assert(getCardType([3, 3, 3, 3, 4, 4, 4, 4]) !== CARD_TYPE.FEI_JI); assert(getCardType([3, 4, 5, 6, 7, 8, 9]) === CARD_TYPE.SHUN_ZI); assert(getCardType([3, 3, 3, 4, 4, 4, 5, 5, 5]) === CARD_TYPE.FEI_JI); assert(getCardType([3, 3, 3, 1, 4, 4, 4, 2, 5, 5, 5, 7]) === CARD_TYPE.FEI_JI); assert(getCardType([3, 3, 3, 1, 1, 4, 4, 4, 2, 2, 5, 5, 5, 7, 7]) === CARD_TYPE.FEI_JI); assert(getCardType([3, 3, 4, 4, 2, 2]) === CARD_TYPE.ILL); assert(getCardType([3, 3, 3, 4, 2]) === CARD_TYPE.ILL); assert(getCardType([3, 3, 3, 4, 4, 4, 2]) === CARD_TYPE.ILL); assert(getCardType([2, 3, 4, 5, 6]) === CARD_TYPE.ILL); assert(getCardType([3, 4, 5, 6, 7, 7]) === CARD_TYPE.ILL); ////////////////////////////////////////////- // 获取牌信息,花色,牌面,权重 function getCardDetail(card) { return AllCards[card]; } // 将多张牌排序后返回牌信息 function getSortedCardsDetail(cards) { sortCards(cards); let cardDetails = []; for (let i in cards) { cardDetails[i] = getCardDetail(cards[i]); } return cardDetails; } // 检查能不能出牌 function checkCards(preCards, curCards) { let card_type = getCardType(curCards); if (card_type === CARD_TYPE.ILL) return false; //头次打 if (preCards === null) return true; // 上家牌类型 let pre_act = getCardType(preCards); if (card_type === CARD_TYPE.WANG_ZHA) { // 王炸无视任何牌 return true; } else if (card_type === CARD_TYPE.ZHA_DAN) { //出炸弹,炸弹需要和上家比大小 if (pre_act === CARD_TYPE.WANG_ZHA) { return false; } else if (pre_act === CARD_TYPE.ZHA_DAN) { if (AllCards[preCards[1]].grade < AllCards[curCards[1]].grade) return true; else return false; } else { return true; } } else if (pre_act !== card_type) { // 和上家比牌型,上家出什么自己也要出什么 return false; } else if (preCards.length !== curCards.length) { return false; } else { // 牌型一样,但必须比上家大 sortCards(preCards); sortCards(curCards); if (AllCards[preCards[1]].grade < AllCards[curCards[1]].grade) return true; else return false; } return false; } function getAllCards() { return AllCards; } exports.checkCards = checkCards; exports.getCardDetail = getCardDetail; exports.getSortedCardsDetail = getSortedCardsDetail; exports.getCardType = getCardType; exports.getAllCards = getAllCards; exports.dump = dump; ================================================ FILE: game_svr/plugin/ddz.js ================================================ const ddzRule = require('./ddz-rule'); class DouDiZhu { constructor() { //牌池 this.cardPool = []; //座位 this.seats = {}; //地主 this.diZhu = 0; //底牌 this.diPai = []; } //初始化 initGame() { let full = []; let allCards = ddzRule.getAllCards(); for(let i=0;i<54;i++) { full.push(allCards[i+1].id); } let pos = 1; this.seats[pos] = {}; this.seats[pos].handCards = []; //手上的牌 this.seats[pos].outCards = []; //已经打出的牌 //洗牌,每人发17张 let arrLength = full.length; for(let i=0;i<17;i++) { let j = Math.floor(Math.random()*arrLength); this.seats[pos].handCards.push(full[j]); full.splice(j,1); arrLength--; } pos = 2; this.seats[pos] = {}; this.seats[pos].handCards = []; //手上的牌 this.seats[pos].outCards = []; //已经打出的牌 for(let i=0;i<17;i++) { let j = Math.floor(Math.random()*arrLength); this.seats[pos].handCards.push(full[j]); full.splice(j,1); arrLength--; } pos = 3; this.seats[pos] = {}; this.seats[pos].handCards = []; //手上的牌 this.seats[pos].outCards = []; //已经打出的牌 for(let i=0;i<17;i++) { let j = Math.floor(Math.random()*arrLength); this.seats[pos].handCards.push(full[j]); full.splice(j,1); arrLength--; } //留下三张给地主 this.diPai = full; //ddzRule.dump(this.seats[1].handCards); //ddzRule.dump(this.seats[2].handCards); //ddzRule.dump(this.seats[3].handCards); //ddzRule.dump(full); } //发牌 sendCards() { } //出牌 outCards() { } //过牌 passCards() { } //叫地主 qiangDiZhu() { } } let ddz = new DouDiZhu(); ddz.initGame(); ================================================ FILE: game_svr/register-center.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const packet = require('../protocol/packet'); const cmdDefine = require('../protocol/cmd-define'); const constDefine = require('../lib/const-define'); const shortID = require('./short-ID'); const logger = require('./logger'); let conn = null; function registerSelf() { let port = sysConfig.centerSvrPort; let host = sysConfig.centerSvrHost; console.log("connec to host:%s,port:%d", host, port); conn = net.createConnection({port:port,host:host}, function () { const psudoID = shortID.getNextID(); conn.psudoID = psudoID; conn.ip = host; let jObj = packet.getPacket(cmdDefine.CENTER, cmdDefine.SUB_CENTER_UPDATE); let serverInfo = {}; serverInfo.type = constDefine.SERVER_TYPE_GAME; serverInfo.port = sysConfig.svrPort; serverInfo.name = "game_svr"; jObj.body = serverInfo; let json = JSON.stringify(jObj); console.log(json); conn.write(json); conn.destroy(); }); conn.on('end', function () { logger.info("server connection closed,fd:%d,ip:%s", conn.psudoID, conn.ip); }); conn.on('error', function (err) { logger.info("server connection error,fd:%d,ip:%s", conn.psudoID, conn.ip); logger.error(err); }); conn.on('data', function (data) { console.log("server fd:%d get data:%s", conn.psudoID, data); }); } exports.registerSelf = registerSelf; ================================================ FILE: game_svr/room-list.js ================================================ // // 房间号生成规则:游戏ID+6个数字,6个数字随机生成,每次服务重启生成的数字都不一样 // 避免被猜到房间号,每个房间号带一个4位随机码,获取房间的时候动态生成 // // // [1,2,3,4,5] 方便随机数获取 // {1:"room1",2:"room2"....} // roomIDList = []; roomList = {}; const Room = require('./room'); //初始化房间列表 function initRoomList(gid,maxRoomCount) { for (let i=0;i 60*1000) { outTimeFd.push(k); } } for (var i=0; i 60*1000) { outTimeFd.push(k); } } for (var i=0; i 0) { rds = redis.createClient({host:host,port:port,password:password}); } else { rds = redis.createClient({host:host,port:port}); } for (let v of redisKey.SERVER_TYPE_LIST) { let key = redisKey.KEY_SERVER_TYPE + v; rds.get(key, function (error,reply) { cb (v, reply); }); } } exports.readCachedServers = readCachedServers; ================================================ FILE: gate_svr/register-center.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const packet = require('../protocol/packet'); const cmdDefine = require('../protocol/cmd-define'); const constDefine = require('../lib/const-define'); const shortID = require('./short-ID'); const logger = require('./logger'); let conn = null; function registerSelf() { let port = sysConfig.centerSvrPort; let host = sysConfig.centerSvrHost; console.log("connec to host:%s,port:%d", host, port); conn = net.createConnection({port:port,host:host}, function () { const psudoID = shortID.getNextID(); conn.psudoID = psudoID; conn.ip = host; let jObj = packet.getPacket(cmdDefine.CENTER, cmdDefine.SUB_CENTER_UPDATE); let serverInfo = {}; serverInfo.type = constDefine.SERVER_TYPE_GATE; serverInfo.port = sysConfig.svrPort; serverInfo.name = "gate_svr"; jObj.body = serverInfo; let json = JSON.stringify(jObj); console.log(json); conn.write(json); conn.destroy(); }); conn.on('end', function () { logger.info("server connection closed,fd:%d,ip:%s", conn.psudoID, conn.ip); }); conn.on('error', function (err) { logger.info("server connection error,fd:%d,ip:%s", conn.psudoID, conn.ip); logger.error(err); }); conn.on('data', function (data) { console.log("server fd:%d get data:%s", conn.psudoID, data); }); } exports.registerSelf = registerSelf; ================================================ FILE: gate_svr/server-handler.js ================================================ // // // 服务器返回数据时的处理 // // const logger = require('./logger'); const connections = require('./connections'); function onConnection(fd,tcpConn) { logger.info("client connected,fd:%d,ip:%s", fd, tcpConn.ip); connections.addSvrConn(fd,tcpConn); } function onMessage(tcpConn,msg) { logger.info("client read data,fd:%d,ip:%s", fd, tcpConn.ip); connections.updateSvrConn(tcpConn); var jObj = JSON.parse(msg); } function onError(tcpConn,err) { logger.error("client error,fd:%d,ip:%s", fd, tcpConn.ip); logger.error(err); const fd = ws.psudoID; connections.removeSvrConn(fd); } function onClose(tcpConn) { logger.info("client close,fd:%d,ip:%s", fd, tcpConn.ip); const fd = tcpConn.psudoID; connections.removeSvrConn(fd); } ================================================ FILE: gate_svr/short-ID.js ================================================ var idx = 1024; function getNextID() { if(idx > 4200000000) { idx = 1024; } idx++; return idx; } exports.getNextID = getNextID; ================================================ FILE: gate_svr/sys-cache.js ================================================ const redisOper = require('./redis-oper'); var cached = {}; function saveServerInfo(type,serverList) { let jArray = JSON.parse(serverList); cached[type] = jArray; } // read servers info from redis function readCached() { redisOper.readCachedServers(saveServerInfo); } function getServerList(type) { return cached[type]; } setInterval(readCached, 60*1000); exports.getServerList = getServerList; exports.readCached = readCached; ================================================ FILE: gate_svr/tcp-client.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const serverHandler = require('./server-handler'); const logger = require('./logger'); const shortID = require('./short-ID'); function getNewConnection(port,host) { if (!port) port = sysConfig.centerSvrPort; if (!host) host = sysConfig.centerSvrHost; var conn = net.createConnection({port:port,host:host}, function () { const psudoID = shortID.getNextID(); conn.psudoID = psudoID; conn.ip = host; logger.info("connect to server,ip:%s,port:%d", host, port); serverHandler.onConnect(psudoID, conn); }); conn.on('end', function () { logger.info("server connection closed,fd:%d,ip:%s", conn.psudoID, conn.ip); serverHandler.onClose(conn); }); conn.on('error', function (err) { logger.info("server connection error,fd:%d,ip:%s", conn.psudoID, conn.ip); logger.error(conn,err); serverHandler.onError(conn); }); conn.on('data', function (data) { console.log("server fd:%d get data:%s", conn.psudoID, data); serverHandler.onData(data); }); }; exports.getNewConnection = getNewConnection; ================================================ FILE: gate_svr/ws-server.js ================================================ const WebSocket = require('ws'); const clientHandler = require('./client-handler'); const shortID = require('./short-ID'); function startServer(sysConfig) { const host = sysConfig.svrHost; const port = sysConfig.svrPort; const wss = new WebSocket.Server({host:host,port:port}); wss.on('connection', function (ws) { const psudoID = shortID.getNextID(); const ip = req.connection.remoteAddress; ws.psudoID = psudoID; ws.ip = ip; clientHandler.onConnection(ws); ws.on('message', function (msg) { clientHandler.onMessage(ws, msg); }); ws.on('error', function (err) { clientHandler.onError(ws,err); }); ws.on('close', function () { clientHandler.onClose(ws); }); }); } exports.startServer = startServer; ================================================ FILE: hall_svr/config/log4js.json ================================================ { "appenders": { "logfile": { "type": "file", "filename": "log/hall.log", "maxLogSize": 10485760, "numBackups": 3 }, "console": { "type": "console", "level": "debug" } }, "categories": { "default": { "appenders": [ "console", "logfile" ], "level": "debug" } } } ================================================ FILE: hall_svr/config/sys-config.json ================================================ { "svrHost":"127.0.0.1", "svrPort":9300, "centerSvrHost":"127.0.0.1", "centerSvrPort":9200, "redisHost":"127.0.0.1", "redisPort":6379, "redisPassword":"", "mysqlHost":"127.0.0.1", "mysqlPort":3306, "mysqlUsername":"root", "mysqlPassword":"123456" } ================================================ FILE: hall_svr/db-oper.js ================================================ const mysql = require('mysql'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); var pool = null; function getPool() { pool = mysql.createPool( { connectionLimit: 50, host: sysConfig.mysqlHost, port: sysConfig.mysqlPort, user: sysConfig.mysqlUsername, password: sysConfig.mysqlPassword, database: 'gamedb' } ); } getPool(); function getGameList() { return new Promise(function (resolve, reject) { try { //avoid sql injection let sql = 'select * from game_list'; console.log(sql); pool.getConnection(function (err, conn) { conn.query(sql, function (err, results, fields) { conn.release(); if (err) { throw err; } resolve(results); }); }); } catch (ex) { logger.error(ex); reject(ex); } }); } function getGameServerList(gameID) { return new Promise(function (resolve, reject) { try { let sql = "select * from game_server_list where gid=" + mysql.escape(gameID); console.log(sql); pool.getConnection(function (err, conn) { conn.query(sql, function (err, results, fields) { conn.release(); if (err) { throw err; } resolve(results); }); }); } catch (ex) { logger.error(ex); reject(ex); } }); } exports.getGameList = getGameList; exports.getGameServerList = getGameServerList; ================================================ FILE: hall_svr/hall-handler.js ================================================ const cmdDefine = require('../protocol/cmd-define'); const dbOper = require('./db-oper'); const packet = require('../protocol/packet'); const logger = require('./logger'); async function process(socket,data) { let jObj = JSON.parse(data); console.log(jObj); let mainCmd = jObj.head.mcmd; let subCmd = jObj.head.scmd; switch (subCmd) { case cmdDefine.SUB_HALL_GAME_LIST: let gameList = await dbOper.getGameList(); let retObj = packet.getPacket(mainCmd, subCmd+100); retObj.body = gameList; let json = JSON.stringify(retObj); socket.write(json); break; default: logger.error("unsupport mainCmd:%d,subCmd:%d", mainCmd,subCmd); break } } exports.process = process; ================================================ FILE: hall_svr/hall-server.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); const process = require('process'); const hallHandler = require('./hall-handler'); const server = net.createServer(); function startServer() { server.on('listening', function () { logger.info("server is listening on port:%d", sysConfig.svrPort); }); server.on('connection', function (socket) { socket.setTimeout(60 * 1000, function () { logger.error("ip:%s,idle timeout, disconncting, bye", socket.remoteAddress); socket.end('idle timeout, disconnecting, bye!'); socket.destroy(); }); socket.on('data', function (data) { logger.info("ip:%s, get data", socket.remoteAddress); console.debug(data); hallHandler.process(socket, data); }); socket.on('error', function (err) { logger.info("ip:%s,error", socket.remoteAddress); logger.error(err); socket.destroy(); }); socket.on('close', function (had_error) { logger.info("ip:%s,socket closed", socket.remoteAddress); if (!socket.destroyed) { logger.info("destroy socket"); socket.destroy(); } }) }); server.on('error', (err) => { logger.error(err); process.exit(1); }); server.listen(sysConfig.svrPort, sysConfig.svrHost, () => { logger.info('server bound on host:%s,port:%d', sysConfig.svrHost, sysConfig.svrPort); }); } exports.startServer = startServer; ================================================ FILE: hall_svr/hall.js ================================================ // 大厅提供以下功能: // 游戏列表(游戏类型,游戏种类) // 服务器列表 // 在线人数等其它信息 const hallServer = require('./hall-server'); const registerCenter = require('./register-center'); setInterval(registerCenter.registerSelf, 5*1000); hallServer.startServer(); ================================================ FILE: hall_svr/logger.js ================================================ const config = require('./config/log4js.json'); const log4js = require('log4js'); log4js.configure(config); module.exports = log4js.getLogger(); ================================================ FILE: hall_svr/register-center.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const packet = require('../protocol/packet'); const cmdDefine = require('../protocol/cmd-define'); const constDefine = require('../lib/const-define'); const shortID = require('./short-ID'); const logger = require('./logger'); let conn = null; function registerSelf() { let port = sysConfig.centerSvrPort; let host = sysConfig.centerSvrHost; console.log("connec to host:%s,port:%d", host, port); conn = net.createConnection({port:port,host:host}, function () { const psudoID = shortID.getNextID(); conn.psudoID = psudoID; conn.ip = host; let jObj = packet.getPacket(cmdDefine.CENTER, cmdDefine.SUB_CENTER_UPDATE); let serverInfo = {}; serverInfo.type = constDefine.SERVER_TYPE_HALL; serverInfo.port = sysConfig.svrPort; serverInfo.name = "login_svr"; jObj.body = serverInfo; let json = JSON.stringify(jObj); console.log(json); conn.write(json); conn.destroy(); }); conn.on('end', function () { logger.info("server connection closed,fd:%d,ip:%s", conn.psudoID, conn.ip); }); conn.on('error', function (err) { logger.info("server connection error,fd:%d,ip:%s", conn.psudoID, conn.ip); logger.error(err); }); conn.on('data', function (data) { console.log("server fd:%d get data:%s", conn.psudoID, data); }); } exports.registerSelf = registerSelf; ================================================ FILE: hall_svr/short-ID.js ================================================ var idx = 1024; function getNextID() { if(idx > 4200000000) { idx = 1024; } idx++; return idx; } exports.getNextID = getNextID; ================================================ FILE: lib/black-ip.js ================================================ //ip黑名单 ================================================ FILE: lib/const-define.js ================================================ module.exports = { SERVER_TYPE_LOGIN : 2, SERVER_TYPE_CENTER : 1, SERVER_TYPE_GATE : 3, SERVER_TYPE_HALL : 4, SERVER_TYPE_GAME : 5 } ================================================ FILE: lib/rds-key.js ================================================ module.exports = { KEY_SERVER_TYPE : "server_type", SERVER_TYPE_LIST : [1,2,3], } ================================================ FILE: login_svr/config/log4js.json ================================================ { "appenders": { "logfile": { "type": "file", "filename": "log/login.log", "maxLogSize": 10485760, "numBackups": 3 }, "console": { "type": "console", "level": "debug" } }, "categories": { "default": { "appenders": [ "console", "logfile" ], "level": "debug" } } } ================================================ FILE: login_svr/config/sys-config.json ================================================ { "svrHost":"127.0.0.1", "svrPort":9100, "centerSvrHost":"127.0.0.1", "centerSvrPort":9200, "redisHost":"127.0.0.1", "redisPort":6379, "redisPassword":"", "mysqlHost":"127.0.0.1", "mysqlPort":3306, "mysqlUsername":"root", "mysqlPassword":"123456" } ================================================ FILE: login_svr/db-oper.js ================================================ const mysql = require('mysql'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); var pool = null; function getPool() { pool = mysql.createPool( { connectionLimit: 50, //TODO config in file host: sysConfig.mysqlHost, port: sysConfig.mysqlPort, user: sysConfig.mysqlUsername, password: sysConfig.mysqlPassword, database: 'userdb' } ); } getPool(); function login(userInfo) { return new Promise(function (resolve, reject) { try { //avoid sql injection let sql = 'select * from user_base where account=' + mysql.escape(userInfo.account) + ' and password=' + mysql.escape(userInfo.password); console.log(sql); pool.getConnection(function (err, conn) { conn.query(sql, function (err, results, fields) { conn.release(); if (err) { throw err; } resolve(results); }); }); } catch (ex) { logger.error(ex); reject(ex); } }); } function register(userInfo) { return new Promise(function (resolve, reject) { try { let sql = 'insert into user_base(accuounts, password) values(' + mysq.escape(userInfo.accounts) + ',' + mysq.escape(userInfo.password) + ')'; console.log(sql); pool.getConnection(function (err, conn) { conn.query(sql, function (err, results, fields) { conn.release(); if(err) { throw ex; } resolve(results); }) }) } catch (ex) { logger.error(ex); reject(ex); } }); } exports.login = login; exports.register = register; ================================================ FILE: login_svr/logger.js ================================================ const config = require('./config/log4js.json'); const log4js = require('log4js'); log4js.configure(config); module.exports = log4js.getLogger(); ================================================ FILE: login_svr/login-handler.js ================================================ const cmdDefine = require('../protocol/cmd-define'); const dbOper = require('./db-oper'); const packet = require('../protocol/packet'); async function process(socket, jObj) { const mainCmd = jObj.head.mcmd; const subCmd = jObj.head.scmd; switch (subCmd) { case cmdDefine.SUB_LOGIN_ACCOUNT: let data = await dbOper.login(userInfo); if (data.length > 0) { data.token = "just for test"; var retObj = packet.getPacket(mainCmd,subCmd+100); retObj.body = data; socket.write(JSON.stringify(retObj)); } break; case cmdDefine.SUB_LOGIN_PHONE: break; case cmdDefine.SUB_LOGIN_VISITOR: break; } } ================================================ FILE: login_svr/login.js ================================================ server = require('./server'); const registerCenter = require('./register-center'); const logger = require('./logger'); const sysConfig = require('./config/sys-config.json'); logger.debug(sysConfig); setInterval(registerCenter.registerSelf, 5*1000); server.startServer(); ================================================ FILE: login_svr/register-center.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const packet = require('../protocol/packet'); const cmdDefine = require('../protocol/cmd-define'); const constDefine = require('../lib/const-define'); const shortID = require('./short-ID'); const logger = require('./logger'); let conn = null; function registerSelf() { let port = sysConfig.centerSvrPort; let host = sysConfig.centerSvrHost; console.log("connec to host:%s,port:%d", host, port); conn = net.createConnection({port:port,host:host}, function () { const psudoID = shortID.getNextID(); conn.psudoID = psudoID; conn.ip = host; let jObj = packet.getPacket(cmdDefine.CENTER, cmdDefine.SUB_CENTER_UPDATE); let serverInfo = {}; serverInfo.type = constDefine.SERVER_TYPE_LOGIN; serverInfo.port = sysConfig.svrPort; serverInfo.name = "login_svr"; jObj.body = serverInfo; let json = JSON.stringify(jObj); console.log(json); conn.write(json); conn.destroy(); }); conn.on('end', function () { logger.info("server connection closed,fd:%d,ip:%s", conn.psudoID, conn.ip); }); conn.on('error', function (err) { logger.info("server connection error,fd:%d,ip:%s", conn.psudoID, conn.ip); logger.error(err); }); conn.on('data', function (data) { console.log("server fd:%d get data:%s", conn.psudoID, data); }); } exports.registerSelf = registerSelf; ================================================ FILE: login_svr/server.js ================================================ const net = require('net'); const sysConfig = require('./config/sys-config.json'); const logger = require('./logger'); const process = require('process'); const loginHandler = require('./login-handler'); const server = net.createServer(); function startServer() { server.on('listening', function () { logger.info("server is listening on port:%d", sysConfig.svrPort); }); server.on('connection', function (socket) { socket.setTimeout(60 * 1000, function () { logger.error("ip:%s,idle timeout, disconncting, bye", socket.remoteAddress); socket.end('idle timeout, disconnecting, bye!'); socket.destroy(); }); socket.on('data', function (data) { logger.info("ip:%s, get data", socket.remoteAddress); console.debug(data); loginHandler.process(socket, data); }); socket.on('error', function (err) { logger.info("ip:%s,error", socket.remoteAddress); logger.error(err); socket.destroy(); }); socket.on('close', function (had_error) { logger.info("ip:%s,socket closed", socket.remoteAddress); if (!socket.destroyed) { logger.info("destroy socket"); socket.destroy(); } }) }); server.on('error', (err) => { logger.error(err); process.exit(1); }); server.listen(sysConfig.svrPort, sysConfig.svrHost, () => { logger.info('server bound on host:%s,port:%d', sysConfig.svrHost, sysConfig.svrPort); }); } exports.startServer = startServer; ================================================ FILE: login_svr/short-ID.js ================================================ var idx = 1024; function getNextID() { if(idx > 4200000000) { idx = 1024; } idx++; return idx; } exports.getNextID = getNextID; ================================================ FILE: package.json ================================================ { "name": "gamex", "version": "1.0.0", "description": "gamex", "main": "null.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "gamex" ], "author": "shp", "license": "ISC", "dependencies": { "express": "^4.16.2", "log4js": "^2.3.5", "mysql": "^2.15.0", "net": "^1.0.2", "process": "^0.11.10", "redis": "^2.8.0", "redis-pool-connection": "^1.4.0", "ws": "^3.2.0" } } ================================================ FILE: protocol/client-protocol.md ================================================ 本文件包含客户端与服务端交互协议 ================================================ FILE: protocol/cmd-define.js ================================================ /* 1-1000 // framework 1001-1200 // login server 1201-1300 // center server 1301-1400 // hall server 3000-10000 // game server 10000-20000 //client <-> server //packet:{ // "head":{ // "mcmd":1, // "scmd:":1, // "remoteAddress":"127.0.0.1", // "seqNo":0 // }, // "body": { // //as your wish // } //} center server // // serverInfo:{ // name // type // ip // port // activeTime // serverNo // ext // } */ module.exports = { HEART_BEAT:1, LOGIN:1001, SUB_LOGIN_ACCOUNT:1, //帐号登录 SUB_LOGIN_PHONE:2, //手机登录 SUB_LOGIN_VISITOR:3, //游客登录 CENTER:1201, SUB_CENTER_UPDATE:1, //向中央服务器上报状态 SUB_CENTER_GET:2, //从中央服务器获取对应的服务列表 SUB_CENTER_PLAYER_OL, //玩家登陆,玩家退出游戏也是这个状态 SUB_CENTER_PLAYER_PL, //玩家游戏中 HALL:1301, SUB_HALL_GAME_LIST:1, //游戏列表 SUB_GAME_SERVER_LIST:2, //游戏服务列表 GAME:1401, SUB_GAME_CREATE_ROOM:1, //创建房间 SUB_GAME_ENTER_ROOM:2, //进入房间 SUB_GAME_LEAVE_ROOM:3, //退出房间 SUB_GAME_DISMISS_ROOM:4, //解散房间 SUB_GAME_DESK_SIT_DOWN:5, //坐下 SUB_GAME_DESK_READY:6, //准备 SUB_GAME_DESK_STAND_UP:7, //起立 SUB_GAME_DESK_OPE_CARD:8, //操作牌 SUB_GAME_DESK_WIN:9, //胡牌 SUB_GAME_REENTER_ROOM:10, //断线重进房间 SUB_GAME_QUERY_PLAYER_INFO:11, //查询玩家游戏状态 SUB_GAME_DESK_WATCH:12, //观战 SUB_GAME_KICK_PLAYER:1, //房主踢人 EVENT_OUT_CARD:1, EVENT_PASS_CARD:2, }; ================================================ FILE: protocol/packet.js ================================================ function Packet(mainCmd,subCmd) { this.head = {}; this.head.mcmd = mainCmd; this.head.scmd = subCmd; this.body = {}; } function getPacket(mainCmd,subCmd) { return new Packet(mainCmd,subCmd); } exports.getPacket = getPacket; ================================================ FILE: protocol/server-server-protocol.md ================================================ 本文件包含服务器内部协议定义 ================================================ FILE: sql/gamedb.sql ================================================ create database if not exists gamedb default charset utf8; use gamedb; -- 游戏表 -- 游戏id手工维护 create table if not exists game_list ( gid int not null default '0' comment '游戏ID', name varchar(32) not null default '' comment '游戏名称', iconURL varchar(255) not null default '' comment '游戏图标', index(gid) ) engine=innodb default charset 'utf8'; -- 游戏服相关信息 -- 注意游戏服ip和端口均为外网ip和端口 create table if not exists game_server_list ( gid int not null default '0' comment '游戏ID', name varchar(64) not null default '' comment '游戏服名称', ip char(16) not null default '' comment '游戏服ip', port int not null default '0' comment '游戏服端口', lowScore bigint not null default '0' comment '允许最低分,为0表示不限制', upScore bigint not null default '0' comment '允许最高分,为0表示不限制', status int not null default '0' comment '1为正常,2为维护中,3为停服', maxRoom int not null default '100' comment '最大房间数', createTime timestamp not null default 0 comment '创建时间', modifyTime timestamp not null default 0 comment '修改时间', index(gid,status) ) engine=innodb default charset 'utf8'; -- 每个用户在每个游戏中都有一份单独的数据,保证分数和金币数不乱 create table if not exists user_game_info ( uid int not null default '0' comment '用户id', uname varchar(64) not null default '' comment '用户昵称', gid int not null default '0' comment '游戏ID', score bigint not null default '0' comment '用户得分', coins bigint not null default '0' comment '用户金币数', gems int not null default '0' comment '用户钻石数', loginTime timestamp not null default 0 comment '登录时间', logoutTime timestamp not null default 0 comment '退出时间', index(uid,gid) ) engine = innodb default charset 'utf8'; ================================================ FILE: sql/userdb.sql ================================================ create database if not exists userdb default charset utf8; use userdb; -- 用户表 -- 用户只能单点登陆,单点登陆在redis中实现,不写数据库,这个表应该是读多写少的 create table if not exists user_base ( uid int not null default 0 comment '用户ID', account varchar(64) not null default '' comment '用户帐号', type int not null default 0 comment '用户类型[渠道],1原始,2游客,3微信', name varchar(64) not null default '' comment '用户名', nickName varchar(64) not null default '' comment '昵称', password char(32) not null default '' comment '用户密码', token char(32) not null default '' comment '用户动态token', sex int not null default 0 comment '性别', city int not null default 0 comment '城市', province int not null default 0 comment '省份', faceURL char(255) not null default '' comment '用户头像', createTime timestamp not null default 0 comment '创建时间', index(account,password) ) engine=innodb default charset 'utf8';