本文基于上一篇node.js的基础文章node.js服务器基础
服务器通用模块
每个服务器都基于同一套底层框架进行开发,即同一套的数据收发流程,不同的地方只在于处理请求和发送响应时候的业务逻辑。因此可以公用一套网络底层。此外,日志、加密等基础模块也是通用的。
日志模块
var util = require('util');
// log level
var LEVEL = {
ALL: Infinity,
INFO: 3,
WARN: 2,
ERROR: 1,
NONE: -Infinity
};
// log color
var COLOR = {
RESET: '\u001b[0m',
INFO: '\u001b[32m', // green
WARN: '\u001b[33m', // yellow
ERROR: '\u001b[31m' // red
}
// global log level
var globalLevel = LEVEL.ALL;
// whether log output should be colored
var coloredOutput = true;
function setLevel(level) {
globalLevel = level;
}
function setColoredOutput(bool) {
coloredOutput = bool;
}
function info() {
if (LEVEL.INFO <= globalLevel) {
log(LEVEL.INFO, util.format.apply(this, arguments));
}
}
function warn() {
if (LEVEL.WARN <= globalLevel) {
log(LEVEL.WARN, util.format.apply(this, arguments));
}
}
function error() {
if (LEVEL.ERROR <= globalLevel) {
log(LEVEL.ERROR, util.format.apply(this, arguments));
}
}
function newPrepareStackTrace(error, structuredStack) {
return structuredStack;
}
// must not be called directly due to stack trace
function log(level, message) {
// get call stack and find the caller
var oldPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = newPrepareStackTrace;
var structuredStack = new Error().stack;
Error.prepareStackTrace = oldPrepareStackTrace;
var caller = structuredStack[2];
var lineSep = process.platform == 'win32' ? '\\' : '/';
var fileNameSplited = caller.getFileName().split(lineSep);
var fileName = fileNameSplited[fileNameSplited.length - 1];
var lineNumber = caller.getLineNumber();
var columnNumber = caller.getColumnNumber();
// function name may be empty if it is a global call
// var functionName = caller.getFunctionName();
var levelString;
switch (level) {
case LEVEL.INFO:
levelString = '[INFO]';
break;
case LEVEL.WARN:
levelString = '[WARN]';
break;
case LEVEL.ERROR:
levelString = '[ERROR]';
break;
default:
levelString = '[]';
break;
}
var output = util.format('%s %s(%d,%d) %s',
levelString, fileName, lineNumber, columnNumber, message
);
if (!coloredOutput) {
process.stdout.write(output + '\n');
} else {
switch (level) {
case LEVEL.INFO:
process.stdout.write(COLOR.INFO + output + COLOR.RESET + '\n');
break;
case LEVEL.WARN:
process.stdout.write(COLOR.WARN + output + COLOR.RESET + '\n');
break;
case LEVEL.ERROR:
process.stdout.write(COLOR.ERROR + output + COLOR.RESET + '\n');
break;
default:
break;
}
}
}
module.exports = {
info: info,
warn: warn,
error: error,
LEVEL: LEVEL,
setLevel: setLevel,
setColoredOutput: setColoredOutput
};
Tcp封包模块
var tcppkg = {
// 根据封包协议我们读取包体的长度;
read_pkg_size: function(pkg_data, offset) {
if (offset > pkg_data.length - 2) { // 没有办法获取长度信息的;
return -1;
}
var len = pkg_data.readUInt16LE(offset);
return len;
},
package_data: function(data) {
var buf = Buffer.allocUnsafe(2 + data.length);
buf.writeInt16LE(2 + data.length, 0);
buf.fill(data, 2);
return buf;
},
};
module.exports = tcppkg;
网络底层
主要需求:支持TCP、WebSocket协议,支持Json和Protobuf数据的接收和发送
netbus.js
var net = require("net");
var ws = require("ws");
var log = require("../utils/log.js");
var tcppkg = require("./tcppkg.js");
var proto_man = require("./proto_man.js");
var service_manager = require("./service_manager.js");
var netbus = {
start_tcp_server: start_tcp_server,
start_ws_server: start_ws_server,
// session_send: session_send,
session_close: session_close,
};
var global_session_list = {};
var global_seesion_key = 1;
// 有客户端的session接入进来
function on_session_enter(session, proto_type, is_ws) {
if (is_ws) {
log.info("session enter", session._socket.remoteAddress, session._socket.remotePort);
}
else {
log.info("session enter", session.remoteAddress, session.remotePort);
}
session.last_pkg = null; // 表示我们存储的上一次没有处理完的TCP包;
session.is_ws = is_ws;
session.proto_type = proto_type;
session.is_connected = true;
// 扩展session的方法
session.send_encoded_cmd = session_send_encoded_cmd;
session.send_cmd = session_send_cmd;
// end
// 加入到我们的serssion 列表里面
global_session_list[global_seesion_key] = session;
session.session_key = global_seesion_key;
global_seesion_key ++;
// end
}
function on_session_exit(session) {
session.is_connected = false;
service_manager.on_client_lost_connect(session);
session.last_pkg = null;
if (global_session_list[session.session_key]) {
global_session_list[session.session_key] = null;
delete global_session_list[session.session_key]; // 把这个key, value从 {}里面删除
session.session_key = null;
}
}
// 一定能够保证是一个整包;
// 如果是json协议 str_or_buf json字符串;
// 如果是buf协议 str_or_buf Buffer对象;
function on_session_recv_cmd(session, str_or_buf) {
if(!service_manager.on_recv_client_cmd(session, str_or_buf)) {
session_close(session);
}
}
function session_send_cmd(stype, ctype, body) {
if (!this.is_connected) {
return;
}
var cmd = null;
cmd = proto_man.encode_cmd(this.proto_type, stype, ctype, body);
if (cmd) {
this.send_encoded_cmd(cmd);
}
}
// 发送命令
function session_send_encoded_cmd(cmd) {
if (!this.is_connected) {
return;
}
if (!this.is_ws) { //
var data = tcppkg.package_data(cmd);
this.write(data);
return;
}
else {
this.send(cmd);
}
}
// 关闭一个session
function session_close(session) {
if (!session.is_ws) {
session.end();
return;
}
else {
session.close();
}
}
// -------------------------------
function add_client_session_event(session, proto_type) {
session.on("close", function() {
on_session_exit(session);
session.end();
});
session.on("data", function(data) {
//
if (!Buffer.isBuffer(data)) { // 不合法的数据
session_close(session);
return;
}
// end
var last_pkg = session.last_pkg;
if (last_pkg != null) { // 上一次剩余没有处理完的半包;
var buf = Buffer.concat([last_pkg, data], last_pkg.length + data.length);
last_pkg = buf;
}
else {
last_pkg = data;
}
var offset = 0;
var pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
return;
}
while(offset + pkg_len <= last_pkg.length) { // 判断是否有完整的包;
// 根据长度信息来读取我们的数据,架设我们穿过来的是文本数据
var cmd_buf;
// 收到了一个完整的数据包
if (session.proto_type == proto_man.PROTO_JSON) {
var json_str = last_pkg.toString("utf8", offset + 2, offset + pkg_len);
if (!json_str) {
session_close(session);
return;
}
on_session_recv_cmd(session, json_str);
}
else {
cmd_buf = Buffer.allocUnsafe(pkg_len - 2); // 2个长度信息
last_pkg.copy(cmd_buf, 0, offset + 2, offset + pkg_len);
on_session_recv_cmd(session, cmd_buf);
}
offset += pkg_len;
if (offset >= last_pkg.length) { // 正好我们的包处理完了;
break;
}
pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
break;
}
}
// 能处理的数据包已经处理完成了,保存 0.几个包的数据
if (offset >= last_pkg.length) {
last_pkg = null;
}
else { // offset, length这段数据拷贝到新的Buffer里面
var buf = Buffer.allocUnsafe(last_pkg.length - offset);
last_pkg.copy(buf, 0, offset, last_pkg.length);
last_pkg = buf;
}
session.last_pkg = last_pkg;
});
session.on("error", function(err) {
});
on_session_enter(session, proto_type, false);
}
function start_tcp_server(ip, port, proto_type) {
var str_proto = {
1: "PROTO_JSON",
2: "PROTO_BUF"
};
log.info("start tcp server ..", ip, port, str_proto[proto_type]);
var server = net.createServer(function(client_sock) {
add_client_session_event(client_sock, proto_type);
});
// 监听发生错误的时候调用
server.on("error", function() {
log.error("server listen error");
});
server.on("close", function() {
log.error("server listen close");
});
server.listen({
port: port,
host: ip,
exclusive: true,
});
}
// -------------------------
function isString(obj){ //判断对象是否是字符串
return Object.prototype.toString.call(obj) === "[object String]";
}
function ws_add_client_session_event(session, proto_type) {
// close事件
session.on("close", function() {
on_session_exit(session);
session.close();
});
// error事件
session.on("error", function(err) {
});
// end
session.on("message", function(data) {
if (session.proto_type == proto_man.PROTO_JSON) {
if (!isString(data)) {
session_close(session);
return;
}
on_session_recv_cmd(session, data);
}
else {
if (!Buffer.isBuffer(data)) {
session_close(session);
return;
}
on_session_recv_cmd(session, data);
}
});
// end
on_session_enter(session, proto_type, true);
}
function start_ws_server(ip, port, proto_type) {
var str_proto = {
1: "PROTO_JSON",
2: "PROTO_BUF"
};
log.info("start ws server ..", ip, port, str_proto[proto_type]);
var server = new ws.Server({
host: ip,
port: port,
});
function on_server_client_comming (client_sock) {
ws_add_client_session_event(client_sock, proto_type);
}
server.on("connection", on_server_client_comming);
function on_server_listen_error(err) {
log.error("ws server listen error!!");
}
server.on("error", on_server_listen_error);
function on_server_listen_close(err) {
log.error("ws server listen close!!");
}
server.on("close", on_server_listen_close);
}
module.exports = netbus;
以后使用start_tcp_server或start_ws_server监听端口后,就能方便地使用session_send和session_close发送数据和关闭链接,同时在on_session_recv_cmd中接收到的数据可给service层来进行使用了
协议管理模块
协议规定数据分为三个部分: 服务号,命令号,数据部分;
1: 提供协议解码函数 – cmd = {};
cmd[0] 服务号; cmd[1]命令号; cmd[2] body 三个部分;
2:提供协议编码函数;
stype, cmd_type, body —> json字符串 或 buffer(2, 2, body)
3: 提供协议服务端buf解码器注册函数;
4: 提供协议服务端buf编码器注册函数;
因为Json数据都带key所以天然知晓含义,而用二进制传输的需要手动编写编码和解码函数,可以用protobuf来代替这部分工作,也可以手动进行编写,这里采用手动编写的方式。
/* 规定:
(1)服务号 命令号 不能为0
(2)服务号与命令号大小不能超过2个字节的整数;
(3) buf协议里面2个字节来存放服务号(0开始的2个字节),命令号(1开始的2个字节);
(4) 加密,解密,
(5) 服务号命令号二进制中都用小尾存储
(6) 所有的文本,都使用utf8
*/
var log = require("../utils/log.js");
var netbus = require("./netbus.js");
var proto_man = {};
// 加密
function encrypt_cmd(str_or_buf) {
return str_or_buf;
}
// 解密
function decrypt_cmd(str_or_buf) {
return str_or_buf;
}
function _json_encode(stype, ctype, body) {
var cmd = {};
cmd[0] = stype;
cmd[1] = ctype;
cmd[2] = body;
var str = JSON.stringify(cmd);
return str;
}
function json_decode(cmd_json) {
var cmd = JSON.parse(cmd_json);
if (!cmd ||
typeof(cmd[0])=="undefined" ||
typeof(cmd[1])=="undefined" ||
typeof(cmd[2])=="undefined") {
return null;
}
return cmd;
}
// key, value, stype + ctype -->key: value
function get_key(stype, ctype) {
return (stype * 65536 + ctype);
}
// 参数1: 协议类型 json, buf协议;
// 参数2: 服务类型
// 参数3: 命令号;
// 参数4: 发送的数据本地,js对象/js文本,...
// 返回是一段编码后的数据;
function encode_cmd(proto_type, stype, ctype, body) {
var buf = null;
if (proto_type == netbus.PROTO_JSON) {
buf = _json_encode(stype, ctype, body);
}
else { // buf协议
var key = get_key(stype, ctype);
if (!encoders[key]) {
return null;
}
// end
buf = encoders[key](body);
}
if (buf) {
buf = encrypt_cmd(buf); // 加密
}
return buf;
}
// 参数1: 协议类型
// 参数2: 接手到的数据命令
// 返回: {0: stype, 1, ctype, 2: body}
function decode_cmd(proto_type, str_or_buf) {
str_or_buf = decrypt_cmd(str_or_buf); // 解密
if (proto_type == netbus.PROTO_JSON) {
return json_decode(str_or_buf);
}
var cmd = null;
var stype = str_or_buf.readUInt16LE(0);
var ctype = str_or_buf.readUInt16LE(2);
var key = get_key(stype, ctype);
if (!decoders[key]) {
return null;
}
cmd = decoders[key](str_or_buf);
return cmd;
}
// buf协议的编码/解码管理 stype, ctype --> encoder/decoder
var decoders = {}; // 保存当前我们buf协议所有的解码函数, stype,ctype --> decoder;
var encoders = {}; // 保存当前我们buf协议所有的编码函数, stype, ctype --> encoder
// encode_func(body) return 二进制bufffer对象
function reg_buf_encoder(stype, ctype, encode_func) {
var key = get_key(stype, ctype);
if (encoders[key]) { // 已经注册过了,是否搞错了
log.warn("stype: " + stype + " ctype: " + ctype + "is reged!!!");
}
encoders[key] = encode_func;
}
// decode_func(cmd_buf) return cmd { 0: 服务号, 1: 命令号, 2: body};
function reg_buf_decoder(stype, ctype, decode_func) {
var key = get_key(stype, ctype);
if (decoders[key]) { // 已经注册过了,是否搞错了
log.warn("stype: " + stype + " ctype: " + ctype + "is reged!!!");
}
decoders[key] = decode_func;
}
proto_man.encode_cmd = encode_cmd;
proto_man.decode_cmd = decode_cmd;
proto_man.reg_decoder = reg_buf_decoder;
proto_man.reg_encoder = reg_buf_encoder;
module.exports = proto_man;
使用案例
var log = require("../utils/log.js");
var netbus = require("../netbus/netbus.js");
var proto_man = require("../netbus/proto_man.js");
var data = {
uname: "blake",
upwd: "123456",
};
// json 编码和解码
var buf = proto_man.encode_cmd(netbus.PROTO_JSON, 1, 1, data);
log.info(buf); // 编码好的
log.error("json length: ", buf.length);
var cmd = proto_man.decode_cmd(netbus.PROTO_JSON, buf);
log.info(cmd); // {0: 1, 1, 1, 2: data}
// end
// 二进制
function encode_cmd_1_1(body) {
var stype = 1;
var ctype = 1;
var total_len = 2 + 2 + body.uname.length + body.upwd.length + 2 + 2;
var buf = Buffer.allocUnsafe(total_len);
buf.writeUInt16LE(stype, 0); // 0, 1
buf.writeUInt16LE(ctype, 2); // 2, 3
// uname的字符串
buf.writeUInt16LE(body.uname.length, 4); // 4, 5
buf.write(body.uname, 6); // 6写入uname的字符串
// end
var offset = 6 + body.uname.length;
buf.writeUInt16LE(body.upwd.length, offset); // offset + 0, offset + 1
buf.write(body.uname, offset + 2); // offset + 2写入upwd的字符串
return buf;
}
function decode_cmd_1_1(cmd_buf) {
var stype = 1;
var ctype = 1;
// uname
var uname_len = cmd_buf.readUInt16LE(4);
if((uname_len + 2 + 2 + 2) > cmd_buf.length) {
return null;
}
var uname = cmd_buf.toString("utf8", 6, 6 + uname_len);
if (!uname) {
return null;
}
// end
var offset = 6 + uname_len;
var upwd_len = cmd_buf.readUInt16LE(offset);
if ((offset + upwd_len + 2) > cmd_buf.length) {
return null;
}
var upwd = cmd_buf.toString("utf8", offset + 2, offset + 2 + upwd_len);
var cmd = {
0: 1,
1: 1,
2: {
"uname": uname,
"upwd": upwd,
}
};
return cmd;
}
proto_man.reg_encoder(1, 1, encode_cmd_1_1);
proto_man.reg_decoder(1, 1, decode_cmd_1_1);
// end
var proto_cmd_buf = proto_man.encode_cmd(netbus.PROTO_BUF, 1, 1, data);
log.info(proto_cmd_buf);
log.error(proto_cmd_buf.length);
cmd = proto_man.decode_cmd(netbus.PROTO_BUF, proto_cmd_buf);
log.info(cmd);
Service服务管理模块
service_manager.js
var log = require("../utils/log.js");
var proto_man = require("./proto_man");
var service_modules = {};
function register_service(stype, service) {
if (service_modules[stype]) {
log.warn(service_modules[stype].name + " service is registed !!!!");
}
service_modules[stype] = service;
service.init();
}
function on_recv_client_cmd(session, str_or_buf) {
// 根据我们的收到的数据解码我们命令
var cmd = proto_man.decode_cmd(session.proto_type, str_or_buf);
if (!cmd) {
return false;
}
// end
var stype, ctype, body;
stype = cmd[0];
ctype = cmd[1];
body = cmd[2];
if (service_modules[stype]) {
service_modules[stype].on_recv_player_cmd(session, ctype, body);
}
return true;
}
// 玩家掉线就走这里
function on_client_lost_connect(session) {
// 遍历所有的服务模块通知在这个服务上的这个玩家掉线了
for(var key in service_modules) {
service_modules[key].on_player_disconnect(session);
}
}
var service_manager = {
on_client_lost_connect: on_client_lost_connect,
on_recv_client_cmd: on_recv_client_cmd,
register_service: register_service,
};
module.exports = service_manager;
netbus修改:
function on_session_exit(session) {
log.info("session exit !!!!");
service_manager.on_client_lost_connect(session);
session.last_pkg = null;
if (global_session_list[session.session_key]) {
global_session_list[session.session_key] = null;
delete global_session_list[session.session_key]; // 把这个key, value从 {}里面删除
session.session_key = null;
}
}
function on_session_recv_cmd(session, str_or_buf) {
if(!service_manager.on_recv_client_cmd(session, str_or_buf)) {
session_close(session);
}
}
Service模块的编写模板
talk_room.js
var log = require("../utils/log.js");
require("./talk_room_proto.js");
var service = {
stype: 1, // 服务号
name: "talk room", // 服务名称
// 每个服务初始化的时候调用
init: function () {
log.info(this.name + " services init!!!");
},
// 每个服务收到数据的时候调用
on_recv_player_cmd: function(session, ctype, body) {
log.info(this.name + " on_recv_player_cmd: ", ctype, body);
},
// 每个服务连接丢失后调用,被动丢失连接
on_player_disconnect: function(session) {
log.info(this.name + " on_player_disconnect: ", session.session_key);
},
};
module.exports = service;
使用案例
在服务器的入口注册服务
var talk_room = require("./talk_room");
service_manager.register_service(1, talk_room);
这样,服务器收到对应的信息之后会自动转到特定的服务进行处理,就把业务分成了不同的Service模块
读写utf8扩展模块
// 扩展DataView 读/写字符串 -->utf8的
DataView.prototype.write_utf8 = function(offset, str) {
var now = offset;
var dataview = this;
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) {
dataview.setUint8(now, charcode);
now ++;
}
else if (charcode < 0x800) {
dataview.setUint8(now, (0xc0 | (charcode >> 6)));
now ++;
dataview.setUint8(now, 0x80 | (charcode & 0x3f));
now ++;
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
dataview.setUint8(now, 0xe0 | (charcode >> 12));
now ++;
dataview.setUint8(now, 0x80 | ((charcode>>6) & 0x3f));
now ++;
dataview.setUint8(now, 0x80 | (charcode & 0x3f));
now ++;
}
// surrogate pair
else {
i ++;
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff));
dataview.setUint8(now, 0xf0 | (charcode >>18));
now ++;
dataview.setUint8(now, 0x80 | ((charcode>>12) & 0x3f));
now ++;
dataview.setUint8(now, 0x80 | ((charcode>>6) & 0x3f));
now ++;
dataview.setUint8(now, 0x80 | (charcode & 0x3f));
now ++;
}
}
}
DataView.prototype.read_utf8 = function(offset, byte_length) {
var out, i, len, c;
var char2, char3;
var dataview = this;
out = "";
len = byte_length;
i = offset;
while(i < len) {
c = dataview.getUint8(i);
i ++;
switch(c >> 4)
{
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = dataview.getUint8(i);
i ++;
char3 = dataview.getUint8(i);
i ++;
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
// 字符串 你好, 长度是2,不代表字节数,buf协议,写入我们的字符串的字节数,String扩充一个接口
String.prototype.utf8_byte_len = function() {
var totalLength = 0;
var i;
var charCode;
for (i = 0; i < this.length; i++) {
charCode = this.charCodeAt(i);
if (charCode < 0x007f) {
totalLength = totalLength + 1;
}
else if ((0x0080 <= charCode) && (charCode <= 0x07ff)) {
totalLength += 2;
}
else if ((0x0800 <= charCode) && (charCode <= 0xffff)) {
totalLength += 3;
}
}
return totalLength;
}
/*
var str = "你好";
var buf = new ArrayBuffer(str.utf8_byte_len());
var dataview = new DataView(buf);
dataview.write_utf8(0, str);
var str2 = dataview.read_utf8(0, str.utf8_byte_len());
console.log(str2);
*/
二进制协议工具模块
包括:读写buffer的方法,编码解码数据的方法
通常发送的数据可以分为几类:空的数据包、只包含状态码的的数据包、包含字符串的数据包。可以根据这几类编写统一的编码解码函数。
新建proto_tools.js, 修改proto_man,buf encoder加入(stype, ctype)
3: 添加read_int16, read_int32, write_int32, write_int16, alloc_buffer,
write_string, read_string;
4: 编写encode_empty_cmd/decode_empty_cmd,只有stype, ctype;
5: 编写stype, ctype, status的编码和解码 encode_status_cmd/decode_status_cmd
6: 编写写入和读取字符串: write_string_inbuf/read_string_inbuf
7: 编写encode_str_cmd, decode_str_cmd;
8: 编写write_cmd_header,写入stype和ctype头,返回offset;
9: 导出这些基本函数
10:修改广播的时候判断二进制协议的bug talkroom.js
function read_int8(cmd_buf, offset) {
return cmd_buf.readInt8(offset);
}
function write_int8(cmd_buf, offset, value) {
cmd_buf.writeInt8(value, offset);
}
function read_int16(cmd_buf, offset) {
return cmd_buf.readInt16LE(offset);
}
function write_int16(cmd_buf, offset, value) {
cmd_buf.writeInt16LE(value, offset);
}
function read_int32(cmd_buf, offset) {
return cmd_buf.readInt32LE(offset);
}
function write_int32(cmd_buf, offset, value) {
cmd_buf.writeInt32LE(value, offset);
}
function read_uint32(cmd_buf, offset) {
return cmd_buf.readUInt32LE(offset);
}
function write_uint32(cmd_buf, offset, value) {
cmd_buf.writeUInt32LE(value, offset);
}
function read_str(cmd_buf, offset, byte_len) {
return cmd_buf.toString("utf8", offset, offset + byte_len);
}
// 性能考虑
function write_str(cmd_buf, offset, str) {
cmd_buf.write(str, offset);
}
function read_float(cmd_buf, offset) {
return cmd_buf.readFloatLE(offset);
}
function write_float(cmd_buf, offset, value) {
cmd_buf.writeFloatLE(value, offset);
}
function alloc_buffer(total_len) {
return Buffer.allocUnsafe(total_len);
}
function write_cmd_header_inbuf(cmd_buf, stype, ctype) {
write_int16(cmd_buf, 0, stype);
write_int16(cmd_buf, 2, ctype);
write_uint32(cmd_buf, 4, 0);
return proto_tools.header_size;
}
function write_prototype_inbuf(cmd_buf, proto_type) {
write_int16(cmd_buf, 8, proto_type);
}
function write_utag_inbuf(cmd_buf, utag) {
write_uint32(cmd_buf, 4, utag);
}
function clear_utag_inbuf(cmd_buf) {
write_uint32(cmd_buf, 4, 0);
}
function read_cmd_header_inbuf(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 1);
ret = [cmd, proto_tools.header_size];
return ret;
}
function write_str_inbuf(cmd_buf, offset, str, byte_len) {
// 写入2个字节字符串长度信息;
write_int16(cmd_buf, offset, byte_len);
offset += 2;
write_str(cmd_buf, offset, str);
offset += byte_len;
return offset;
}
// 返回 str, offset
function read_str_inbuf(cmd_buf, offset) {
var byte_len = read_int16(cmd_buf, offset);
offset += 2;
var str = read_str(cmd_buf, offset, byte_len);
offset += byte_len;
return [str, offset];
}
function decode_empty_cmd(cmd_buf) {
var cmd = {};
cmd[0] = read_int16(cmd_buf, 0);
cmd[1] = read_int16(cmd_buf, 2);
cmd[2] = null;
return cmd;
}
function encode_empty_cmd(stype, ctype, body) {
var cmd_buf = alloc_buffer(proto_tools.header_size);
write_cmd_header_inbuf(cmd_buf, stype, ctype);
return cmd_buf;
}
function encode_status_cmd(stype, ctype, status) {
var cmd_buf = alloc_buffer(proto_tools.header_size + 2);
write_cmd_header_inbuf(cmd_buf, stype, ctype);
write_int16(cmd_buf, proto_tools.header_size, status);
return cmd_buf;
}
function decode_status_cmd(cmd_buf) {
var cmd = {};
cmd[0] = read_int16(cmd_buf, 0);
cmd[1] = read_int16(cmd_buf, 2);
cmd[2] = read_int16(cmd_buf, proto_tools.header_size);
return cmd;
}
function encode_str_cmd(stype, ctype, str) {
var byte_len = str.utf8_byte_len();
var total_len =proto_tools.header_size + 2 + byte_len;
var cmd_buf = alloc_buffer(total_len);
var offset = write_cmd_header_inbuf(cmd_buf, stype, ctype);
offset = write_str_inbuf(cmd_buf, offset, str, byte_len);
return cmd_buf;
}
function decode_str_cmd(cmd_buf) {
var cmd = {};
cmd[0] = read_int16(cmd_buf, 0);
cmd[1] = read_int16(cmd_buf, 2);
var ret = read_str_inbuf(cmd_buf, proto_tools.header_size);
cmd[2] = ret[0];
return cmd;
}
var proto_tools = {
header_size: 10, // 2 + 2 + 4 + 2;
// 原操作
read_int8: read_int8,
write_int8: write_int8,
read_int16: read_int16,
write_int16, write_int16,
read_int32: read_int32,
write_int32, write_int32,
read_float: read_float,
write_float: write_float,
read_uint32: read_uint32,
write_uint32, write_uint32,
alloc_buffer: alloc_buffer,
// 通用操作
write_cmd_header_inbuf: write_cmd_header_inbuf,
write_prototype_inbuf: write_prototype_inbuf,
write_utag_inbuf: write_utag_inbuf,
clear_utag_inbuf: clear_utag_inbuf,
write_str_inbuf: write_str_inbuf,
read_str_inbuf: read_str_inbuf,
// end
// 模板编码解码器
encode_str_cmd: encode_str_cmd,
encode_status_cmd: encode_status_cmd,
encode_empty_cmd: encode_empty_cmd,
decode_str_cmd: decode_str_cmd,
decode_status_cmd: decode_status_cmd,
decode_empty_cmd: decode_empty_cmd,
//
};
module.exports = proto_tools;
};
module.exports = proto_tools;
utils:时间戳、base64编码、sha1编码
var crypto = require("crypto");
// 返回当前的时间戳,单位是秒
function timestamp() {
var date = new Date();
var time = Date.parse(date); // 1970到现在过去的毫秒数
time = time / 1000;
return time;
}
// 时间戳是秒,Date是毫秒
function timestamp2date(time) {
var date = new Date();
date.setTime(time * 1000); //
return [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()];
}
// "2017-06-28 18:00:00"
function date2timestamp(strtime) {
var date = new Date(strtime.replace(/-/g, '/'));
var time = Date.parse(date);
return (time / 1000);
}
// 今天00:00:00的时间戳
function timestamp_today() {
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
var time = Date.parse(date); // 1970到现在过去的毫秒数
time = time / 1000;
return time;
}
function timestamp_yesterday() {
var time = timestamp_today();
return (time - 24 * 60 * 60)
}
function base64_encode(content) {
var buf = new Buffer(content);
var base64 = buf.toString("base64");
return base64;
}
function base64_decode(base64_str) {
var buf = new Buffer(base64_str, "base64");
return buf;
}
function md5(data) {
var md5 = crypto.createHash("md5");
md5.update(data);
return md5.digest('hex');
}
function sha1(data) {
var sha1 = crypto.createHash("sha1");
sha1.update(data);
return sha1.digest('hex');
}
/*
function check_params_len(body, len) {
if(!body) {
return false;
}
console.log(body.length, len);
if (body.length == len) {
return true;
}
return false;
}*/
var utils = {
random_string: function(len){
var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = $chars.length;
var str = '';
for (var i = 0; i < len; i++) {
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},
random_int_str: function(len) {
var $chars = '0123456789';
var maxPos = $chars.length;
var str = '';
for (var i = 0; i < len; i++) {
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},
// 随机的生成[begin, end] 范围内的数据
random_int: function(begin, end) {
var num = begin + Math.random() * (end - begin + 1);
num = Math.floor(num);
if (num > end) {
num = end;
}
return num;
},
timestamp: timestamp,
date2timestamp: date2timestamp,
timestamp2date: timestamp2date,
timestamp_yesterday: timestamp_yesterday,
timestamp_today: timestamp_today,
base64_decode: base64_decode,
base64_encode: base64_encode,
md5: md5,
sha1: sha1,
// check_params_len: check_params_len,
};
module.exports = utils
聊天室案例
需求:当客户端有人进入聊天室时发送进入请求,服务器回复1进入成功,同时服务器把进入聊天室的人的信息广播给其他人,并把已经在聊天室的成员信息发送给玩家,如果已经在聊天室回复-100。只有在聊天室的人才能发送信息,并广播给所有人。当玩家退出聊天室时,广播给其他玩家。Service模块编写如下:
兼容Json和二进制协议
var log = require("../utils/log.js");
var proto_man = require("../netbus/proto_man.js");
require("./talk_room_proto.js");
var STYPE_TALKROOM = 1;
var TalkCmd = {
Enter: 1, // 用户进来
Exit: 2, // 用户离开ia
UserArrived: 3, // 别人进来;
UserExit: 4, // 别人离开
SendMsg: 5, // 自己发送消息,
UserMsg: 6, // 收到别人的消息
};
var Respones = {
OK: 1,
IS_IN_TALKROOM: -100, // 玩家已经在聊天室
NOT_IN_TALKROOM: -101, // 玩家不在聊天室
INVALD_OPT: -102, // 玩家非法操作
INVALID_PARAMS: -103, // 命令格式不对
};
function broadcast_cmd(ctype, body, noto_user) {
var json_encoded = null;
var buf_encoded = null;
for(var key in room) {
if (room[key].session == noto_user) {
continue;
}
var session = room[key].session;
// 本来可以这么写
// session.send_cmd(STYPE_TALKROOM, ctype, body);
if (session.proto_type == proto_man.PROTO_JSON) {
if (json_encoded == null) {
json_encoded = proto_man.encode_cmd(proto_man.PROTO_JSON, STYPE_TALKROOM, ctype, body);
}
session.send_encoded_cmd(json_encoded);
}
else if(session.proto_type == proto_man.PROTO_BUF) {
if (buf_encoded == null) {
buf_encoded = proto_man.encode_cmd(proto_man.PROTO_BUF, STYPE_TALKROOM, ctype, body);
}
session.send_encoded_cmd(buf_encoded);
}
}
}
// 保存我们聊天室里面所有用户的
var room = {};
function on_user_enter_talkroom(session, body) {
if (!body.uname || !body.usex) {
session.send_cmd(STYPE_TALKROOM, TalkCmd.Enter, Respones.INVALID_PARAMS);
return;
}
if (room[session.session_key]) { // 已经在聊天室
session.send_cmd(STYPE_TALKROOM, TalkCmd.Enter, Respones.IS_IN_TALKROOM);
return;
}
// 告诉我们的客户端,你进来成功了
session.send_cmd(STYPE_TALKROOM, TalkCmd.Enter, Respones.OK);
// end
// 把我们进来的消息广播给其他的人
broadcast_cmd(TalkCmd.UserArrived, body, session);
// end
// 把所有在聊天室的人发送给我们的刚进来的用户
for(var key in room) {
session.send_cmd(STYPE_TALKROOM, TalkCmd.UserArrived, room[key].uinfo);
}
// end
// 保存玩家信息到聊天室
var talkman = {
session: session,
uinfo: body,
};
room[session.session_key] = talkman;
}
function on_user_exit_talkroom(session, is_lost_connect) {
if (!room[session.session_key]) { // 不再我的聊天室,你也没有离开这么一说
if (!is_lost_connect) {
session.send_cmd(STYPE_TALKROOM, TalkCmd.Exit, Respones.NOT_IN_TALKROOM);
}
return;
}
// 把我们进来的消息广播给其他的人
broadcast_cmd(TalkCmd.UserExit, room[session.session_key].uinfo, session);
// end
// 把你的数据从聊天室删除
room[session.session_key] = null;
delete room[session.session_key];
// end
// 发送命令, 你已经成功离开了。
if (!is_lost_connect) {
session.send_cmd(STYPE_TALKROOM, TalkCmd.Exit, Respones.OK);
}
}
function on_user_send_msg(session, msg) {
if (!room[session.session_key]) { // 不再我的聊天室,你也没有离开这么一说
session.send_cmd(STYPE_TALKROOM, TalkCmd.SendMsg, {
0: Respones.INVALD_OPT,
});
return;
}
// 发送成功,发给客户端
session.send_cmd(STYPE_TALKROOM, TalkCmd.SendMsg, {
0: Respones.OK,
1: room[session.session_key].uinfo.uname,
2: room[session.session_key].uinfo.usex,
3: msg,
});
// end
// 告诉给其他的人,这个人发送了一个消息
broadcast_cmd(TalkCmd.UserMsg, {
0: room[session.session_key].uinfo.uname,
1: room[session.session_key].uinfo.usex,
2: msg,
}, session);
// end
}
var service = {
stype: STYPE_TALKROOM, // 服务号
name: "talk room", // 服务名称
// 每个服务初始化的时候调用
init: function () {
log.info(this.name + " services init!!!");
},
// 每个服务收到数据的时候调用
on_recv_player_cmd: function(session, ctype, bo dy) {
log.info(this.name + " on_recv_player_cmd: ", ctype, body);
switch(ctype) {
case TalkCmd.Enter:
on_user_enter_talkroom(session, body);
break;
case TalkCmd.Exit: // 主动请求
on_user_exit_talkroom(session, false);
break;
case TalkCmd.SendMsg:
on_user_send_msg(session, body);
break;
}
},
// 每个服务连接丢失后调用,被动丢失连接
on_player_disconnect: function(session) {
log.info(this.name + " on_player_disconnect: ", session.session_key);
on_user_exit_talkroom(session, true);
},
};
module.exports = service;
使用二进制协议需要编写编码解码器
var proto_man = require("../netbus/proto_man.js");
var proto_tools = require("../netbus/proto_tools.js");
var log = require("../utils/log.js");
/*
enter:
客户端: 进入聊天室
1, 1, body = {
uname: "名字",
usex: 0 or 1, // 性别
};
返回:
1, 1, status = OK;
exit
客户端: 离开聊天室
1, 2, body = null;
返回:
1, 2, status = OK;
客户端请求发送消息
1, 5, body = "消息内容"
返回
1, 5, body = {
0: status, OK, 失败的状态码
1: uname,
2: usex,
3: msg, // 消息内容
}
UserMsg: 服务器主动发送
1, 6, body = {
0: uname,
1: usex
2: msg,
};
UserExit: 主动发送
1, 4, body = uinfo {
uname: "名字",
usex: 0, 1 // 性别
}
UserEnter: 主动发送
1, 3, body = uinfo{
uname: "名字",
usex: 0 or 1, // 性别
}
*/
function decode_enter_talkroom(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var ret = proto_tools.read_str_inbuf(cmd_buf, 4);
body.uname = ret[0];
var offset = ret[1];
body.usex = proto_tools.read_int16(cmd_buf, offset);
cmd[2] = body;
return cmd;
}
function decode_exit_talkroom(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
cmd[2] = null;
return cmd;
}
function encode_send_msg_return_talkroom(stype, ctype, body) {
if (body[0] != 1) {
return proto_tools.encode_status_cmd(stype, ctype, body[0]);
}
var uname_len = body[1].utf8_byte_len();
var msg_len = body[3].utf8_byte_len();
var total_len = 2 + 2 + 2 + 2 + uname_len + 2 + msg_len + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, 1, 5);
proto_tools.write_int16(cmd_buf, offset, body[0]);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body[1], uname_len);
proto_tools.write_int16(cmd_buf, offset, body[2]);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body[3], msg_len);
log.info(cmd_buf);
return cmd_buf;
}
function encode_user_enter(stype, ctype, body) {
var uname_len = body.uname.utf8_byte_len();
var total_len = 2 + 2 + 2 + uname_len + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.uname, uname_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
return cmd_buf;
}
function encode_user_exit(stype, ctype, body) {
var uname_len = body.uname.utf8_byte_len();
var total_len = 2 + 2 + 2 + uname_len + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.uname, uname_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
return cmd_buf;
}
function encode_use_msg(stype, ctype, body) {
var uname_len = body[0].utf8_byte_len();
var msg_len = body[2].utf8_byte_len();
var total_len = 2 + 2 + 2 + uname_len + 2 + msg_len + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body[0], uname_len);
proto_tools.write_int16(cmd_buf, offset, body[1]);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body[2], msg_len);
log.info(cmd_buf);
return cmd_buf;
}
proto_man.reg_encoder(1, 1, proto_tools.encode_status_cmd);
proto_man.reg_encoder(1, 2, proto_tools.encode_status_cmd);
proto_man.reg_encoder(1, 5, encode_send_msg_return_talkroom);
proto_man.reg_encoder(1, 3, encode_user_enter);
proto_man.reg_encoder(1, 4, encode_user_exit);
proto_man.reg_encoder(1, 6, encode_use_msg);
proto_man.reg_decoder(1, 1, decode_enter_talkroom);
proto_man.reg_decoder(1, 2, decode_exit_talkroom);
proto_man.reg_decoder(1, 5, proto_tools.decode_str_cmd);
分布式服务器
服务器基于分布式框架,主要有以下几类服务器
- gateway:网关服务器,需要注册所有服务,因为需要接收所有数据并转发,不需要解包。另外需要编写一个定时器来检查与其他服务器的连接,当接收到登录请求的时候,需要判断是否已经有人登陆;发送其他数据的时候,判断登录状态(通过uid)。当用户断线的时候,需要对session做清理,并通知所有其他服务器。
- authserver:管理用户数据,实现登录,修改用户数据,账号的升级,登出的功能,发送用户数据给客户端。
- systemserver:管理游戏大厅数据,实现金币、经验等游戏数据的获取,修改,每日登陆奖励等。
- logicserver:
- 管理房间内玩家的数据,实现逻辑服务器的登录,断开链接的响应,进入和退出比赛的响应,与最重要的同步。
- 玩家在该服务器上都有一个player的对象,并用房间类match_mgr来管理一局比赛的玩家,该服务器主要使用面向对象来编程。
- Webserver:提供Http服务,与第三方服务器进行通信,提供资源的上传下载。
除了服务器之外,需要编写一个数据库的脚本来提供数据库操作的服务。
服务器如何做到高效
1:凡是CPU需要等待的操作,全部为异步模式,提高CPU的利用率;
2: 服务器每次处理,都需要消耗时间片,那么在这个时间片段内无法响应其它的请求, 那么要提高处理能力,尽可能的减少时间片,比如优化算法,比如使用C/C++编写耗时代码;
3: 服务器与系统的交互,通过增加机器总能解决问题,和系统交互实时性要求不高,这部分好处理;
4: 玩家与玩家以及与游戏服务器之前的交互式不能靠简单的增加机器来解决。
(1) 提高单台游戏服务的负载人数;
(2) 将一个服务拆分成多个服务,比如一个地图,有几个地图服务器,形成服务器组; 把一个任务分成多个机器使用;
(3)分开导流,如果一组解决不了问题,再加一组服务器,不过这两组服务器之间的玩家,彼此不能交互,实际能满足流量需求,也保证了玩家的可玩性。5000个人就能够展现在线的乐趣,何必非要10000人在同一组服务器里彼此看到呢?
5:如何防外挂:
(1)加密协议,尽量做到不被破解; (2) 关键逻辑的判断全部都在服务器,即使破解了协议,也能保证游戏的公平性,不轻易相信客户端发过来的参数。
(3)确保这个客户端,是自己发布的,不是其它的工具或有串改;
底层框架Pro
- 为了提高效率,网关的Json在编码解码时不需要解开所有数据,而只需要解开服务号,命令号即可,然后把剩余的数据发送给其他服务器,因此Json编码的时服务号和命令号使用二进制编码,body部分使用Json编码即可。
- protoubf同理,如果服务器是网关,只解开头部的服务器号和命令号,然后转发即可
- 加密协议的部分可以放到协议管理模块给协议进行编码之后,解密协议的部分可以放到解码之前。并且只有网关才需要设置加密解密,和内部服务器的通信不需要。
- 为了在内部服务器之间辨别数据是哪个用户发送过来的,需要在数据的服务号命令号之后加上4个字节的用户标识utag。
- 为了识别协议类型,在utag后加入一个2个字节的协议类型(Json还是二进制),用于支持两种数据的接收。
- 在netbus新增服务器作为客户端连接其他服务器的函数connect_tcp_server
netbus网络底层
var net = require("net");
var ws = require("ws");
var log = require("../utils/log.js");
var tcppkg = require("./tcppkg.js");
var proto_man = require("./proto_man.js");
var service_manager = require("./service_manager.js");
var netbus = {
start_tcp_server: start_tcp_server,
start_ws_server: start_ws_server,
// session_send: session_send,
session_close: session_close,
get_client_session: get_client_session,
connect_tcp_server: connect_tcp_server,
get_server_session: get_server_session,
};
var global_session_list = {};
var global_seesion_key = 1;
function get_client_session(session_key) {
return global_session_list[session_key];
}
// 有客户端的session接入进来,Json和二进制协议都走这里
function on_session_enter(session, is_ws, is_encrypt) {
if (is_ws) {
log.info("session enter", session._socket.remoteAddress, session._socket.remotePort);
}
else {
log.info("session enter", session.remoteAddress, session.remotePort);
}
session.last_pkg = null; // 表示我们存储的上一次没有处理完的TCP包;
session.is_ws = is_ws;
session.is_connected = true;
session.is_encrypt = is_encrypt;
session.uid = 0; // 用户的UID
// 扩展session的方法
session.send_encoded_cmd = session_send_encoded_cmd;
session.send_cmd = session_send_cmd;
// end
// 加入到我们的serssion 列表里面
global_session_list[global_seesion_key] = session;
session.session_key = global_seesion_key;
global_seesion_key ++;
// end
}
// 当客户端断开链接
function on_session_exit(session) {
session.is_connected = false;
service_manager.on_client_lost_connect(session);
session.last_pkg = null;
if (global_session_list[session.session_key]) {
global_session_list[session.session_key] = null;
delete global_session_list[session.session_key]; // 把这个key, value从 {}里面删除
session.session_key = null;
}
}
// 一定能够保证是一个整包;
// 包含了服务号stype,命令号ctype,用户标识utag,协议类型proto_type,数据body。Json协议的body是字符串,二进制协议的body是二进制
function on_session_recv_cmd(session, str_or_buf) {
if(!service_manager.on_recv_client_cmd(session, str_or_buf)) {
session_close(session);
}
}
// 打包数据,并发送命令
function session_send_cmd(stype, ctype, body, utag, proto_type) {
if (!this.is_connected) {
return;
}
var cmd = null;
cmd = proto_man.encode_cmd(utag, proto_type, stype, ctype, body);
if (cmd) {
this.send_encoded_cmd(cmd);
}
}
// 发送打包好的命令
function session_send_encoded_cmd(cmd) {
if (!this.is_connected) {
return;
}
if(this.is_encrypt) {
cmd = proto_man.encrypt_cmd(cmd);
}
if (!this.is_ws) { // 加上长度信息
var data = tcppkg.package_data(cmd);
this.write(data);
return;
}
else {
this.send(cmd);
}
}
// 关闭一个session
function session_close(session) {
if (!session.is_ws) {
session.end();
return;
}
else {
session.close();
}
}
// 这里包含了处理TCP粘包和半包的逻辑
function add_client_session_event(session, is_encrypt) {
session.on("close", function() {
on_session_exit(session);
session.end();
});
session.on("data", function(data) {
//
if (!Buffer.isBuffer(data)) { // 不合法的数据
session_close(session);
return;
}
// end
var last_pkg = session.last_pkg;
if (last_pkg != null) { // 上一次剩余没有处理完的半包;
var buf = Buffer.concat([last_pkg, data], last_pkg.length + data.length);
last_pkg = buf;
}
else {
last_pkg = data;
}
var offset = 0;
var pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
return;
}
while(offset + pkg_len <= last_pkg.length) { // 判断是否有完整的包;
// 根据长度信息来读取我们的数据,架设我们穿过来的是文本数据
var cmd_buf;
// 收到了一个完整的数据包
{
cmd_buf = Buffer.allocUnsafe(pkg_len - 2); // 2个长度信息
last_pkg.copy(cmd_buf, 0, offset + 2, offset + pkg_len);
on_session_recv_cmd(session, cmd_buf);
}
offset += pkg_len;
if (offset >= last_pkg.length) { // 正好我们的包处理完了;
break;
}
pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
break;
}
}
// 能处理的数据包已经处理完成了,保存 0.几个包的数据
if (offset >= last_pkg.length) {
last_pkg = null;
}
else { // offset, length这段数据拷贝到新的Buffer里面
var buf = Buffer.allocUnsafe(last_pkg.length - offset);
last_pkg.copy(buf, 0, offset, last_pkg.length);
last_pkg = buf;
}
session.last_pkg = last_pkg;
});
session.on("error", function(err) {
});
on_session_enter(session, false, is_encrypt);
}
// 开启TCP服务器
function start_tcp_server(ip, port, is_encrypt) {
var str_proto = {
1: "PROTO_JSON",
2: "PROTO_BUF"
};
log.info("start tcp server ..", ip, port);
var server = net.createServer(function(client_sock) {
add_client_session_event(client_sock);
});
// 监听发生错误的时候调用
server.on("error", function() {
log.error("server listen error");
});
server.on("close", function() {
log.error("server listen close");
});
server.listen({
port: port,
host: ip,
exclusive: true,
});
}
// -------------------------
function isString(obj){ //判断对象是否是字符串
return Object.prototype.toString.call(obj) === "[object String]";
}
function ws_add_client_session_event(session, is_encrypt) {
// close事件
session.on("close", function() {
on_session_exit(session);
session.close();
});
// error事件
session.on("error", function(err) {
});
// end
session.on("message", function(data) {
{
if (!Buffer.isBuffer(data)) {
session_close(session);
return;
}
on_session_recv_cmd(session, data);
}
});
// end
on_session_enter(session, true, is_encrypt);
}
// 开启WebSocket服务器
function start_ws_server(ip, port, is_encrypt) {
var str_proto = {
1: "PROTO_JSON",
2: "PROTO_BUF"
};
log.info("start ws server ..", ip, port);
var server = new ws.Server({
host: ip,
port: port,
});
function on_server_client_comming (client_sock) {
ws_add_client_session_event(client_sock, is_encrypt);
}
server.on("connection", on_server_client_comming);
function on_server_listen_error(err) {
log.error("ws server listen error!!");
}
server.on("error", on_server_listen_error);
function on_server_listen_close(err) {
log.error("ws server listen close!!");
}
server.on("close", on_server_listen_close);
}
// session成功接入服务器
var server_connect_list = {};
function get_server_session(stype) {
return server_connect_list[stype];
}
function on_session_connected(stype, session, is_ws, is_encrypt) {
if (is_ws) {
log.info("session connect:", session._socket.remoteAddress, session._socket.remotePort);
}
else {
log.info("session connect:", session.remoteAddress, session.remotePort);
}
session.last_pkg = null; // 表示我们存储的上一次没有处理完的TCP包;
session.is_ws = is_ws;
session.is_connected = true;
session.is_encrypt = is_encrypt;
// 扩展session的方法
session.send_encoded_cmd = session_send_encoded_cmd;
session.send_cmd = session_send_cmd;
// end
// 加入到我们的serssion 列表里面
server_connect_list[stype] = session;
session.session_key = stype;
// end
}
function on_session_disconnect(session) {
session.is_connected = false;
var stype = session.session_key;
session.last_pkg = null;
session.session_key = null;
if (server_connect_list[stype]) {
server_connect_list[stype] = null;
delete server_connect_list[stype]; // 把这个key, value从 {}里面删除
}
}
function on_recv_cmd_server_return(session, str_or_buf) {
if(!service_manager.on_recv_server_return(session, str_or_buf)) {
session_close(session);
}
}
// 用于给网关连接到其他服务器
function connect_tcp_server(stype, host, port, is_encrypt) {
var session = net.connect({
port: port,
host: host,
});
session.is_connected = false;
session.on("connect",function() {
on_session_connected(stype, session, false, is_encrypt);
});
session.on("close", function() {
if (session.is_connected === true) {
on_session_disconnect(session);
}
session.end();
// 重新连接到服务器
setTimeout(function() {
log.warn("reconnect: ", stype, host, port, is_encrypt);
connect_tcp_server(stype, host, port, is_encrypt);
}, 3000);
// end
});
session.on("data", function(data) {
//
if (!Buffer.isBuffer(data)) { // 不合法的数据
session_close(session);
return;
}
// end
var last_pkg = session.last_pkg;
if (last_pkg != null) { // 上一次剩余没有处理完的半包;
var buf = Buffer.concat([last_pkg, data], last_pkg.length + data.length);
last_pkg = buf;
}
else {
last_pkg = data;
}
var offset = 0;
var pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
return;
}
while(offset + pkg_len <= last_pkg.length) { // 判断是否有完整的包;
// 根据长度信息来读取我们的数据,架设我们穿过来的是文本数据
var cmd_buf;
// 收到了一个完整的数据包
{
cmd_buf = Buffer.allocUnsafe(pkg_len - 2); // 2个长度信息
last_pkg.copy(cmd_buf, 0, offset + 2, offset + pkg_len);
on_recv_cmd_server_return(session, cmd_buf);
}
offset += pkg_len;
if (offset >= last_pkg.length) { // 正好我们的包处理完了;
break;
}
pkg_len = tcppkg.read_pkg_size(last_pkg, offset);
if (pkg_len < 0) {
break;
}
}
// 能处理的数据包已经处理完成了,保存 0.几个包的数据
if (offset >= last_pkg.length) {
last_pkg = null;
}
else { // offset, length这段数据拷贝到新的Buffer里面
var buf = Buffer.allocUnsafe(last_pkg.length - offset);
last_pkg.copy(buf, 0, offset, last_pkg.length);
last_pkg = buf;
}
session.last_pkg = last_pkg;
});
session.on("error", function(err) {
});
}
module.exports = netbus;
proto_man协议管理模块
主要负责Json和二进制协议的编码,解码。获得cmd
/* 规定:
(1)服务号 命令号 不能为0
(2)服务号与命令号大小不能超过2个字节的整数;
(3) buf协议里面2个字节来存放服务号(0开始的2个字节),命令号(1开始的2个字节); utag用户标识(4个字节);proto_type协议类型(2个字节)
(4) 加密,解密,
(5) 服务号命令号二进制中都用小尾存储
(6) 所有的文本,都使用utf8
*/
// 数据: 服务号2字节 | 命令号2字节 | 用户标识4字节 | 协议类型2字节 | 数据body
var log = require("../utils/log.js");
var proto_tools = require("./proto_tools.js");
var proto_man = {
PROTO_JSON: 1,
PROTO_BUF: 2,
encode_cmd: encode_cmd,
decode_cmd: decode_cmd,
reg_decoder: reg_buf_decoder,
reg_encoder: reg_buf_encoder,
decrypt_cmd: decrypt_cmd,
encrypt_cmd: encrypt_cmd,
decode_cmd_header: decode_cmd_header,
};
// 加密数据,待完成
function encrypt_cmd(str_of_buf) {
return str_of_buf;
}
// 解密数据,待完成
function decrypt_cmd(str_of_buf) {
return str_of_buf;
}
// Json协议的编码函数
function _json_encode(stype, ctype, body) {
var cmd = {};
cmd[0] = body;
var str = JSON.stringify(cmd);
// stype, ctype, str, 打入到我们的buffer
var cmd_buf = proto_tools.encode_str_cmd(stype, ctype, str);
return cmd_buf;
}
// Json协议的解码函数,直接返回数据body
function _json_decode(cmd_buf) {
var cmd = proto_tools.decode_str_cmd(cmd_buf);
var cmd_json = cmd[2];
try {
var body_set = JSON.parse(cmd_json);
cmd[2] = body_set[0];
}
catch(e) {
return null;
}
if (!cmd ||
typeof(cmd[0])=="undefined" ||
typeof(cmd[1])=="undefined" ||
typeof(cmd[2])=="undefined") {
return null;
}
return cmd;
}
// 根据命令号和服务号生成key
function get_key(stype, ctype) {
return (stype * 65536 + ctype);
}
// proto_type: 协议类型 json, buf协议;
// stype: 服务类型
// ctype: 命令号;
// body: 发送的数据本体,js对象/js文本,二进制数据
// 返回是一段编码后的数据;
function encode_cmd(utag, proto_type, stype, ctype, body) {
var buf = null;
// 根据两种不同的协议写入stype,ctype,body
if (proto_type == proto_man.PROTO_JSON) { // 如果是json协议,直接编码
buf = _json_encode(stype, ctype, body);
}
else { // buf协议,根据我们注册的的编码函数编码
var key = get_key(stype, ctype);
if (!encoders[key]) {
return null;
}
// end
// buf = encoders[key](body);
buf = encoders[key](stype, ctype, body);
}
// 写入utag,proto_type
proto_tools.write_utag_inbuf(buf, utag);
proto_tools.write_prototype_inbuf(buf, proto_type);
/*if (buf) {
buf = encrypt_cmd(buf); // 加密
}*/
return buf;
}
// 只解码header
function decode_cmd_header(cmd_buf) {
var cmd = {};
if (cmd_buf.length < proto_tools.header_size) {
return null;
}
cmd[0] = proto_tools.read_int16(cmd_buf, 0); // stype
cmd[1] = proto_tools.read_int16(cmd_buf, 2); // ctype
cmd[2] = proto_tools.read_uint32(cmd_buf, 4);// utag
cmd[3] = proto_tools.read_int16(cmd_buf, 8); // proto_type
return cmd;
}
// 解码body本体。如果是json协议,直接解码,如果是二进制协议,根据注册的解码函数解码
function decode_cmd(proto_type, stype, ctype, cmd_buf) {
log.info(cmd_buf);
// str_or_buf = decrypt_cmd(str_or_buf); // 解密
if (cmd_buf.length < proto_tools.header_size) {
return null;
}
if (proto_type == proto_man.PROTO_JSON) {
return _json_decode(cmd_buf);
}
var cmd = null;
var key = get_key(stype, ctype);
if (!decoders[key]) {
return null;
}
cmd = decoders[key](cmd_buf);
return cmd;
}
// buf协议的编码/解码管理 stype, ctype --> encoder/decoder
var decoders = {}; // 保存当前我们buf协议所有的解码函数, stype,ctype --> decoder;
var encoders = {}; // 保存当前我们buf协议所有的编码函数, stype, ctype --> encoder
// encode_func(body) return 二进制bufffer对象
function reg_buf_encoder(stype, ctype, encode_func) {
var key = get_key(stype, ctype);
if (encoders[key]) { // 已经注册过了,是否搞错了
log.warn("stype: " + stype + " ctype: " + ctype + "is reged!!!");
}
encoders[key] = encode_func;
}
// decode_func(cmd_buf) return cmd { 0: 服务号, 1: 命令号, 2: body};
function reg_buf_decoder(stype, ctype, decode_func) {
var key = get_key(stype, ctype);
if (decoders[key]) { // 已经注册过了,是否搞错了
log.warn("stype: " + stype + " ctype: " + ctype + "is reged!!!");
}
decoders[key] = decode_func;
}
module.exports = proto_man;
service_manager服务管理模块
var log = require("../utils/log.js");
var proto_man = require("./proto_man");
var service_modules = {};
// 注册服务
function register_service(stype, service) {
if (service_modules[stype]) {
log.warn(service_modules[stype].name + " service is registed !!!!");
}
service_modules[stype] = service;
}
// 当接收到内网服务器的返回数据时,只有连接到其他服务器的网关才会调用这个函数
function on_recv_server_return (session, cmd_buf) {
// 根据我们的收到的数据解码我们命令
if (session.is_encrypt) {
cmd_buf = proto_man.decrypt_cmd(cmd_buf);
}
var stype, ctype, body, utag, proto_type;
var cmd = proto_man.decode_cmd_header(cmd_buf);
if (!cmd) {
return false;
}
stype = cmd[0];
ctype = cmd[1];
utag = cmd[2];
proto_type = cmd[3];
if (service_modules[stype].is_transfer) { // 是否直接转发,不解包
service_modules[stype].on_recv_server_return(session, stype, ctype, null, utag, proto_type, cmd_buf);
return true;
}
var cmd = proto_man.decode_cmd(proto_type, stype, ctype, cmd_buf);
if (!cmd) {
return false;
}
// end
body = cmd[2];
service_modules[stype].on_recv_server_return(session, stype, ctype, body, utag, proto_type, cmd_buf);
return true;
}
// 当接收到客户端的返回数据时
function on_recv_client_cmd(session, cmd_buf) {
// 根据我们的收到的数据解码我们命令
if (session.is_encrypt) {
cmd_buf = proto_man.decrypt_cmd(cmd_buf);
}
var stype, ctype, body, utag, proto_type;
var cmd = proto_man.decode_cmd_header(cmd_buf);
if (!cmd) {
return false;
}
stype = cmd[0];
ctype = cmd[1];
utag = cmd[2];
proto_type = cmd[3];
if (!service_modules[stype]) {
return false;
}
if (service_modules[stype].is_transfer) {// 是否直接转发,不解包
service_modules[stype].on_recv_player_cmd(session, stype, ctype, null, utag, proto_type, cmd_buf);
return true;
}
var cmd = proto_man.decode_cmd(proto_type, stype, ctype, cmd_buf);
if (!cmd) {
return false;
}
// end
body = cmd[2];
service_modules[stype].on_recv_player_cmd(session, stype, ctype, body, utag, proto_type, cmd_buf);
return true;
}
// 玩家掉线,通知所有服务
function on_client_lost_connect(session) {
var uid = session.uid;
if (uid === 0) {
return;
}
session.uid = 0;
for(var key in service_modules) {
service_modules[key].on_player_disconnect(key, uid);
}
}
var service_manager = {
on_client_lost_connect: on_client_lost_connect,
on_recv_client_cmd: on_recv_client_cmd,
register_service: register_service,
on_recv_server_return: on_recv_server_return,
};
module.exports = service_manager;
网关服务器
网关的功能:
(1) 将客户端发送过来的请求,转发给对应的服务,管理玩家链接;
(2) 将对应服务发回来的请求转给客户端,管理服务器链接,断开连接会重连;转发时屏蔽用户标识utag信息
(3) 加密解密协议数据;
(4) 做负载均衡(可选,那么可以考虑Master-Work模式)
(5) 做登录管理,没有登录的用户的数据会被拒绝,重复登录的用户会被顶号。
(6) 始终保持和用户的链接,即使重启了其他的服务器,用户也不会断开链接,不用重新登录。
入口
require("../../init.js");
var game_config = require("../game_config.js");
var proto_man = require("../../netbus/proto_man.js");
var netbus = require("../../netbus/netbus.js");
var service_manager = require("../../netbus/service_manager.js");
var gw_service = require("./gw_service.js");
var host = game_config.gateway_config.host;
var posts = game_config.gateway_config.ports;
netbus.start_tcp_server(host, posts[0], true);
netbus.start_ws_server(host, posts[1], true);
// 链接我们的服务器
var game_server = game_config.game_server;
for(var key in game_server) {
netbus.connect_tcp_server(game_server[key].stype, game_server[key].host, game_server[key].port, false);
service_manager.register_service(game_server[key].stype, gw_service);
}
Service
var netbus = require("../../netbus/netbus.js");
var proto_tools = require("../../netbus/proto_tools.js");
var proto_man = require("../../netbus/proto_man.js");
var log = require("../../utils/log.js");
var Respones = require("../Respones.js");
var Cmd = require("../Cmd.js");
var Stype = require("../Stype.js");
require("./login_gw_proto.js");
function is_login_cmd(stype, ctype) {
if (stype != Stype.Auth) {
return false;
}
if(ctype == Cmd.Auth.GUEST_LOGIN) {
return true;
}
else if(ctype == Cmd.Auth.UNAME_LOGIN) {
return true;
}
return false;
}
function is_befor_login_cmd(stype, ctype) {
if (stype != Stype.Auth) {
return false;
}
var cmd_set = [Cmd.Auth.GUEST_LOGIN, Cmd.Auth.UNAME_LOGIN, Cmd.Auth.GET_PHONE_REG_VERIFY,
Cmd.Auth.PHONE_REG_ACCOUNT, Cmd.Auth.GET_FORGET_PWD_VERIFY, Cmd.Auth.RESET_USER_PWD];
for(var i = 0; i < cmd_set.length; i ++) {
if(ctype == cmd_set[i]) {
return true;
}
}
return false;
}
var uid_session_map = {};
function get_session_by_uid(uid) {
return uid_session_map[uid];
}
function save_session_with_uid(uid, session, proto_type) {
uid_session_map[uid] = session;
session.proto_type = proto_type;
}
function clear_session_with_uid(uid) {
uid_session_map[uid] = null;
delete uid_session_map[uid];
}
var service = {
name: "gw_service", // 服务名称
is_transfer: true, // 是否为转发模块,
// 收到客户端给我们发来的数据,打上utag,转发给服务器
on_recv_player_cmd: function(session, stype, ctype, body, utag, proto_type, raw_cmd) {
log.info(raw_cmd);
var server_session = netbus.get_server_session(stype);
if (!server_session) {
return;
}
// 打入能够标识client的utag, 登录后用uid, 登录前用session.session_key,
if(is_befor_login_cmd(stype, ctype)) {
utag = session.session_key;
}
else {
if(session.uid === 0) { // 没有登陆,发送了非法的命令
return;
}
utag = session.uid;
}
proto_tools.write_utag_inbuf(raw_cmd, utag);
// end
server_session.send_encoded_cmd(raw_cmd);
},
// 收到我们连接的服务给我们发过来的数据;清空utag,发送给客户端
on_recv_server_return: function (session, stype, ctype, body, utag, proto_type, raw_cmd) {
log.info(raw_cmd);
var client_session;
if(is_befor_login_cmd(stype, ctype)) { // utag == session_key
client_session = netbus.get_client_session(utag);
if (!client_session) {
return;
}
if (is_login_cmd(stype, ctype)) {
var cmd_ret = proto_man.decode_cmd(proto_type, stype, ctype, raw_cmd);
body = cmd_ret[2];
if (body.status == Respones.OK) {
// 重复登录,发送一个命令给这个客户端,告诉它说有人登陆
var prev_session = get_session_by_uid(body.uid);
if (prev_session) {
prev_session.send_cmd(stype, Cmd.Auth.RELOGIN, null, 0, prev_session.proto_type);
prev_session.uid = 0; // 可能回有隐患,是否通知其它的服务
netbus.session_close(prev_session);
}
// 登录之后给session附加上uid
client_session.uid = body.uid;
save_session_with_uid(body.uid, client_session, proto_type);
body.uid = 0;
raw_cmd = proto_man.encode_cmd(utag, proto_type, stype, ctype, body);
}
}
}
else { // utag is uid
client_session = get_session_by_uid(utag);
if (!client_session) {
return;
}
}
proto_tools.clear_utag_inbuf(raw_cmd);
client_session.send_encoded_cmd(raw_cmd);
},
// 收到客户端断开连接;通知其他服务
on_player_disconnect: function(stype, uid) {
if (stype == Stype.Auth) { // 由Auth服务保存的,那么我们就由Auth清空
clear_session_with_uid(uid);
}
var server_session = netbus.get_server_session(stype);
if (!server_session) {
return;
}
// 客户端被迫掉线
// var utag = session.session_key;
var utag = uid;
server_session.send_cmd(stype, Cmd.USER_DISCONNECT, null, utag, proto_man.PROTO_JSON);
},
};
module.exports = service;
使用二进制协议
需要编写两外的编码和解码函数,并在proto_man注册
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var utils = require("../../utils/utils.js");
var Respones = require("../Respones.js");
var proto_man = require("../../netbus/proto_man.js");
var proto_tools = require("../../netbus/proto_tools.js");
var log = require("../../utils/log.js");
function decode_guest_login(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
cmd[2] = body;
var offset = proto_tools.header_size;
body.status = proto_tools.read_int16(cmd_buf, offset);
if (body.status != Respones.OK) {
return cmd;
}
offset += 2;
body.uid = proto_tools.read_uint32(cmd_buf, offset);
offset += 4;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body.unick = ret[0];
offset = ret[1];
body.usex = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
body.uface = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
body.uvip = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body.ukey = ret[0];
offset = ret[1];
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.GUEST_LOGIN, decode_guest_login);
function encode_guest_login(stype, ctype, body) {
if(body.status != Respones.OK) {
return proto_tools.encode_status_cmd(stype, ctype, body.status);
}
var unick_len = body.unick.utf8_byte_len();
var ukey_len = body.ukey.utf8_byte_len();
var total_len = proto_tools.header_size + 2 + 4 + (2 + unick_len) + 2 + 2 + 2 + (2 + ukey_len);
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
proto_tools.write_int16(cmd_buf, offset, body.status);
offset += 2;
proto_tools.write_uint32(cmd_buf, offset, body.uid);
offset += 4;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.unick, unick_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uface);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uvip);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.ukey, ukey_len);
return cmd_buf;
}
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.GUEST_LOGIN, encode_guest_login);
function decode_uname_login(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
cmd[2] = body;
var offset = proto_tools.header_size;
body.status = proto_tools.read_int16(cmd_buf, offset);
if (body.status != Respones.OK) {
return cmd;
}
offset += 2;
body.uid = proto_tools.read_uint32(cmd_buf, offset);
offset += 4;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body.unick = ret[0];
offset = ret[1];
body.usex = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
body.uface = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
body.uvip = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.UNAME_LOGIN, decode_uname_login);
function encode_uname_login(stype, ctype, body) {
if(body.status != Respones.OK) {
return proto_tools.encode_status_cmd(stype, ctype, body.status);
}
var unick_len = body.unick.utf8_byte_len();
var total_len = proto_tools.header_size + 2 + 4 + (2 + unick_len) + 2 + 2 + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
proto_tools.write_int16(cmd_buf, offset, body.status);
offset += 2;
proto_tools.write_uint32(cmd_buf, offset, body.uid);
offset += 4;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.unick, unick_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uface);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uvip);
offset += 2;
return cmd_buf;
}
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.UNAME_LOGIN, encode_uname_login);
用户中心服务器
包含功能:
- 处理游客登录,正式账号的注册登录,游客账号升级
- 提供修改用户密码的接口
- 提供修改用户数据的接口
用户中心数据库模块
mysql_center.js
给用户数据库提供增查改的功能,例如:用户数据的插入修改,验证码数据库的插入修改
var mysql = require("mysql");
var util = require('util')
var Respones = require("../apps/Respones.js");
var log = require("../utils/log.js");
var utils = require("../utils/utils.js");
var conn_pool = null;
function connect_to_center(host, port, db_name, uname, upwd) {
conn_pool = mysql.createPool({
host: host, // 数据库服务器的IP地址
port: port, // my.cnf指定了端口,默认的mysql的端口是3306,
database: db_name, // 要连接的数据库
user: uname,
password: upwd,
});
}
function mysql_exec(sql, callback) {
conn_pool.getConnection(function(err, conn) {
if (err) { // 如果有错误信息
if(callback) {
callback(err, null, null);
}
return;
}
conn.query(sql, function(sql_err, sql_result, fields_desic) {
conn.release(); // 忘记加了
if (sql_err) {
if (callback) {
callback(sql_err, null, null);
}
return;
}
if (callback) {
callback(null, sql_result, fields_desic);
}
});
// end
});
}
function get_uinfo_by_uname_upwd(uname, upwd, callback) {
var sql = "select uid, unick, usex, uface, uvip, status from uinfo where uname = \"%s\" and upwd = \"%s\" and is_guest = 0 limit 1";
var sql_cmd = util.format(sql, uname, upwd);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
callback(Respones.OK, sql_ret);
});
}
function get_guest_uinfo_by_ukey(ukey, callback) {
var sql = "select uid, unick, usex, uface, uvip, status, is_guest from uinfo where guest_key = \"%s\" limit 1";
var sql_cmd = util.format(sql, ukey);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
callback(Respones.OK, sql_ret);
});
}
function insert_guest_user(unick, uface, usex, ukey, callback) {
var sql = "insert into uinfo(`guest_key`, `unick`, `uface`, `usex`, `is_guest`)values(\"%s\", \"%s\", %d, %d, 1)";
var sql_cmd = util.format(sql, ukey, unick, uface, usex);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
});
}
function insert_phone_account_user(unick, uface, usex, phone, pwd_md5, callback) {
var sql = "insert into uinfo(`uname`, `upwd`, `unick`, `uface`, `usex`, `is_guest`)values(\"%s\", \"%s\", \"%s\", %d, %d, 0)";
var sql_cmd = util.format(sql, phone, pwd_md5, unick, uface, usex);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
});
}
function edit_profile(uid, unick, usex, callback) {
var sql = "update uinfo set unick = \"%s\", usex = %d where uid = %d";
var sql_cmd = util.format(sql, unick, usex, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
})
}
function is_exist_guest(uid, callback) {
var sql = "select is_guest, status from uinfo where uid = %d limit 1";
var sql_cmd = util.format(sql, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
if(sql_ret.length <= 0) {
callback(Respones.INVALID_PARAMS);
return;
}
if (sql_ret[0].is_guest === 1 && sql_ret[0].status === 0) {
callback(Respones.OK);
return;
}
callback(Respones.INVALID_PARAMS);
});
}
function check_phone_code_valid(phone, phone_code, opt_type, callback) {
var sql = "select id from phone_chat where phone = \"%s\" and opt_type = %d and code = \"%s\" and end_time >= %d limit 1";
var t = utils.timestamp();
var sql_cmd = util.format(sql, phone, opt_type, phone_code, t);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
if(sql_ret.length <= 0) { // 找不到,才是验证码不对
callback(Respones.PHONE_CODE_ERR);
return;
}
callback(Respones.OK);
});
}
function check_phone_unuse(phone_num, callback) {
var sql = "select uid from uinfo where uname = %s limit 1";
var sql_cmd = util.format(sql, phone_num);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
if(sql_ret.length <= 0) {
callback(Respones.OK);
return;
}
callback(Respones.PHONE_IS_REG);
});
}
function check_phone_is_reged(phone_num, callback) {
var sql = "select uid from uinfo where uname = %s limit 1";
var sql_cmd = util.format(sql, phone_num);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
if(sql_ret.length <= 0) {
callback(Respones.PHONE_IS_NOT_REG);
return;
}
callback(Respones.OK);
});
}
function _is_phone_indentify_exist(phone, opt, callback) {
var sql = "select id from phone_chat where phone = \"%s\" and opt_type = %d";
var sql_cmd = util.format(sql, phone, opt);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(false);
return;
}
if(sql_ret.length <= 0) {
callback(false);
return;
}
callback(true);
});
}
function _update_phone_indentify_time(code, phone, opt, end_duration) {
var end_time = utils.timestamp() + end_duration;
var sql = "update phone_chat set code = \"%s\", end_time=%d, count=count+1 where phone = \"%s\" and opt_type = %d";
var sql_cmd = util.format(sql, code, end_time, phone, opt);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
})
}
function _insert_phone_indentify(code, phone, opt, end_duration) {
var end_time = utils.timestamp() + end_duration;
var sql = "insert into phone_chat(`code`, `phone`, `opt_type`, `end_time`, `count`)values(\"%s\", \"%s\", %d, %d, 1)";
var sql_cmd = util.format(sql, code, phone, opt, end_time);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
})
}
// callback(Respons.OK)
function update_phone_indentify(code, phone, opt, end_duration, callback) {
_is_phone_indentify_exist(phone, opt, function(b_exisit) {
if (b_exisit) {
// 更新时间和操作次数
_update_phone_indentify_time(code, phone, opt, end_duration);
// end
}
else { // 插入一条记录
_insert_phone_indentify(code, phone, opt, end_duration);
}
callback(Respones.OK);
});
}
function do_upgrade_guest_account(uid, phone, pwd, callback) {
var sql = "update uinfo set uname = \"%s\", upwd = \"%s\", is_guest = 0 where uid = %d";
var sql_cmd = util.format(sql, phone, pwd, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
}
else {
callback(Respones.OK);
}
})
}
function reset_account_pwd(phone, pwd, callback) {
var sql = "update uinfo set upwd = \"%s\" where uname = \"%s\"";
var sql_cmd = util.format(sql, pwd, phone);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
}
else {
callback(Respones.OK);
}
})
}
module.exports = {
connect: connect_to_center,
get_guest_uinfo_by_ukey: get_guest_uinfo_by_ukey,
get_uinfo_by_uname_upwd: get_uinfo_by_uname_upwd,
insert_guest_user: insert_guest_user,
edit_profile: edit_profile,
is_exist_guest: is_exist_guest,
update_phone_indentify: update_phone_indentify,
check_phone_unuse: check_phone_unuse,
do_upgrade_guest_account: do_upgrade_guest_account,
check_phone_code_valid: check_phone_code_valid,
insert_phone_account_user: insert_phone_account_user,
check_phone_is_reged: check_phone_is_reged,
reset_account_pwd: reset_account_pwd,
};
用户中心redis模块
内存数据库,用于存储需要频繁读取的数据库数据,提高查阅效率
var redis = require("redis");
var util = require('util')
var Respones = require("../apps/Respones.js");
var log = require("../utils/log.js");
var utils = require("../utils/utils.js");
var center_redis = null;
function connect_to_center(host, port, db_index) {
center_redis = redis.createClient({
host: host,
port: port,
db: db_index,
});
center_redis.on("error", function(err) {
log.error(err);
});
}
/* key, --> value
bycw_center_user_uid_8791
uinfo : {
unick: string,
uface: 图像ID,
usex: 性别,
uvip: VIP等级
is_guest: 是否为游客
}
*/
function set_uinfo_inredis(uid, uinfo) {
if (center_redis === null) {
return;
}
var key = "bycw_center_user_uid_" + uid;
uinfo.uface = uinfo.uface.toString();
uinfo.usex = uinfo.usex.toString();
uinfo.uvip = uinfo.uvip.toString();
uinfo.is_guest = uinfo.is_guest.toString();
log.info("redis center hmset " + key);
center_redis.hmset(key, uinfo, function(err) {
if(err) {
log.error(err);
}
});
}
// callback(status, body)
function get_uinfo_inredis(uid, callback) {
if (center_redis === null) {
callback(Respones.SYSTEM_ERR, null);
return;
}
var key = "bycw_center_user_uid_" + uid;
log.info("hgetall ", key);
center_redis.hgetall(key, function(err, data) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
var uinfo = data;
uinfo.uface = parseInt(uinfo.uface);
uinfo.usex = parseInt(uinfo.usex);
uinfo.uvip = parseInt(uinfo.uvip);
uinfo.is_guest = parseInt(uinfo.is_guest);
callback(Respones.OK, uinfo);
});
}
module.exports = {
connect: connect_to_center,
set_uinfo_inredis: set_uinfo_inredis,
get_uinfo_inredis: get_uinfo_inredis,
};
代码框架
启动服务器:注册对应服务
require("../../init.js");
var game_config = require("../game_config.js");
var proto_man = require("../../netbus/proto_man.js");
var netbus = require("../../netbus/netbus.js");
var service_manager = require("../../netbus/service_manager.js");
var Stype = require("../Stype.js");
var auth_service = require("./auth_service.js");
var center = game_config.center_server;
netbus.start_tcp_server(center.host, center.port, false);
service_manager.register_service(Stype.Auth, auth_service);
// 连接中心数据库
var center_mysql_config = game_config.center_database;
var mysql_center = require("../../database/mysql_center.js");
mysql_center.connect(center_mysql_config.host, center_mysql_config.port,
center_mysql_config.db_name, center_mysql_config.uname, center_mysql_config.upwd);
// end
// 连接中心服务器的redis
var center_redis_config = game_config.center_redis;
var redis_center = require("../../database/redis_center.js");
redis_center.connect(center_redis_config.host, center_redis_config.port, center_redis_config.db_index);
// end
用户服务本体
auth_service:
var log = require("../../utils/log.js");
var Cmd = require("../Cmd.js");
var auth_model = require("./auth_model.js");
var Respones = require("../Respones.js");
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var utils = require("../../utils/utils.js");
require("./auth_proto.js");
//游客登录
function guest_login(session, utag, proto_type, body) {
// 验证数据合法性
if(!body) {
session.send_cmd(Stype.Auth, Cmd.Auth.GUEST_LOGIN, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
// end
var ukey = body;
auth_model.guest_login(ukey, function(ret) {
session.send_cmd(Stype.Auth, Cmd.Auth.GUEST_LOGIN, ret, utag, proto_type);
});
}
function uname_login(session, utag, proto_type, body) {
// 验证数据合法性
if(!body || !body[0] || !body[1]) {
session.send_cmd(Stype.Auth, Cmd.Auth.GUEST_LOGIN, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
// end
var uname = body[0];
var upwd = body[1];
auth_model.uname_login(uname, upwd, function(ret) {
session.send_cmd(Stype.Auth, Cmd.Auth.UNAME_LOGIN, ret, utag, proto_type);
});
}
function edit_profile(session, uid, proto_type, body) {
// 验证数据合法性
if (!body || !body.unick || (body.usex != 0 && body.usex != 1)) {
session.send_cmd(Stype.Auth, Cmd.Auth.EDIT_PROFILE, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
// end
auth_model.edit_profile(uid, body.unick, body.usex, function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.EDIT_PROFILE, body, uid, proto_type);
});
}
function is_phone_number(num) {
if (num.length != 11) {
return false;
}
for(var i = 0; i < num.length; i ++) {
var ch = num.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
return true;
}
function get_guest_upgrade_indentify(session, uid, proto_type, body) {
if(!body || typeof(body[0]) == "undefined" ||
typeof(body[1]) == "undefined" || !is_phone_number(body[1]) ||
typeof(body[2]) == "undefined") {
session.send_cmd(Stype.Auth, Cmd.Auth.GUEST_UPGRADE_INDENTIFY, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
auth_model.get_upgrade_indentify(uid, body[2], body[1], body[0], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.GUEST_UPGRADE_INDENTIFY, body, uid, proto_type);
});
}
function get_phone_reg_verify_code(session, utag, proto_type, body) {
if(!body || typeof(body[0]) == "undefined" ||
typeof(body[1]) == "undefined") {
session.send_cmd(Stype.Auth, Cmd.Auth.GET_PHONE_REG_VERIFY, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
auth_model.get_phone_reg_verify_code(body[1], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.GET_PHONE_REG_VERIFY, body, utag, proto_type);
});
}
function get_forget_pwd_verify_code(session, utag, proto_type, body) {
if(!body || typeof(body[0]) == "undefined" ||
typeof(body[1]) == "undefined") {
session.send_cmd(Stype.Auth, Cmd.Auth.GET_FORGET_PWD_VERIFY, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
auth_model.get_forget_pwd_verify_code(body[1], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.GET_FORGET_PWD_VERIFY, body, utag, proto_type);
});
}
function guest_bind_phone_num(session, uid, proto_type, body) {
if(!body || !body[0] || !body[1] || !body[2]) {
session.send_cmd(Stype.Auth, Cmd.Auth.BIND_PHONE_NUM, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
auth_model.guest_bind_phone_number(uid, body[0], body[1], body[2], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.BIND_PHONE_NUM, body, uid, proto_type);
})
}
function reg_phone_account(session, utag, proto_type, body) {
if(!body || !body[0] || !body[1] || !body[2] || !body[3]) {
session.send_cmd(Stype.Auth, Cmd.Auth.PHONE_REG_ACCOUNT, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
auth_model.reg_phone_account(body[0], body[1], body[2], body[3], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.PHONE_REG_ACCOUNT, body, utag, proto_type);
})
}
function reset_user_pwd(session, utag, proto_type, body) {
if(!body || !body[0] || !body[1] || !body[2]) {
session.send_cmd(Stype.Auth, Cmd.Auth.RESET_USER_PWD, Respones.INVALID_PARAMS, utag, proto_type);
return;
}
auth_model.reset_user_pwd(body[0], body[1], body[2], function(body) {
session.send_cmd(Stype.Auth, Cmd.Auth.RESET_USER_PWD, body, utag, proto_type);
})
}
var service = {
name: "auth_service", // 服务名称
is_transfer: false, // 是否为转发模块,
// 收到客户端给我们发来的数据
on_recv_player_cmd: function(session, stype, ctype, body, utag, proto_type, raw_cmd) {
log.info(stype, ctype, body);
switch(ctype) {
// 游客登录
case Cmd.Auth.GUEST_LOGIN:
guest_login(session, utag, proto_type, body);
break;
// 修改资料
case Cmd.Auth.EDIT_PROFILE:
edit_profile(session, utag, proto_type, body);
break;
// 升级账号
case Cmd.Auth.GUEST_UPGRADE_INDENTIFY:
get_guest_upgrade_indentify(session, utag, proto_type, body);
break;
// 接收游客账号绑定手机号验证码
case Cmd.Auth.BIND_PHONE_NUM:
guest_bind_phone_num(session, utag, proto_type, body);
break;
// 正式账号登录
case Cmd.Auth.UNAME_LOGIN:
uname_login(session, utag, proto_type, body);
break;
// 接收手机号注册验证码
case Cmd.Auth.GET_PHONE_REG_VERIFY:
get_phone_reg_verify_code(session, utag, proto_type, body);
break;
// 接收重置密码验证码
case Cmd.Auth.GET_FORGET_PWD_VERIFY:
get_forget_pwd_verify_code(session, utag, proto_type, body);
break;
// 手机号登录
case Cmd.Auth.PHONE_REG_ACCOUNT:
reg_phone_account(session, utag, proto_type, body);
break;
// 重置密码
case Cmd.Auth.RESET_USER_PWD:
reset_user_pwd(session, utag, proto_type, body);
break;
}
},
// 收到我们连接的服务给我们发过来的数据;
on_recv_server_return: function (session, stype, ctype, body, utag, proto_type, raw_cmd) {
},
// 收到客户端断开连接;
on_player_disconnect: function(stype, session) {
},
};
module.exports = service;
调用数据库的接口,完成对应业务需求auth_model:
var Respones = require("../Respones.js");
var mysql_center = require("../../database/mysql_center.js");
var redis_center = require("../../database/redis_center.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var phone_msg = require("../phone_msg.js");
function guest_login_success(guest_key, data, ret_func) {
var ret = {};
// 登陆成功了
ret.status = Respones.OK;
ret.uid = data.uid;
ret.unick = data.unick;
ret.usex = data.usex;
ret.uface = data.uface;
ret.uvip = data.uvip;
ret.ukey = guest_key;
redis_center.set_uinfo_inredis({
unick: data.unick,
uface: data.uface,
usex: data.usex,
uvip: data.uvip,
is_guest: 1,
});
ret_func(ret);
}
function uname_login_success(data, ret_func) {
var ret = {};
// 登陆成功了
ret.status = Respones.OK;
ret.uid = data.uid;
ret.unick = data.unick;
ret.usex = data.usex;
ret.uface = data.uface;
ret.uvip = data.uvip;
redis_center.set_uinfo_inredis(data.uid, {
unick: data.unick,
uface: data.uface,
usex: data.usex,
uvip: data.uvip,
is_guest: 0,
});
ret_func(ret);
}
function write_err(status, ret_func) {
var ret = {};
ret.status = status;
ret_func(ret);
}
function guest_login(ukey, ret_func) {
var unick = "游客" + utils.random_int_str(4); // 游客9527
var usex = utils.random_int(0, 1); // 性别
var uface = 0; // 系统只有一个默认的uface,要么就是自定义face;
// 查询数据库有无用户, 数据库
mysql_center.get_guest_uinfo_by_ukey(ukey, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的key, 注册一个
mysql_center.insert_guest_user(unick, uface, usex, ukey, function(status) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
guest_login(ukey, ret_func);
});
}
else {
var sql_uinfo = data[0];
if (sql_uinfo.status != 0) { // 游客账号被封
write_err(Respones.ILLEGAL_ACCOUNT, ret_func);
return;
}
if (!sql_uinfo.is_guest) { // 不是游客不能用游客登陆;
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
guest_login_success(ukey, sql_uinfo, ret_func);
}
});
// end
}
function _do_reg_phone_account(phone, pwd_md5, unick, ret_func) {
var usex = utils.random_int(0, 1); // 性别
var uface = 0; // 系统只有一个默认的uface,要么就是自定义face;
mysql_center.insert_phone_account_user(unick, uface, usex, phone, pwd_md5, function(status) {
ret_func(status);
});
}
function _do_account_reset_pwd(phone, pwd_md5, ret_func) {
mysql_center.reset_account_pwd(phone, pwd_md5, function(status) {
ret_func(status);
});
}
function uname_login(uname, upwd, ret_func) {
// 查询数据库有无用户, 数据库
mysql_center.get_uinfo_by_uname_upwd(uname, upwd, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的uname, upwd
write_err(Respones.UNAME_OR_UPWD_ERR, ret_func);
}
else {
var sql_uinfo = data[0];
if (sql_uinfo.status != 0) { // 账号被封
write_err(Respones.ILLEGAL_ACCOUNT, ret_func);
return;
}
uname_login_success(sql_uinfo, ret_func);
}
});
// end
}
function edit_profile(uid, unick, usex, ret_func) {
mysql_center.edit_profile(uid, unick, usex, function(status) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
var body = {
status: status,
unick: unick,
usex: usex,
};
ret_func(body);
});
}
// end_duration 单位是秒
function _send_indentify_code(phone_num, opt, end_duration, ret_func) {
var code = utils.random_int_str(6);
// 把code 插入到数据库
mysql_center.update_phone_indentify(code, phone_num, opt, end_duration, function(status) {
if(status == Respones.OK) {
// 发送短信
phone_msg.send_indentify_code(phone_num, code);
// end
}
ret_func(status);
});
// end
}
function get_upgrade_indentify(uid, ukey, phone, opt, ret_func) {
// 判断账号的合法性
mysql_center.is_exist_guest(uid, function(status) {
if(status != Respones.OK) {
ret_func(Respones.INVALIDI_OPT);
return;
}
_send_indentify_code(phone, opt, 60, ret_func);
})
// end
}
function get_phone_reg_verify_code(phone, ret_func) {
mysql_center.check_phone_unuse(phone, function(status) {
if(status != Respones.OK) {
ret_func(Respones.PHONE_IS_REG);
return;
}
_send_indentify_code(phone, 1, 60, ret_func);
})
}
function get_forget_pwd_verify_code(phone, ret_func) {
mysql_center.check_phone_is_reged(phone, function(status) {
if(status != Respones.OK) {
ret_func(Respones.PHONE_IS_NOT_REG);
return;
}
_send_indentify_code(phone, 2, 60, ret_func);
})
}
function _do_bind_guest_account(uid, phone_num, pwd_md5, phone_code, ret_func) {
mysql_center.do_upgrade_guest_account(uid, phone_num, pwd_md5, function(status) {
ret_func(status);
})
}
function _check_guest_upgrade_phone_code_valid(uid, phone_num, pwd_md5, phone_code, ret_func) {
mysql_center.check_phone_code_valid(phone_num, phone_code, 0, function(status) {
if (status != Respones.OK) {
ret_func(Respones.PHONE_CODE_ERR);
return;
}
// 账号升级, 更新数据库,返回结果
_do_bind_guest_account(uid, phone_num, pwd_md5, phone_code, ret_func);
// end
})
}
function _check_reg_phone_account_verify_code(phone, pwd_md5, verify_code, unick, ret_func) {
mysql_center.check_phone_code_valid(phone, verify_code, 1, function(status) {
if (status != Respones.OK) {
ret_func(Respones.PHONE_CODE_ERR);
return;
}
// 新建一个手机注册的账号
_do_reg_phone_account(phone, pwd_md5, unick, ret_func);
// end
})
}
function _check_reset_pwd_verify_code(phone, pwd_md5, verify_code, ret_func) {
mysql_center.check_phone_code_valid(phone, verify_code, 2, function(status) {
if (status != Respones.OK) {
ret_func(Respones.PHONE_CODE_ERR);
return;
}
// 新建一个手机注册的账号
_do_account_reset_pwd(phone, pwd_md5, ret_func);
// end
})
}
function _check_phone_is_binded(uid, phone_num, pwd_md5, phone_code, ret_func) {
mysql_center.check_phone_unuse(phone_num, function(status) {
if(status != Respones.OK) {
ret_func(Respones.PHONE_IS_REG);
return;
}
// 手机绑定, 检查验证码
_check_guest_upgrade_phone_code_valid(uid, phone_num, pwd_md5, phone_code, ret_func);
// end
})
}
// uid 用户ID, phone_num手机号 pwd_md5密码, ukey 是游客的ukey, 验证码
function guest_bind_phone_number(uid, phone_num, pwd_md5, phone_code, ret_func) {
// 判断账号的合法性
mysql_center.is_exist_guest(uid, function(status) {
if(status != Respones.OK) {
ret_func(Respones.INVALIDI_OPT);
return;
}
_check_phone_is_binded(uid, phone_num, pwd_md5, phone_code, ret_func);
})
// end
}
function reg_phone_account(phone, pwd_md5, verify_code, unick, ret_func) {
mysql_center.check_phone_unuse(phone, function(status) {
if(status != Respones.OK) {
ret_func(Respones.PHONE_IS_REG);
return;
}
// 检查验证码
_check_reg_phone_account_verify_code(phone, pwd_md5, verify_code, unick, ret_func);
// end
})
}
function reset_user_pwd(phone, pwd_md5, verify_code, ret_func) {
mysql_center.check_phone_is_reged(phone, function(status) {
if(status != Respones.OK) {
ret_func(Respones.PHONE_IS_NOT_REG);
return;
}
// 检查验证码
_check_reset_pwd_verify_code(phone, pwd_md5, verify_code, ret_func);
// end
})
}
module.exports = {
guest_login: guest_login,
uname_login: uname_login,
edit_profile: edit_profile,
get_upgrade_indentify: get_upgrade_indentify, // 游客升级,拉去手机验证码
guest_bind_phone_number: guest_bind_phone_number, // 游客绑定手机号码;
get_phone_reg_verify_code: get_phone_reg_verify_code,
reg_phone_account: reg_phone_account, // 手机注册,
get_forget_pwd_verify_code: get_forget_pwd_verify_code, // 忘记密码的手机验证码
reset_user_pwd: reset_user_pwd, // 重置密码
};
绑定手机号——短信验证平台
需求:给用户发送验证码短信,用于登录、注册、修改密码等行为。
验证码管理
1: 数据库中用一个表来保存验证码的与对应的手机号,并记录下这个手机号码申请验证码的次数;
2: 验证码保存的表结构: id, code, phone_num, opt_type, end_time, count;
id 每个手机号对应一个id, phone_num 电话号码, opt_type操作类型每一个电话号码一个操作类型,注册为0, 修改密码为1,不同的操作类型是不同的定义, end_time过期的时间, count:这个电话号码的这个操作做了多少次,方便做刷短信管理;
3: 拉黑电话号码的表; id, number, status, 在获取短线前线做验证,这个电话是否被拉黑,拉黑可以由程序来做条件,比如短信的操作次数,频率等,更具实际的工具来做决策,目前这里占不考虑;
获取验证码的协议
请求:服务号, 命令号, {
0: 操作类型, 0为账号升级拉取,1为修改密码拉取, 2为手机号注册拉取;
1: 电话号码,
2: ukey,
}
返回: 服务号,命令号 status;
账号升级(正式账户注册)协议
- 请求:服务号, 命令号, body { 手机号, 密码md5, 手机验证码};
- 服务器判断:
(1) 参数合法性;
(2) uid是否有效,并且是游客账号;
(3)检查手机号码是否已经绑定;
(4)检查验证码是否已经过期;
(5)绑定账号;
(6)返回: 服务号 命令号 状态码;
(7) 保存 用户名和密码,在本地;
(8) 下一次就使用本地的 账号和密码登录,不再用游客登陆;
正式账户注册
手机注册命令:
服务号 命令号
body {
0: “手机号码”,
1: “密码的md5值”,
2: 手机验证码,
3: unick,
}
返回:
服务号, 命令号, status;
成功后,将账号密码保存到本地,自动登陆上去,并记录账号密码信息;
正式账户登录
1: 游客升级成功后,将正式账号保存在本地,这样下次就可以直接登陆,而不用再输入用户名和密码;
2: 正式账号的登陆协议:
请求: 服务号, 命令号, body {0: 手机号码, 1: 密码md5}
返回: 服务号,命令号,
{
status: 状态码, OK,如果错误就没有后面的数据;
uid
unick
usex
uface
uvip
}
修改密码
1: 重置密码:
服务号 命令号
body {
0: “手机号码”,
1: “新密码的md5值”,
2: 手机验证码,
}
返回:
服务号, 命令号, status;
添加二进制协议支持
如果使用二进制协议,需要编写如下的编码和解码函数:
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var utils = require("../../utils/utils.js");
var Respones = require("../Respones.js");
var proto_man = require("../../netbus/proto_man.js");
var proto_tools = require("../../netbus/proto_tools.js");
var log = require("../../utils/log.js");
/*
游客登陆:
服务号
命令号
ukey --> string;
返回:
服务号
命令号
{
status: 状态, OK,错误,就没有后面的色数据
uid
unick
usex
uface
uvip
ukey
}
*/
function encode_guest_login(stype, ctype, body) {
if(body.status != Respones.OK) {
return proto_tools.encode_status_cmd(stype, ctype, body.status);
}
var unick_len = body.unick.utf8_byte_len();
var ukey_len = body.ukey.utf8_byte_len();
var total_len = proto_tools.header_size + 2 + 4 + (2 + unick_len) + 2 + 2 + 2 + (2 + ukey_len);
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
proto_tools.write_int16(cmd_buf, offset, body.status);
offset += 2;
proto_tools.write_uint32(cmd_buf, offset, body.uid);
offset += 4;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.unick, unick_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uface);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uvip);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.ukey, ukey_len);
return cmd_buf;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.GUEST_LOGIN, proto_tools.decode_str_cmd);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.GUEST_LOGIN, encode_guest_login);
/*
重复登陆:
服务号
命令号
body: null,
*/
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.RELOGIN, proto_tools.encode_empty_cmd);
/*
修改用户资料:
服务号
命令号
body {
unick:
usex:
}
返回:
服务号
命令号
body {
status: OK or 失败
unick
usex
}
*/
function decode_edit_profile(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var ret = proto_tools.read_str_inbuf(cmd_buf, proto_tools.header_size);
body.unick = ret[0];
var offset = ret[1];
body.usex = proto_tools.read_int16(cmd_buf, offset);
cmd[2] = body;
return cmd;
}
function encode_edit_profile(stype, ctype, body) {
if(body.status != Respones.OK) {
return proto_tools.encode_status_cmd(stype, ctype, body.status);
}
var unick_len = body.unick.utf8_byte_len();
var total_len = proto_tools.header_size + 2 + (2 + unick_len) + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
proto_tools.write_int16(cmd_buf, offset, body.status);
offset += 2;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.unick, unick_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
offset += 2;
return cmd_buf;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.EDIT_PROFILE, decode_edit_profile);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.EDIT_PROFILE, encode_edit_profile);
/*
拉取用户账号升级的验证码:
服务号
命令号
body {
0: opt_code, 操作码0, 游客升级, 1手机注册, 2忘记密码
1: phone number, 电话号码
2: guest_key, 游客的key
}
返回:
服务号, 命令号, status 状态码
*/
function decode_upgrade_verify_code(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
body[0] = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[2] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.GUEST_UPGRADE_INDENTIFY, decode_upgrade_verify_code);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.GUEST_UPGRADE_INDENTIFY, proto_tools.encode_status_cmd);
/*
绑定游客账号的协议:
服务号
命令号
body {
0: phone_num,
1: pwd_md5,
2: phone code, 手机验证码
}
返回: status
*/
function decode_bind_phone(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[0] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[2] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.BIND_PHONE_NUM, decode_bind_phone);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.BIND_PHONE_NUM, proto_tools.encode_status_cmd);
/*
账号密码登录:
服务号
命令号
body {
0: uname,
1: upwd,
}
返回:
服务号
命令号
body
{
status: = Respones.OK;
uid: = data.uid;
unick: = data.unick;
usex: = data.usex;
uface: = data.uface;
uvip: = data.uvip;
}
*/
function decode_uname_login(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[0] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
function encode_uname_login(stype, ctype, body) {
if(body.status != Respones.OK) {
return proto_tools.encode_status_cmd(stype, ctype, body.status);
}
var unick_len = body.unick.utf8_byte_len();
var total_len = proto_tools.header_size + 2 + 4 + (2 + unick_len) + 2 + 2 + 2;
var cmd_buf = proto_tools.alloc_buffer(total_len);
var offset = proto_tools.write_cmd_header_inbuf(cmd_buf, stype, ctype);
proto_tools.write_int16(cmd_buf, offset, body.status);
offset += 2;
proto_tools.write_uint32(cmd_buf, offset, body.uid);
offset += 4;
offset = proto_tools.write_str_inbuf(cmd_buf, offset, body.unick, unick_len);
proto_tools.write_int16(cmd_buf, offset, body.usex);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uface);
offset += 2;
proto_tools.write_int16(cmd_buf, offset, body.uvip);
offset += 2;
return cmd_buf;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.UNAME_LOGIN, decode_uname_login);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.UNAME_LOGIN, encode_uname_login);
/*
获取手机注册的验证码:
服务号
命令号
body {
0: opt_code, 操作码0, 游客升级, 1手机注册, 2忘记密码
1: phone number, 电话号码
}
返回:
服务号, 命令号, status 状态码
*/
function decode_phone_reg_verify_code(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
body[0] = proto_tools.read_int16(cmd_buf, offset);
offset += 2;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.GET_PHONE_REG_VERIFY, decode_phone_reg_verify_code);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.GET_PHONE_REG_VERIFY, proto_tools.encode_status_cmd);
/*
手机注册账号
服务号
命令号
body {
0: phone,
1: pwd,
2: verify_code,
3: unick,
}
返回:
服务号
命令号
status
*/
function decode_phone_reg_account(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[0] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[2] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[3] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.PHONE_REG_ACCOUNT, decode_phone_reg_account);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.PHONE_REG_ACCOUNT, proto_tools.encode_status_cmd);
/*
修改用户密码:
服务号
命令号
body {
0: opt_code, 操作码0, 游客升级, 1手机注册, 2忘记密码
1: phone number, 电话号码
}
返回:
服务号, 命令号, status 状态码
*/
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.GET_FORGET_PWD_VERIFY, decode_phone_reg_verify_code);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.GET_FORGET_PWD_VERIFY, proto_tools.encode_status_cmd);
/*
找回密码
服务号
命令号
body {
0: phone,
1: pwd,
2: verify_code,
}
返回:
服务号
命令号
status
*/
function decode_reset_upwd(cmd_buf) {
var cmd = {};
cmd[0] = proto_tools.read_int16(cmd_buf, 0);
cmd[1] = proto_tools.read_int16(cmd_buf, 2);
var body = {};
var offset = proto_tools.header_size;
var ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[0] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[1] = ret[0];
offset = ret[1];
ret = proto_tools.read_str_inbuf(cmd_buf, offset);
body[2] = ret[0];
offset = ret[1];
cmd[2] = body;
return cmd;
}
proto_man.reg_decoder(Stype.Auth, Cmd.Auth.RESET_USER_PWD, decode_reset_upwd);
proto_man.reg_encoder(Stype.Auth, Cmd.Auth.RESET_USER_PWD, proto_tools.encode_status_cmd);
游戏系统服务器
1: 每一个游戏有特定一组游戏服务器;
2: 每一组游戏服务器分为两类:
(1)只和系统交互的部分(排行,任务,奖励等),容易扩展;
(2)所有玩家一起游戏的部分;
3: 登陆成功后,再发送命令登陆到游戏服务器
拉取玩家的游戏信息存放到game redis服务器里面。
每次玩家获取游戏信息都是与系统服务器交互。
4: 每次开始有的时候,进入到游戏服务器,拉取游戏数据,开始多人游戏;
5: 每一个游戏一个游戏数据库;
游戏数据库的设计:
id, uid, uchip,uchip2, uchip3,uexp, ustatus, uvip, uvip_endtime, udata1(用户游戏信息), udata2, udata3, udata4;
如果是复杂种类放多的比如背包等,可以独立数据表,这里只放重要的其他玩家都可以看见的信息;
游戏服务器登录
登录用户服务器后,用户服务器会自动向系统服务器发送登录请求进行登录。
每日登录奖励
1: 每次登陆的时候,继续下登陆的时间,根据时间来判断是否给奖励
2: 数据表的字段设计如下: login_bonueses
字段 id uid, bonues, status,bunues_time, days
3: id: 作为数据记录的唯一的主key;
uid: 每个用户登录的信息,每个用户有且只有一条记录;
4: bonues: 今天用户登录可获取的奖励数目,过后将作废不累积;
5: status: 今天的奖励是否领取0,为可以领取, 1为已经领取;
6: bunues_time: 上一次登陆的时间;
7: days: 连续登录的天数,如果大于最大的天数,就按照最大的天数来算,也可以从新计算,根据需求来做
发放奖励条件:
1: 判断是否发放登陆奖励: 上一次发放奖励的时间是否小于今天 00:00:000的时间戳;
2: 判断是否是连续登录, 上一次发放奖励的时间是否大于昨天00:00:00的时间戳;
3: 每日登录奖励配置: 连续登录5天后,重新开始计算登陆的天数,或都奖励最多连续天数的奖励,更具需求而定;
4: 加入登陆奖励的配置文件;
请求:
服务号
命令号
null
返回:
服务号
命令号
{
0: status,
1: 是否有奖励 , 0表示今天你没有奖励,或者已经领取了,1表示有奖励
2: id 领取的ID,
3: bonues
4: days:
}
世界排行榜制作
原理:通过redis的有序集合来实现自动排序的功能
请求:
服务号:
命令号:
null
返回
服务号
命令号
body: {
0: status, OK, 错误
num: 排行榜记录的数目
[
[unick, usex, uface, uchip],
[..],
[..],
…..
]
}
代码
入口(开启监听,注册服务)
require("../../init.js");
var game_config = require("../game_config.js");
var proto_man = require("../../netbus/proto_man.js");
var netbus = require("../../netbus/netbus.js");
var service_manager = require("../../netbus/service_manager.js");
var Stype = require("../Stype.js");
var game_system_service = require("./game_system_service.js");
var game_system = game_config.game_system_server;
netbus.start_tcp_server(game_system.host, game_system.port, false);
service_manager.register_service(Stype.GameSystem, game_system_service);
// 连接中心redis
var center_redis_config = game_config.center_redis;
var redis_center = require("../../database/redis_center.js");
redis_center.connect(center_redis_config.host, center_redis_config.port, center_redis_config.db_index);
// end
// 连接游戏redis
var game_redis_config = game_config.game_redis;
var redis_game = require("../../database/redis_game.js");
redis_game.connect(game_redis_config.host, game_redis_config.port, game_redis_config.db_index);
// end
// 连接游戏数据库
var game_mysql_config = game_config.game_database;
var mysql_game = require("../../database/mysql_game.js");
mysql_game.connect(game_mysql_config.host, game_mysql_config.port,
game_mysql_config.db_name, game_mysql_config.uname, game_mysql_config.upwd);
// end
服务模块(system_service)
var log = require("../../utils/log.js");
var Cmd = require("../Cmd.js");
var Respones = require("../Respones.js");
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var utils = require("../../utils/utils.js");
require("./game_system_proto.js");
var system_model = require("./game_system_model.js");
function get_game_info(session, uid, proto_type, body) {
system_model.get_game_info(uid, function(body) {
session.send_cmd(Stype.GameSystem, Cmd.GameSystem.GET_GAME_INFO, body, uid, proto_type);
})
}
function get_login_bonues_info(session, uid, proto_type, body) {
system_model.get_login_bonues_info(uid, function(body) {
session.send_cmd(Stype.GameSystem, Cmd.GameSystem.LOGIN_BONUES_INFO, body, uid, proto_type);
})
}
function recv_login_bonues(session, uid, proto_type, body) {
if (!body) {
session.send_cmd(Stype.GameSystem, Cmd.GameSystem.RECV_LOGIN_BUNUES, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
var bonuesid = body;
system_model.recv_login_bonues(uid, bonuesid, function(body) {
session.send_cmd(Stype.GameSystem, Cmd.GameSystem.RECV_LOGIN_BUNUES, body, uid, proto_type);
})
}
function get_world_rank_info(session, uid, proto_type, body) {
system_model.get_world_rank_info(uid, function(body) {
session.send_cmd(Stype.GameSystem, Cmd.GameSystem.GET_WORLD_RANK_INFO, body, uid, proto_type);
})
}
var service = {
name: "game_system_service", // 服务名称
is_transfer: false, // 是否为转发模块,
// 收到客户端给我们发来的数据
on_recv_player_cmd: function(session, stype, ctype, body, utag, proto_type, raw_cmd) {
log.info(stype, ctype, body);
switch(ctype) {
case Cmd.GameSystem.GET_GAME_INFO:
get_game_info(session, utag, proto_type, body);
break;
case Cmd.GameSystem.LOGIN_BONUES_INFO:
get_login_bonues_info(session, utag, proto_type, body);
break;
case Cmd.GameSystem.RECV_LOGIN_BUNUES:
recv_login_bonues(session, utag, proto_type, body);
break;
case Cmd.GameSystem.GET_WORLD_RANK_INFO:
get_world_rank_info(session, utag, proto_type, body);
break;
}
},
// 收到我们连接的服务给我们发过来的数据;
on_recv_server_return: function (session, stype, ctype, body, utag, proto_type, raw_cmd) {
},
// 收到客户端断开连接;
on_player_disconnect: function(stype, session) {
},
};
module.exports = service;
逻辑模块(system_model)
var Respones = require("../Respones.js");
var redis_center = require("../../database/redis_center.js");
var redis_game = require("../../database/redis_game.js");
var mysql_game = require("../../database/mysql_game.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var game_config = require("../game_config.js");
var login_bonues_config = game_config.game_data.login_bonues_config;
function write_err(status, ret_func) {
var ret = {};
ret[0] = status;
ret_func(ret);
}
function check_login_bonues(uid) {
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
return;
}
if (data.length <= 0) { // 没有这样的uid, 插入一个,发放奖励
var bonues = login_bonues_config.bonues[0];
mysql_game.insert_user_login_bonues(uid, bonues, function(status) {
return;
});
}
else {
var sql_login_bonues = data[0];
// days, bunues_time
var has_bonues = sql_login_bonues.bunues_time < utils.timestamp_today();
if (has_bonues) { // 更新本次登陆奖励
// 连续登录了多少天;
var days = 1;
var is_straight = (sql_login_bonues.bunues_time >= utils.timestamp_yesterday());
if (is_straight) {
days = sql_login_bonues.days + 1;
}
var index = days - 1;
if (days > login_bonues_config.bonues.length) { //
if (login_bonues_config.clear_login_straight) {
days = 1;
index = 0;
}
else {
index = login_bonues_config.bonues.length - 1;
}
}
// 发放今天的奖励
mysql_game.update_user_login_bunues(uid, login_bonues_config.bonues[index], days, function(status) {});
// end
}
}
});
}
function get_ugame_info_success(uid, data, ret_func) {
var ret = {};
// 登陆成功了
ret[0] = Respones.OK;
ret[1] = data.uchip;
ret[2] = data.uexp;
ret[3] = data.uvip;
redis_game.set_ugame_info_inredis(uid, {
uchip: data.uchip,
uexp: data.uexp,
uvip: data.uvip,
});
// "NODE_GAME_WOLRD_RANK"
redis_game.update_game_world_rank("NODE_GAME_WORLD_RANK", uid, data.uchip);
// 检查是否要发放登陆奖励
check_login_bonues(uid);
ret_func(ret);
}
function get_rank_info_success(my_rank, rank_array, ret_func) {
var ret = {};
ret[0] = Respones.OK;
ret[1] = rank_array.length;
ret[2] = rank_array;
ret[3] = my_rank;
ret_func(ret);
}
function get_login_bonues_info_success(uid, b_has, data, ret_func) {
var ret = {};
// 登陆成功了
ret[0] = Respones.OK;
ret[1] = b_has; // 0表示没有奖励,1表示有奖励
if (b_has !== 1) {
ret_func(ret);
return;
}
ret[2] = data.id;
ret[3] = data.bonues;
ret[4] = data.days;
ret_func(ret);
}
function recv_login_bonues_success(uid, bonuesid, bonues, ret_func) {
// 更新数据库,讲奖励标记为已经领取
mysql_game.update_login_bonues_recved(bonuesid);
// end
// 更新玩家的数据库的金币的,
mysql_game.add_ugame_uchip(uid, bonues, true);
// end
// 更新game redis
redis_game.get_ugame_info_inredis(uid, function(status, ugame_info) {
if (status != Respones.OK) {
log.error("redis game get ugame info failed!!!", status);
return;
}
// 成功获取
ugame_info.uchip += bonues;
redis_game.set_ugame_info_inredis(uid, ugame_info);
// end
});
// end
var ret = {
0: Respones.OK,
1: bonues,
};
ret_func(ret);
}
function get_login_bonues_info(uid, ret_func) {
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的uid, 注册一个
get_login_bonues_info_success(uid, 0, null, ret_func);
}
else {
var sql_bonues_info = data[0];
if (sql_bonues_info.status != 0) { // 今天的已经领取
get_login_bonues_info_success(uid, 0, null, ret_func);
return;
}
get_login_bonues_info_success(uid, 1, sql_bonues_info, ret_func);
}
});
}
function get_game_info(uid, ret_func) {
mysql_game.get_ugame_info_by_uid(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的uid, 注册一个
mysql_game.insert_ugame_user(uid, game_config.game_data.first_uexp,
game_config.game_data.first_uchip, function(status) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
get_game_info(uid, ret_func);
});
}
else {
var sql_ugame = data[0];
if (sql_ugame.status != 0) { // 账号被封
write_err(Respones.ILLEGAL_ACCOUNT, ret_func);
return;
}
get_ugame_info_success(uid, sql_ugame, ret_func);
}
});
}
// 领取登陆奖励
function recv_login_bonues(uid, bonuesid, ret_func) {
// 查询登陆奖励的合法性
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) {
write_err(Respones.INVALIDI_OPT, ret_func);
}
else {
var sql_bonues_info = data[0];
if (sql_bonues_info.status != 0 || sql_bonues_info.id != bonuesid) { // 今天的已经领取
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// 发放奖励
recv_login_bonues_success(uid, bonuesid, sql_bonues_info.bonues, ret_func);
}
});
}
function get_players_rank_info(my_uid, data, ret_func) {
var rank_array = []; // [[unick, usex, uface, uchip], [], [],]
var total_len = Math.floor(data.length / 2); // uid, chip, uid, chip
var is_sended = false;
var loaded = 0;
var my_rank = -1;
for(var i = 0; i < total_len; i ++) {
rank_array.push([]); // 放[unick, usex, uface, uchip]
}
// 获取每一个在榜的玩家的信息[unick, usex, uface, uchip]
// rendis_center去获取
var call_func = function(uid, uchip, out_array) {
redis_center.get_uinfo_inredis(uid, function(status, data) {
if (status != Respones.OK) {
if (!is_sended) {
write_err(status, ret_func);
is_sended = true;
}
return;
}
//
out_array.push(data.unick);
out_array.push(data.usex);
out_array.push(data.uface);
out_array.push(uchip);
loaded ++;
if (loaded >= total_len) {
get_rank_info_success(my_rank, rank_array, ret_func);
return;
}
// end
});
}
var j = 0;
for(var i = 0; i < data.length; i += 2, j ++) {
if (my_uid == data[i]) {
my_rank = (i + 1);
}
call_func(data[i], data[i + 1], rank_array[j]);
}
}
function get_world_rank_info(uid, ret_func) {
redis_game.get_world_rank_info("NODE_GAME_WORLD_RANK", 30, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
// uid, uchip,
get_players_rank_info(uid, data, ret_func);
// end
});
}
module.exports = {
get_game_info: get_game_info,
get_login_bonues_info: get_login_bonues_info,
recv_login_bonues: recv_login_bonues,
get_world_rank_info: get_world_rank_info,
};
二进制协议支持
var Respones = require("../Respones.js");
var redis_center = require("../../database/redis_center.js");
var redis_game = require("../../database/redis_game.js");
var mysql_game = require("../../database/mysql_game.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var game_config = require("../game_config.js");
var login_bonues_config = game_config.game_data.login_bonues_config;
function write_err(status, ret_func) {
var ret = {};
ret[0] = status;
ret_func(ret);
}
function check_login_bonues(uid) {
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
return;
}
if (data.length <= 0) { // 没有这样的uid, 插入一个,发放奖励
var bonues = login_bonues_config.bonues[0];
mysql_game.insert_user_login_bonues(uid, bonues, function(status) {
return;
});
}
else {
var sql_login_bonues = data[0];
// days, bunues_time
var has_bonues = sql_login_bonues.bunues_time < utils.timestamp_today();
if (has_bonues) { // 更新本次登陆奖励
// 连续登录了多少天;
var days = 1;
var is_straight = (sql_login_bonues.bunues_time >= utils.timestamp_yesterday());
if (is_straight) {
days = sql_login_bonues.days + 1;
}
var index = days - 1;
if (days > login_bonues_config.bonues.length) { //
if (login_bonues_config.clear_login_straight) {
days = 1;
index = 0;
}
else {
index = login_bonues_config.bonues.length - 1;
}
}
// 发放今天的奖励
mysql_game.update_user_login_bunues(uid, login_bonues_config.bonues[index], days, function(status) {});
// end
}
}
});
}
function get_ugame_info_success(uid, data, ret_func) {
var ret = {};
// 登陆成功了
ret[0] = Respones.OK;
ret[1] = data.uchip;
ret[2] = data.uexp;
ret[3] = data.uvip;
redis_game.set_ugame_info_inredis(uid, {
uchip: data.uchip,
uexp: data.uexp,
uvip: data.uvip,
});
// "NODE_GAME_WOLRD_RANK"
redis_game.update_game_world_rank("NODE_GAME_WORLD_RANK", uid, data.uchip);
// 检查是否要发放登陆奖励
check_login_bonues(uid);
ret_func(ret);
}
function get_rank_info_success(my_rank, rank_array, ret_func) {
var ret = {};
ret[0] = Respones.OK;
ret[1] = rank_array.length;
ret[2] = rank_array;
ret[3] = my_rank;
ret_func(ret);
}
function get_login_bonues_info_success(uid, b_has, data, ret_func) {
var ret = {};
// 登陆成功了
ret[0] = Respones.OK;
ret[1] = b_has; // 0表示没有奖励,1表示有奖励
if (b_has !== 1) {
ret_func(ret);
return;
}
ret[2] = data.id;
ret[3] = data.bonues;
ret[4] = data.days;
ret_func(ret);
}
function recv_login_bonues_success(uid, bonuesid, bonues, ret_func) {
// 更新数据库,讲奖励标记为已经领取
mysql_game.update_login_bonues_recved(bonuesid);
// end
// 更新玩家的数据库的金币的,
mysql_game.add_ugame_uchip(uid, bonues, true);
// end
// 更新game redis
redis_game.get_ugame_info_inredis(uid, function(status, ugame_info) {
if (status != Respones.OK) {
log.error("redis game get ugame info failed!!!", status);
return;
}
// 成功获取
ugame_info.uchip += bonues;
redis_game.set_ugame_info_inredis(uid, ugame_info);
// end
});
// end
var ret = {
0: Respones.OK,
1: bonues,
};
ret_func(ret);
}
function get_login_bonues_info(uid, ret_func) {
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的uid, 注册一个
get_login_bonues_info_success(uid, 0, null, ret_func);
}
else {
var sql_bonues_info = data[0];
if (sql_bonues_info.status != 0) { // 今天的已经领取
get_login_bonues_info_success(uid, 0, null, ret_func);
return;
}
get_login_bonues_info_success(uid, 1, sql_bonues_info, ret_func);
}
});
}
function get_game_info(uid, ret_func) {
mysql_game.get_ugame_info_by_uid(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) { // 没有这样的uid, 注册一个
mysql_game.insert_ugame_user(uid, game_config.game_data.first_uexp,
game_config.game_data.first_uchip, function(status) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
get_game_info(uid, ret_func);
});
}
else {
var sql_ugame = data[0];
if (sql_ugame.status != 0) { // 账号被封
write_err(Respones.ILLEGAL_ACCOUNT, ret_func);
return;
}
get_ugame_info_success(uid, sql_ugame, ret_func);
}
});
}
// 领取登陆奖励
function recv_login_bonues(uid, bonuesid, ret_func) {
// 查询登陆奖励的合法性
mysql_game.get_login_bonues_info(uid, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
if (data.length <= 0) {
write_err(Respones.INVALIDI_OPT, ret_func);
}
else {
var sql_bonues_info = data[0];
if (sql_bonues_info.status != 0 || sql_bonues_info.id != bonuesid) { // 今天的已经领取
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// 发放奖励
recv_login_bonues_success(uid, bonuesid, sql_bonues_info.bonues, ret_func);
}
});
}
function get_players_rank_info(my_uid, data, ret_func) {
var rank_array = []; // [[unick, usex, uface, uchip], [], [],]
var total_len = Math.floor(data.length / 2); // uid, chip, uid, chip
var is_sended = false;
var loaded = 0;
var my_rank = -1;
for(var i = 0; i < total_len; i ++) {
rank_array.push([]); // 放[unick, usex, uface, uchip]
}
// 获取每一个在榜的玩家的信息[unick, usex, uface, uchip]
// rendis_center去获取
var call_func = function(uid, uchip, out_array) {
redis_center.get_uinfo_inredis(uid, function(status, data) {
if (status != Respones.OK) {
if (!is_sended) {
write_err(status, ret_func);
is_sended = true;
}
return;
}
//
out_array.push(data.unick);
out_array.push(data.usex);
out_array.push(data.uface);
out_array.push(uchip);
loaded ++;
if (loaded >= total_len) {
get_rank_info_success(my_rank, rank_array, ret_func);
return;
}
// end
});
}
var j = 0;
for(var i = 0; i < data.length; i += 2, j ++) {
if (my_uid == data[i]) {
my_rank = (i + 1);
}
call_func(data[i], data[i + 1], rank_array[j]);
}
}
function get_world_rank_info(uid, ret_func) {
redis_game.get_world_rank_info("NODE_GAME_WORLD_RANK", 30, function(status, data) {
if (status != Respones.OK) {
write_err(status, ret_func);
return;
}
// uid, uchip,
get_players_rank_info(uid, data, ret_func);
// end
});
}
module.exports = {
get_game_info: get_game_info,
get_login_bonues_info: get_login_bonues_info,
recv_login_bonues: recv_login_bonues,
get_world_rank_info: get_world_rank_info,
};
游戏数据库模块
var mysql = require("mysql");
var util = require('util')
var Respones = require("../apps/Respones.js");
var log = require("../utils/log.js");
var utils = require("../utils/utils.js");
var conn_pool = null;
function connect_to_gserver(host, port, db_name, uname, upwd) {
conn_pool = mysql.createPool({
host: host, // 数据库服务器的IP地址
port: port, // my.cnf指定了端口,默认的mysql的端口是3306,
database: db_name, // 要连接的数据库
user: uname,
password: upwd,
});
}
function mysql_exec(sql, callback) {
conn_pool.getConnection(function(err, conn) {
if (err) { // 如果有错误信息
if(callback) {
callback(err, null, null);
}
return;
}
conn.query(sql, function(sql_err, sql_result, fields_desic) {
conn.release(); // 忘记加了
if (sql_err) {
if (callback) {
callback(sql_err, null, null);
}
return;
}
if (callback) {
callback(null, sql_result, fields_desic);
}
});
// end
});
}
function get_login_bonues_info(uid, callback) {
var sql = "select days, bunues_time, id, bonues, status from login_bonues where uid = %d limit 1";
var sql_cmd = util.format(sql, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
callback(Respones.OK, sql_ret);
});
}
function insert_user_login_bonues(uid, bonues, callback) {
var time = utils.timestamp();
var sql = "insert into login_bonues(`days`, `bunues_time`, `bonues`, `uid`)values(%d, %d, %d, %d)";
var sql_cmd = util.format(sql, 1, time, bonues, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
});
}
function update_user_login_bunues(uid, bonues, days, callback) {
var time = utils.timestamp();
var sql = "update login_bonues set days = %d, bunues_time = %d, status = 0, bonues = %d where uid = %d";
var sql_cmd = util.format(sql, days, time, bonues, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
})
}
function get_ugame_info_by_uid(uid, callback) {
var sql = "select uexp, uid, uchip, uvip, status from ugame where uid = %d limit 1";
var sql_cmd = util.format(sql, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
callback(Respones.OK, sql_ret);
});
}
function insert_ugame_user(uid, uexp, uchip, callback) {
var sql = "insert into ugame(`uid`, `uexp`, `uchip`)values(%d, %d, %d)";
var sql_cmd = util.format(sql, uid, uexp, uchip);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
callback(Respones.SYSTEM_ERR);
return;
}
callback(Respones.OK);
});
}
// 有增加,也减少
function add_ugame_uchip(uid, uchip, is_add) {
if (!is_add) { // 扣除
uchip = -uchip; // 负数
}
var sql = "update ugame set uchip = uchip + %d where uid = %d";
var sql_cmd = util.format(sql, uchip, uid);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
// callback(Respones.SYSTEM_ERR);
log.error(err);
return;
}
})
}
function update_login_bonues_recved(bonues_id) {
var sql = "update login_bonues set status = 1 where id = %d";
var sql_cmd = util.format(sql, bonues_id);
log.info(sql_cmd);
mysql_exec(sql_cmd, function(err, sql_ret, fields_desic) {
if (err) {
// callback(Respones.SYSTEM_ERR);
log.error(err);
return;
}
})
}
module.exports = {
connect: connect_to_gserver,
get_ugame_info_by_uid: get_ugame_info_by_uid,
insert_ugame_user: insert_ugame_user,
get_login_bonues_info: get_login_bonues_info,
insert_user_login_bonues: insert_user_login_bonues,
update_user_login_bunues: update_user_login_bunues,
update_login_bonues_recved: update_login_bonues_recved, // 更新登陆奖励为领取状态
add_ugame_uchip: add_ugame_uchip, // 更新玩家的金币
}
游戏redis模块
var redis = require("redis");
var util = require('util')
var Respones = require("../apps/Respones.js");
var log = require("../utils/log.js");
var utils = require("../utils/utils.js");
var game_redis = null;
function connect_to_game(host, port, db_index) {
game_redis = redis.createClient({
host: host,
port: port,
db: db_index,
});
game_redis.on("error", function(err) {
log.error(err);
});
}
/* key, --> value
bycw_center_user_uid_8791
uinfo : {
unick: string,
uface: 图像ID,
usex: 性别,
uvip: VIP等级
is_guest: 是否为游客
}
*/
function set_ugame_info_inredis(uid, ugame_info) {
if (game_redis === null) {
return;
}
var key = "bycw_game_user_uid_" + uid;
ugame_info.uchip = ugame_info.uchip.toString();
ugame_info.uexp = ugame_info.uexp.toString();
ugame_info.uvip = ugame_info.uvip.toString();
log.info("redis game hmset " + key);
game_redis.hmset(key, ugame_info, function(err) {
if(err) {
log.error(err);
}
});
}
// callback(status, body)
function get_ugame_info_inredis(uid, callback) {
if (game_redis === null) {
callback(Respones.SYSTEM_ERR, null);
return;
}
var key = "bycw_game_user_uid_" + uid;
log.info("hgetall ", key);
game_redis.hgetall(key, function(err, data) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
var ugame_info = data;
ugame_info.uchip = parseInt(ugame_info.uchip);
ugame_info.uexp = parseInt(ugame_info.uexp);
ugame_info.uvip = parseInt(ugame_info.uvip);
callback(Respones.OK, ugame_info);
});
}
function add_ugame_uchip(uid, uchip, is_add) {
get_ugame_info_inredis(uid, function(status, ugame_info) {
if(status != Respones.OK) {
return;
}
if (!is_add) {
uchip = -uchip;
}
ugame_info.uchip += uchip;
set_ugame_info_inredis(uid, ugame_info);
})
}
function update_game_world_rank(rank_name, uid, uchip) {
game_redis.zadd(rank_name, uchip, "" + uid);
}
function get_world_rank_info(rank_name, rank_num, callback) {
// 由大到小
game_redis.zrevrange(rank_name, 0, rank_num, "withscores", function(err, data) {
if (err) {
callback(Respones.SYSTEM_ERR, null);
return;
}
if (!data || data.length <= 0) {
callback(Respones.RANK_IS_EMPTY, null);
return;
}
// uid, uchip --> 整数redis 字符串,
// [uid, uchip, uid, uchip, uid, uchip ...]
for(var i = 0; i < data.length; i ++) {
data[i] = parseInt(data[i]);
}
callback(Respones.OK, data);
});
}
module.exports = {
connect: connect_to_game,
set_ugame_info_inredis: set_ugame_info_inredis,
get_ugame_info_inredis: get_ugame_info_inredis,
update_game_world_rank: update_game_world_rank,
get_world_rank_info: get_world_rank_info,
add_ugame_uchip: add_ugame_uchip,
};
游戏逻辑服务器
游戏服务器可以同时配置很多台,一个服务器负责一种类型的游戏(对战),这里以五子棋为例
进入分区
进入游戏分区的其中一个:例如:初级场,高级场,大师场
请求: 进入区间协议
服务号
命令号
区间号
返回:
服务号
命令号
status: 状态码;
玩家离开
分为主动离开和被动掉线离开,处理的区别是被动掉线不用给该用户发数据了。后面会进行掉线重连的处理。重新连上游戏会搜索房间号,拉取进度信息,重新进入游戏。
玩家匹配
1: 目前使用自动配桌的模式,开发人员可自行加入命令,获取区间的桌子信息;
2: 每隔一段时间启动定时器去找等待列表上的玩家,找到后,再寻找合适的房间;
3: 没有合适的房间就创建一个,并把这个房间加入到区间进行管理;
4: 玩家进入房间后,先把玩家放到旁观区(设置旁观区的大小: 人数上限20);
进入房间
进入房间协议:
服务号
命令号
桌子ID
返回:
服务号
命令号
{
status
区间号
房间号
}
网关的广播机制(重要)
应用场景:同一个数据包发送给不同uid的用户
方法:
- 游戏服务器发送数据的本体,以及uid的集合,传给网关,网关发送给多个用户
- 网关中注册一个bc_service,使用二进制传输协议,只接收广播命令
网关处注册:
service_manager.register_service(Stype.Broadcast, bc_service);
bc_service
require("./bc_proto.js");
var gw_service = require("./gw_service.js");
var service = {
name: "broadcast service", // 服务名称
is_transfer: false, // 是否为转发模块,
// 收到客户端给我们发来的数据
on_recv_player_cmd: function(session, stype, ctype, body, utag, proto_type, raw_cmd) {
},
// 收到我们连接的服务给我们发过来的数据;
on_recv_server_return: function (session, stype, ctype, body, utag, proto_type, raw_cmd) {
var cmd_buf = body.cmd_buf;
var users = body.users;
for(var i in users) {
var client_session = gw_service.get_session_by_uid(users[i]);
if (!client_session) {
continue;
}
client_session.send_encoded_cmd(cmd_buf);
}
},
// 收到客户端断开连接;
on_player_disconnect: function(stype, session) {
},
};
module.exports = service;
玩家准备与开始
准备
1:协议:
服务号
命令号
2: 返回: 广播给所有的玩家,当前这个玩家准备好了
服务号
命令号
{
status: OK, 错误码
sv_seatid: 哪个座位玩家准备好了;
}
3: 玩家抵达的时候多传一个玩家的状态过去;
开始
1: 服务器主动通知:
服务号
命令号
body {
0: 玩家的思考时间
1: 多少秒后正式开始, 留给客户端时间播放动画
2: 持黑的玩家,先开始下棋的玩家, 第一次随机,后面轮着来,等玩家换了第一次随机;
… 根据游戏需要自行加入
}
2: 客户端:
清理桌子
清理座位
准备开始;
显示黑子和白子
轮到玩家
1: 服务器创建一个棋盘, 黑棋是1, 白棋是2,没有是0, 开始的时候棋盘清零;
2:当游戏开始了以后,房间去管理游戏流程,轮到当前的玩家,从执黑玩家开始;
3: 服务器主动通知:
服务号
命令号
{
0:思考时间, // 本次的操作的思考时间
1:座位号, // 轮到哪个玩家
2:[用户当前可以得操作, …], 客户端根据这个显示他可以有的操作, }
}
4: 客户端的时间进度条显示
玩家操作(下棋)
1: 客户端棋盘响应触摸事件, 在指定的位置下棋;
2: 发送下棋命令到游戏服务器;
3: 游戏服务器验证,返回结果给客户端;
4: 客户端显示下的棋;
服务号
命令号
{
0: x,
1: y,
}
返回:
服务号,命令号 {
0: status,
1: x, 2: y,
3: 棋子的颜色
}
回合结算与游戏结束
每下完一步棋,进行一次结算,看是否满足胜利条件,满足胜利条件时,给玩家发送结算数据。如果玩家逃跑,直接结算。
每次轮到玩家时,开启一个新的定时器,定时器超时时,也会调用结算
1: 向房间里面所有的人广播本局的结算结果;
2: 玩家扣掉分数, 数据库与redis的分数;
3: 命令协议格式:
服务号
命令号
{
0: winner_seatid, // 如果是平局,winner_seatid = -1;
1: winner_score, // 输赢
}
4: 客户端弹出对话框;
服务端清理:
- 改变房间的状态;
- 改变玩家的状态;
- 踢出不符合金币要求的玩家;
- 广播结算完成的命令:
服务号
命令号
null; - 清理数据;
玩家断线重连
玩家被迫退出后,服务器不会马上判定游戏结束,而是会给客户端断线重连的时间,当客户端重新连接后(重新进入游戏),服务器把本局所需的所有数据发送给该玩家。
断线重连协议:
服务号, 命令号,
body: {
0: 自己的座位号,
1: 座位数据, user_arrived 数据,
2: round_info, // 游戏开始的时候的数据,
3: 当前的棋盘数据, 当前棋盘数据,
4: 游戏进程数据, [当前正轮到的玩家, 玩家剩余的思考时间];
}
重播功能
原理:当进行游戏的时候,服务器保存发给每个在座的客户端的所有请求和时间戳。然后重播的时候全波发给对应的客户端,客户端依次播放即可。
注意:
1: 如果游戏的回放数据过大,可以考虑每次写入文件;
2: 如果游戏的回放数据过大,可以考虑走另外的webserver的方式下载,而不是阻塞网关的带宽;
代码
入口(开启监听,注册服务)
require("../../init.js");
var game_config = require("../game_config.js");
var proto_man = require("../../netbus/proto_man.js");
var netbus = require("../../netbus/netbus.js");
var service_manager = require("../../netbus/service_manager.js");
var Stype = require("../Stype.js");
var five_chess_service = require("./five_chess_service.js");
var game_server = game_config.game_server;
netbus.start_tcp_server(game_server.host, game_server.port, false);
service_manager.register_service(Stype.Game5Chess, five_chess_service);
// 连接中心redis
var center_redis_config = game_config.center_redis;
var redis_center = require("../../database/redis_center.js");
redis_center.connect(center_redis_config.host, center_redis_config.port, center_redis_config.db_index);
// end
// 连接游戏redis
var game_redis_config = game_config.game_redis;
var redis_game = require("../../database/redis_game.js");
redis_game.connect(game_redis_config.host, game_redis_config.port, game_redis_config.db_index);
// end
// 连接游戏数据库
var game_mysql_config = game_config.game_database;
var mysql_game = require("../../database/mysql_game.js");
mysql_game.connect(game_mysql_config.host, game_mysql_config.port,
game_mysql_config.db_name, game_mysql_config.uname, game_mysql_config.upwd);
服务模块(service)
var log = require("../../utils/log.js");
var Cmd = require("../Cmd.js");
var Respones = require("../Respones.js");
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var utils = require("../../utils/utils.js");
require("./five_chess_proto.js");
require("../gateway/bc_proto.js");
var five_chess_model = require("./five_chess_model.js");
function enter_zone(session, uid, proto_type, body) {
if (!body) { // 最后加一下
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.ENTER_ZONE, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
var zid = body;
five_chess_model.enter_zone(uid, zid, session, proto_type, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.ENTER_ZONE, body, uid, proto_type);
})
}
function user_quit(session, uid, proto_type, body) {
five_chess_model.user_quit(uid, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.USER_QUIT, body, uid, proto_type);
})
}
function send_prop(session, uid, proto_type, body) {
if (!body) { // 最后加一下
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.SEND_PROP, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
var propid = body[0];
var to_seatid = body[1];
five_chess_model.send_prop(uid, to_seatid, propid, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.SEND_PROP, body, uid, proto_type);
});
}
function do_player_ready(session, uid, proto_type, body) {
five_chess_model.do_player_ready(uid, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.SEND_DO_READY, body, uid, proto_type);
});
}
function do_player_put_chess(session, uid, proto_type, body) {
if (!body) { // 最后加一下
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.PUT_CHESS, Respones.INVALID_PARAMS, uid, proto_type);
return;
}
var block_x = body[0];
var block_y = body[1];
five_chess_model.do_player_put_chess(uid, block_x, block_y, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.PUT_CHESS, body, uid, proto_type);
});
}
function do_player_get_prev_round_data(session, uid, proto_type, body) {
five_chess_model.do_player_get_prev_round_data(uid, function(body) {
session.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.GET_PREV_ROUND, body, uid, proto_type);
});
}
var service = {
name: "five_chess_service", // 服务名称
is_transfer: false, // 是否为转发模块,
// 收到客户端给我们发来的数据
on_recv_player_cmd: function(session, stype, ctype, body, utag, proto_type, raw_cmd) {
log.info(stype, ctype, body);
switch(ctype) {
case Cmd.Game5Chess.ENTER_ZONE:
enter_zone(session, utag, proto_type, body);
break;
case Cmd.Game5Chess.USER_QUIT:
user_quit(session, utag, proto_type, body);
break;
case Cmd.Game5Chess.SEND_PROP:
send_prop(session, utag, proto_type, body);
break;
case Cmd.Game5Chess.SEND_DO_READY:
do_player_ready(session, utag, proto_type, body);
break;
case Cmd.Game5Chess.PUT_CHESS:
do_player_put_chess(session, utag, proto_type, body);
break;
case Cmd.Game5Chess.GET_PREV_ROUND:
do_player_get_prev_round_data(session, utag, proto_type, body);
break;
case Cmd.USER_DISCONNECT:
five_chess_model.user_lost_connect(utag);
break;
}
},
// 收到我们连接的服务给我们发过来的数据;
on_recv_server_return: function (session, stype, ctype, body, utag, proto_type, raw_cmd) {
},
// 收到客户端断开连接;
on_player_disconnect: function(stype, session) {
},
};
module.exports = service;
逻辑模块
module.exports = {
enter_zone: enter_zone,
user_quit: user_quit,
user_lost_connect: user_lost_connect,
send_prop: send_prop,
do_player_ready: do_player_ready,
do_player_put_chess: do_player_put_chess,
kick_player_chip_not_enough: kick_player_chip_not_enough,
kick_offline_player: kick_offline_player,
do_player_get_prev_round_data: do_player_get_prev_round_data,
};
var Respones = require("../Respones.js");
var redis_center = require("../../database/redis_center.js");
var redis_game = require("../../database/redis_game.js");
var mysql_game = require("../../database/mysql_game.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var game_config = require("../game_config.js");
var five_chess_player = require("./five_chess_player.js");
var five_chess_room = require("./five_chess_room.js");
// zid --> zone对象的表
var zones = {};
var player_set = {}; // uid --> player对应表
var QuitReason = require("./QuitReason.js");
function get_player(uid) {
if (player_set[uid]) {
return player_set[uid];
}
return null;
}
function alloc_player(uid, session, proto_type) {
if (player_set[uid]) {
log.warn("alloc_player: user is exist!!!!");
return player_set[uid];
}
var p = new five_chess_player(uid);
p.init_session(session, proto_type);
return p;
}
function delete_player(uid) {
if (player_set[uid]) {
player_set[uid].init_session(null, -1);
player_set[uid] = null;
delete player_set[uid];
}
else {
log.warn("delete_player:", uid, "is not in game server!!!!");
}
}
function zone(config) {
this.config = config;
this.wait_list = {}; // 链表来做,玩家的等待列表;
this.room_list = {}; // 房间ID-->房间;
this.autoinc_roomid = 1; // 自增房间ID,来生成唯一的ID
// ...
}
function init_zones() {
var zones_config = game_config.game_data.five_chess_zones;
for(var i in zones_config) {
var zid = zones_config[i].zid;
var z = new zone(zones_config[i]);
zones[zid] = z;
}
}
init_zones();
function write_err(status, ret_func) {
var ret = {};
ret[0] = status;
ret_func(ret);
}
function player_enter_zone(player, zid, ret_func) {
var zone = zones[zid];
// 判断ZID的合法性
if (!zones[zid]) {
ret_func(Respones.INVALID_ZONE);
return;
}
// ....
// end
// 玩家是否能进入这个区间
if (player.uchip < zone.config.min_chip) {
ret_func(Respones.CHIP_IS_NOT_ENOUGH);
return;
}
// end
// 玩家的VIP等级是否能够进入
if (player.uvip < zone.config.vip_level) {
ret_func(Respones.VIP_IS_NOT_ENOUGH);
return;
}
player.zid = zid;
player.room_id = -1;
zone.wait_list[player.uid] = player;
ret_func(Respones.OK);
log.info("player:", player.uid, "etner zone and add to waitlist", zid);
}
function get_uinfo_inredis(uid, player, zid, ret_func) {
redis_center.get_uinfo_inredis(uid, function(status, data) {
if (status != Respones.OK) {
ret_func(status);
return;
}
player.init_uinfo(data);
player_set[uid] = player;
// end
player_enter_zone(player, zid, ret_func);
});
}
function enter_zone(uid, zid, session, proto_type, ret_func) {
var player = get_player(uid);
if (!player) {
player = alloc_player(uid, session, proto_type);
// 获取我们的用户信息, 数据库里面读取
mysql_game.get_ugame_info_by_uid(uid, function(status, data) {
if (status != Respones.OK) {
ret_func(status);
return;
}
if (data.length < 0) {
ret_func(Respones.ILLEGAL_ACCOUNT);
return;
}
var ugame_info = data[0];
if (ugame_info.status != 0) {
ret_func(Respones.ILLEGAL_ACCOUNT);
return;
}
player.init_ugame_info(ugame_info);
get_uinfo_inredis(uid, player, zid, ret_func);
});
// end
}
else {
// player已经在特定的房间了
if (player.zid != -1 && player.room_id != -1) {
var zone = zones[player.zid];
var room = zone.room_list[player.room_id];
// 把这个玩家对象的session恢复一下
player.init_session(session, proto_type);
// end
// 当前房间的游戏进度数据传递给我们的客户端,让它能回到游戏
room.do_reconnect(player);
}
else {
player_enter_zone(player, zid, ret_func);
}
}
}
// 执行玩家离开的动作
// is_force: 玩家是主动离开,还是被动离开
function do_user_quit(uid, quit_reason) {
var player = get_player(uid);
if (!player) {
return;
}
if (quit_reason == QuitReason.UserLostConn) { // 断线离开要清理一下;
player.init_session(null, -1);
}
log.info("player uid=", uid, "quit game_server reason:", quit_reason);
if (player.zid != -1 && zones[player.zid]) { // 玩家已经在游戏区间里面了,从区间里面离开
var zone = zones[player.zid];
if (player.room_id != -1) { // 玩家已经在房间里面了,从房间里面退出
var room = zone.room_list[player.room_id];
if (room) {
// 如果玩家正在房间游戏,就不允许退出,
if (!room.do_exit_room(player, quit_reason)) {
return;
}
}
else {
player.room_id = -1;
}
player.zid = -1;
log.info("player uid:", uid, "exit zone:", player.zid, "at room:", player.room_id);
}
else { // 从等待列表里面退出
if (zone.wait_list[uid]) {
log.info("player uid", uid, "remove from waitlist at zone:", player.zid);
player.zid = -1;
player.room_id = -1;
zone.wait_list[uid] = null;
delete zone.wait_list[uid];
}
}
}
delete_player(uid);
}
function user_quit(uid, ret_func) {
do_user_quit(uid, QuitReason.UserQuit);
ret_func(Respones.OK);
}
function user_lost_connect(uid) {
do_user_quit(uid, QuitReason.UserLostConn);
}
function kick_player_chip_not_enough(uid) {
do_user_quit(uid, QuitReason.CHIP_IS_NOT_ENOUGH);
}
function kick_offline_player() {
do_user_quit(uid, QuitReason.SystemKick);
}
// 自动配桌
function alloc_room(zone) {
var room = new five_chess_room(zone.autoinc_roomid ++, zone.config);
zone.room_list[room.room_id] = room;
return room;
}
function do_search_room(zone) {
var min_empty = 1000000;
var min_room = null;
for(var key in zone.room_list) {
room = zone.room_list[key];
var empty_num = room.empty_seat();
// 说明,有可能你有三个人一桌,那么你可能需要先找2个人的桌子
if (room && empty_num >= 1) {
if (empty_num < min_empty) {
min_room = room;
min_empty = empty_num;
}
}
}
if (min_room) {
return min_room;
}
// 没有找到合适的房间, 创建一个;
min_room = alloc_room(zone);
return min_room;
}
function do_assign_room() {
for(var i in zones) { // 遍历所有的区间
// 查询等待列表,看有么有玩家
var zone = zones[i];
for(var key in zone.wait_list) { // 遍历区间的等待列表
var p = zone.wait_list[key];
var room = do_search_room(zone);
if (room) {
// 玩家加入到房间
room.do_enter_room(p);
zone.wait_list[key] = null;
delete zone.wait_list[key];
}
}
}
}
// end
setInterval(do_assign_room, 500);
function send_prop(uid, to_seatid, propid, ret_func) {
var player = get_player(uid);
if (!player) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (player.zid === -1 || player.room_id === -1) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var zone = zones[player.zid];
if (!zone) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var room = zone.room_list[player.room_id];
if (!room) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
room.send_prop(player, to_seatid, propid, ret_func);
}
function do_player_get_prev_round_data(uid, ret_func) {
var player = get_player(uid);
if (!player) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (player.zid === -1 || player.room_id === -1) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var zone = zones[player.zid];
if (!zone) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var room = zone.room_list[player.room_id];
if (!room) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
room.do_player_get_prev_round_data(player, ret_func);
}
function do_player_ready(uid, ret_func) {
var player = get_player(uid);
if (!player) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (player.zid === -1 || player.room_id === -1) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var zone = zones[player.zid];
if (!zone) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var room = zone.room_list[player.room_id];
if (!room) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
room.do_player_ready(player, ret_func);
}
function do_player_put_chess(uid, block_x, block_y, ret_func) {
var player = get_player(uid);
if (!player) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (player.zid === -1 || player.room_id === -1) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var zone = zones[player.zid];
if (!zone) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var room = zone.room_list[player.room_id];
if (!room) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
room.do_player_put_chess(player, block_x, block_y, ret_func);
}
房间模块
该模块基于面向对象编程
var Respones = require("../Respones.js");
var redis_center = require("../../database/redis_center.js");
var redis_game = require("../../database/redis_game.js");
var mysql_game = require("../../database/mysql_game.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var proto_man = require("../../netbus/proto_man.js");
var State = require("./State.js");
var QuitReason = require("./QuitReason.js");
var five_chess_model = require("./five_chess_model.js");
var INVIEW_SEAT = 20;
var GAME_SEAT = 2; //
var DISK_SIZE = 15; // 棋盘的大小
var ChessType = {
NONE: 0,
BLACK: 1,
WHITE: 2,
};
function write_err(status, ret_func) {
var ret = {};
ret[0] = status;
ret_func(ret);
}
function five_chess_room(room_id, zone_conf) {
this.zid = zone_conf.zid; // 玩家当前所在的区间
this.room_id = room_id; // 玩家当前所在的房间ID号
this.think_time = zone_conf.think_time;
this.min_chip = zone_conf.min_chip; // 玩家有可能一直游戏,
this.bet_chip = zone_conf.one_round_chip;
this.state = State.Ready; // 房间已经准备好,可以游戏了
// game
this.black_rand = true; // 随机生成黑色的玩家
this.black_seatid = -1; // 黑色的座位
this.cur_seatid = -1; // 当前轮到的这个玩家
// end
// 0, INVIEW_SEAT
this.inview_players = [];
for(var i = 0; i < INVIEW_SEAT; i ++) {
this.inview_players.push(null);
}
// end
// 游戏座位
this.seats = [];
for(var i = 0; i < GAME_SEAT; i ++) {
this.seats.push(null);
}
// end
// 创建棋盘 15x15
this.chess_disk = [];
for(var i = 0; i < DISK_SIZE * DISK_SIZE; i ++) {
this.chess_disk.push(ChessType.NONE);
}
// end
// 定时器对象
this.action_timer = null;
this.action_timeout_timestamp = 0; // 玩家这个超时的时间挫
// end
// 上局回访数据
this.prev_round_data = null;
this.round_data = {};
// end
}
five_chess_room.prototype.reset_chess_disk = function() {
for(var i = 0; i < DISK_SIZE * DISK_SIZE; i ++) {
this.chess_disk[i] = ChessType.NONE;
}
}
five_chess_room.prototype.search_empty_seat_inview = function() {
for(var i = 0; i < INVIEW_SEAT; i ++) {
if(this.inview_players[i] == null) {
return i;
}
}
return -1;
}
five_chess_room.prototype.get_user_arrived = function(other) {
var body = {
0: other.seatid,
1: other.unick,
2: other.usex,
3: other.uface,
4: other.uchip,
5: other.uexp,
6: other.uvip,
7: other.state, // 玩家当前游戏状态
};
return body;
}
// 玩家进入到我们的游戏房间
five_chess_room.prototype.do_enter_room = function(p) {
var inview_seat = this.search_empty_seat_inview();
if (inview_seat < 0) {
log.warn("inview seat is full!!!!!");
return;
}
this.inview_players[inview_seat] = p;
p.room_id = this.room_id;
p.enter_room(this);
// 如果你觉得有必要,那么需要把玩家进入房间的消息,玩家的信息
// 广播给所有的人,有玩家进来旁观了
// 。。。。
// end
// 我们要把座位上的所有的玩家,发送给进来旁观的这位同学
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i]) {
continue;
}
var other = this.seats[i];
/*var body = {
0: other.seatid,
1: other.unick,
2: other.usex,
3: other.uface,
4: other.uchip,
5: other.uexp,
6: other.uvip,
7: other.state, // 玩家当前游戏状态
};*/
var body = this.get_user_arrived(other);
p.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.USER_ARRIVED, body);
}
// end
log.info("player:", p.uid, "enter room:", this.zid, "--", this.room_id);
var body = {
0: Respones.OK,
1: this.zid,
2: this.room_id,
// .... 房间信息
};
p.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.ENTER_ROOM, body);
// 自动分配一个座位给我们的玩家,学员也可以改成发送命令手动分配
this.do_sitdown(p);
// end
}
// end
five_chess_room.prototype.do_sitdown = function(p) {
if (p.seatid !== -1) {
return;
}
// 搜索一个可用的空位
var sv_seat = this.search_empty_seat();
if (sv_seat === -1) { // 只能旁观
return;
}
// end
log.info(p.uid, "sitdown at seat: ", sv_seat);
this.seats[sv_seat] = p;
p.seatid = sv_seat;
p.sitdown(this);
// 发送消息给客户端,这个玩家已经坐下来了
var body = {
0: Respones.OK,
1: sv_seat,
};
p.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.SITDOWN, body);
// end
// 广播给所有的其他玩家(旁观的玩家),玩家坐下,
/*var body = {
0: p.seatid,
1: p.unick,
2: p.usex,
3: p.uface,
4: p.uchip,
5: p.uexp,
6: p.uvip,
7: p.state, // 当前玩家的状态
};*/
var body = this.get_user_arrived(p);
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.USER_ARRIVED, body, p.uid);
// end
}
five_chess_room.prototype.do_exit_room = function(p, quit_reason) {
// 短线重连的流程
if (quit_reason == QuitReason.UserLostConn &&
this.state == State.Playing &&
p.state == State.Playing) { // 短线重连的流程
return false;
}
// end
var winner = null;
// ....
if (p.seatid != -1) { // 当前玩家在座位上
if (p.state == State.Playing) { // 当前正在游戏,逃跑,对家赢
var winner_seatid = GAME_SEAT - p.seatid - 1;
winner = this.seats[winner_seatid];
if (winner) {
this.checkout_game(1, winner);
}
}
// end
var seatid = p.seatid;
log.info(p.uid, "standup at seat: ", p.seatid);
p.standup(this);
this.seats[p.seatid] = null;
p.seatid = -1;
// 广播给所有的玩家(旁观的玩家),玩家站起,
var body = {
0: Respones.OK,
1: seatid,
};
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.STANDUP, body, null);
// end
}
// end
log.info("player:", p.uid, "exit room:", this.zid, "--", this.room_id);
// 把玩家从旁观列表里面删除
for(var i = 0; i < INVIEW_SEAT; i ++) {
if (this.inview_players[i] == p) {
this.inview_players[i] = null;
}
}
// end
p.exit_room(this);
p.room_id = -1;
// 广播给所有的玩家(旁观的玩家), 玩家离开了房间,(如果有必要)
// 。。。。
// end
return true;
}
five_chess_room.prototype.search_empty_seat = function() {
// for(var i in this.seats) { // bug
for(var i = 0; i < GAME_SEAT; i ++) {
if (this.seats[i] === null) {
return i;
}
}
return -1;
}
five_chess_room.prototype.empty_seat = function() {
var num = 0;
for(var i in this.seats) {
if (this.seats[i] === null) {
num ++;
}
}
return num;
}
// 基于旁观列表来广播
// 我们是分了json, buf协议的
five_chess_room.prototype.room_broadcast = function(stype, ctype, body, not_to_uid) {
var json_uid = [];
var buf_uid = [];
var cmd_json = null;
var cmd_buf = null;
var gw_session = null;
for(var i = 0; i < this.inview_players.length; i ++) {
if (!this.inview_players[i] ||
this.inview_players[i].session === null ||
this.inview_players[i].uid == not_to_uid) {
continue;
}
gw_session = this.inview_players[i].session;
if (this.inview_players[i].proto_type == proto_man.PROTO_JSON) {
json_uid.push(this.inview_players[i].uid);
if (!cmd_json) {
cmd_json = proto_man.encode_cmd(0, proto_man.PROTO_JSON, stype, ctype, body);
}
}
else {
buf_uid.push(this.inview_players[i].uid);
if (!cmd_buf) {
cmd_buf = proto_man.encode_cmd(0, proto_man.PROTO_BUF, stype, ctype, body);
}
}
}
if (json_uid.length > 0) {
var body = {
cmd_buf: cmd_json,
users: json_uid,
};
// 网关的session
gw_session.send_cmd(Stype.Broadcast, Cmd.BROADCAST, body, 0, proto_man.PROTO_BUF);
// end
}
if (buf_uid.length > 0) {
var body = {
cmd_buf: cmd_buf,
users: buf_uid,
};
// 网关的session
gw_session.send_cmd(Stype.Broadcast, Cmd.BROADCAST, body, 0, proto_man.PROTO_BUF);
}
}
five_chess_room.prototype.send_prop = function(p, to_seatid, propid, ret_func) {
if (p.seatid === -1) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (p != this.seats[p.seatid]) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (!this.seats[to_seatid]) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if (propid <= 0 || propid > 5) {
write_err(Respones.INVALID_PARAMS, ret_func);
return;
}
// 在房间里面广播,发送道具也能收到
var body = {
0: Respones.OK,
1: p.seatid,
2: to_seatid,
3: propid,
};
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.SEND_PROP, body, null);
// end
}
five_chess_room.prototype.next_seat = function(cur_seatid) {
var i = cur_seatid;
for(i = cur_seatid + 1; i < GAME_SEAT; i ++) {
if (this.seats[i] && this.seats[i].state == State.Playing) {
return i;
}
}
for(var i = 0; i < cur_seatid; i ++) {
if (this.seats[i] && this.seats[i].state == State.Playing) {
return i;
}
}
return -1;
}
five_chess_room.prototype.get_round_start_info = function() {
var wait_client_time = 3000; // 单位是ms
var body = {
0: this.think_time,
1: wait_client_time, // 给客户端3秒, 3, 2, 1,
2: this.black_seatid,
};
return body;
}
five_chess_room.prototype.game_start = function() {
// 改变房间的状态
this.state = State.Playing;
// end
// 清理我们的棋盘
this.reset_chess_disk();
// end
// 通知所有的玩家
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i] || this.seats[i].state != State.Ready) {
continue;
}
this.seats[i].on_round_start();
}
// end
// 到底是谁先开始,谁执黑棋
// (1)第一局游戏,我们随机,后面我们轮着来;
// (2)一旦玩家变化了,重新开始随机
if (this.black_rand) {
this.black_rand = false;
this.black_seatid = Math.random() * 2; // [0, 2)
this.black_seatid = Math.floor(this.black_seatid);
}
else {
this.black_seatid = this.next_seat(this.black_seatid);
}
// end
// 广播给所有的人,游戏马上要开始了
/*var wait_client_time = 3000; // 单位是ms
var body = {
0: this.think_time,
1: wait_client_time, // 给客户端3秒, 3, 2, 1,
2: this.black_seatid,
};*/
var body = this.get_round_start_info();
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.ROUND_START, body, null);
// end
this.cur_seatid = -1; // 在这个游戏已经开始了,但是还要等3秒这个时间段,当前操作的玩家为-1
// wait_client_time 轮到当前的执黑的玩家开始
setTimeout(this.turn_to_player.bind(this), body[1]/*wait_client_time*/, this.black_seatid);
// end
// 保存一下当前的开局信息
var seats_data = [];
for(var i = 0; i < GAME_SEAT; i ++) {
if(!this.seats[i] || this.seats[i].state != State.Playing) {
continue;
}
var data = this.get_user_arrived(this.seats[i]);
seats_data.push(data);
}
this.round_data[0] = seats_data;
this.round_data[1] = []; // 保存操作民命令
var action_cmd = [utils.timestamp(), Stype.Game5Chess, Cmd.Game5Chess.ROUND_START, body];
this.round_data[1].push(action_cmd);
// end
}
five_chess_room.prototype.do_player_action_timeout = function(seatid) {
this.action_timer = null;
/*
// 结算
var winner_seatid = GAME_SEAT - seatid - 1;
var winner = this.seats[winner_seatid];
this.checkout_game(1, winner)
// end
*/
this.turn_to_next();
}
five_chess_room.prototype.turn_to_player = function(seatid) {
if(this.action_timer !== null) {
clearTimeout(this.action_timer);
this.action_timer = null;
}
if(!this.seats[seatid] || this.seats[seatid].state != State.Playing) {
log.warn("turn_to_player: ", seatid, "seat is invalid!!!!");
return;
}
// 启动一个定时器, 定时器如果触发了以后调用我们超时处理函数
this.action_timer = setTimeout(this.do_player_action_timeout.bind(this), this.think_time * 1000, seatid);
this.action_timeout_timestamp = utils.timestamp() + this.think_time;
// end
var p = this.seats[seatid];
p.turn_to_player(room);
this.cur_seatid = seatid;
var body = {
0: this.think_time,
1: seatid,
};
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.TURN_TO_PLAYER, body, null);
var action_cmd = [utils.timestamp(), Stype.Game5Chess, Cmd.Game5Chess.TURN_TO_PLAYER, body];
this.round_data[1].push(action_cmd);
}
five_chess_room.prototype.check_game_start = function() {
var ready_num = 0;
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i] || this.seats[i].state != State.Ready) {
continue;
}
ready_num ++;
}
if (ready_num >= 2) {
this.game_start();
}
}
five_chess_room.prototype.do_player_get_prev_round_data = function(p, ret_func) {
if (!this.prev_round_data || p.state == State.Playing || p.state == State.Ready) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
var body = {
0: Respones.OK,
1: this.prev_round_data,
};
ret_func(body);
}
five_chess_room.prototype.do_player_ready = function(p, ret_func) {
// 玩家是否已经是坐下在房间里面的
if (p != this.seats[p.seatid]) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// end
// 当前房间是否为准备好了,
if (this.state != State.Ready || p.state != State.InView) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// end
p.do_ready();
// 广播给所有的人,这个玩家准备好了
// 所有旁观的人应该都能看到这个玩家准备好了;
var body = {
0: Respones.OK,
1: p.seatid
};
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.SEND_DO_READY, body, null);
// end
this.check_game_start();
}
five_chess_room.prototype.get_next_seat = function() {
// 从当前的 seatid开始,往后遍历
for(var i = this.cur_seatid + 1; i < GAME_SEAT; i ++) {
if (!this.seats[i] || this.seats[i].state != State.Playing) {
continue;
}
return i;
}
// end
for(var i = 0; i < this.cur_seatid; i ++) {
if (!this.seats[i] || this.seats[i].state != State.Playing) {
continue;
}
return i;
}
return -1;
}
five_chess_room.prototype.check_game_over = function(chess_type) {
// 横向检查
for(var i = 0; i < 15; i ++) {
for(var j = 0; j <= (15 - 5); j ++) {
if (this.chess_disk[i * 15 + j + 0] == chess_type &&
this.chess_disk[i * 15 + j + 1] == chess_type &&
this.chess_disk[i * 15 + j + 2] == chess_type &&
this.chess_disk[i * 15 + j + 3] == chess_type &&
this.chess_disk[i * 15 + j + 4] == chess_type) {
return 1;
}
}
}
// end
// 竖向检查
for(var i = 0; i < 15; i ++) {
for(var j = 0; j <= (15 - 5); j ++) {
if (this.chess_disk[(j + 0) * 15 + i] == chess_type &&
this.chess_disk[(j + 1) * 15 + i] == chess_type &&
this.chess_disk[(j + 2) * 15 + i] == chess_type &&
this.chess_disk[(j + 3) * 15 + i] == chess_type &&
this.chess_disk[(j + 4) * 15 + i] == chess_type) {
return 1;
}
}
}
// end
// 右上角
var line_total = 15;
for(var i = 0; i <= (15 - 5); i ++) {
for(var j = 0; j < (line_total - 4); j ++) {
if (this.chess_disk[(i + j + 0) * 15 + j + 0] == chess_type &&
this.chess_disk[(i + j + 1) * 15 + j + 1] == chess_type &&
this.chess_disk[(i + j + 2) * 15 + j + 2] == chess_type &&
this.chess_disk[(i + j + 3) * 15 + j + 3] == chess_type &&
this.chess_disk[(i + j + 4) * 15 + j + 4] == chess_type) {
return 1;
}
}
line_total --;
}
line_total = 15 - 1;
for(var i = 1; i <= (15 - 5); i ++) {
for(var j = 0; j < (line_total - 4); j ++) {
if (this.chess_disk[(j + 0) * 15 + i + j + 0] == chess_type &&
this.chess_disk[(j + 1) * 15 + i + j + 1] == chess_type &&
this.chess_disk[(j + 2) * 15 + i + j + 2] == chess_type &&
this.chess_disk[(j + 3) * 15 + i + j + 3] == chess_type &&
this.chess_disk[(j + 4) * 15 + i + j + 4] == chess_type) {
return 1;
}
}
line_total --;
}
// end
// 左下角
line_total = 15;
for(var i = 14; i >= 4; i --) {
for(var j = 0; j < (line_total - 4); j ++) {
if (this.chess_disk[(i - j - 0) * 15 + j + 0] == chess_type &&
this.chess_disk[(i - j - 1) * 15 + j + 1] == chess_type &&
this.chess_disk[(i - j - 2) * 15 + j + 2] == chess_type &&
this.chess_disk[(i - j - 3) * 15 + j + 3] == chess_type &&
this.chess_disk[(i - j - 4) * 15 + j + 4] == chess_type) {
return 1;
}
}
line_total --;
}
line_total = 1;
var offset = 0;
for(var i = 1; i <= (15 - 5); i ++) {
offset = 0;
for(var j = 14; j >= (line_total + 4); j --) {
if (this.chess_disk[(j - 0) * 15 + i + offset + 0] == chess_type &&
this.chess_disk[(j - 1) * 15 + i + offset + 1] == chess_type &&
this.chess_disk[(j - 2) * 15 + i + offset + 2] == chess_type &&
this.chess_disk[(j - 3) * 15 + i + offset + 3] == chess_type &&
this.chess_disk[(j - 4) * 15 + i + offset + 4] == chess_type) {
return 1;
}
offset ++;
}
line_total ++;
}
// end
// 检查棋盘是否全部满了,如果没有满,表示游戏可以继续
for(var i = 0; i < DISK_SIZE * DISK_SIZE; i ++) {
if (this.chess_disk[i] == ChessType.NONE) {
return 0;
}
}
// end
return 2; // 返回平局
}
five_chess_room.prototype.checkout_game = function(ret, winner) {
if(this.action_timer !== null) {
clearTimeout(this.action_timer);
this.action_timer = null;
}
this.state = State.CheckOut; // 更新房间的状态为结算状态
// 遍历所有的在游戏的玩家,结算
for(var i = 0; i < GAME_SEAT; i ++) {
if(this.seats[i] === null || this.seats[i].state != State.Playing) {
continue;
}
this.seats[i].checkout_game(this, ret, this.seats[i] === winner);
}
// end
var winner_score = this.bet_chip;
var winner_seat = winner.seatid;
if (ret === 2) {
winner_seat = -1; // 没有赢家
}
// 广播给所有的玩家游戏结算
var body = {
0: winner_seat, // -1, 表示平局,其他的就是赢家的座位号
1: winner_score,
// ...自己加入其他的数据
};
// end
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.CHECKOUT, body, null);
var action_cmd = [utils.timestamp(), Stype.Game5Chess, Cmd.Game5Chess.CHECKOUT, body];
this.round_data[1].push(action_cmd);
this.prev_round_data = this.round_data;
this.round_data = {}; // 清空
// 踢掉离线的玩家
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i]) {
continue;
}
if (this.seats[i].session === null) {
five_chess_model.kick_offline_player(this.seats[i]);
continue;
}
}
// end
// 4秒以后结算结束
var check_time = 4000;
setTimeout(this.on_checkout_over.bind(this), check_time);
}
five_chess_room.prototype.on_checkout_over = function() {
// 更新一下房间的状态
this.state = State.Ready;
// end
for(var i = 0; i < GAME_SEAT; i ++) {
if(!this.seats[i] || this.seats[i].state != State.CheckOut) {
continue;
}
// 通知玩家,游戏结算完成了
this.seats[i].on_checkout_over(this);
// end
}
// 广播给所有的人,结算结束
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.CHECKOUT_OVER, null, null);
// end
// 踢掉不满足要求的玩家
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i]) {
continue;
}
// 玩家金币数目
if (this.seats[i].uchip < this.min_chip) {
five_chess_model.kick_player_chip_not_enough(this.seats[i]);
continue;
}
// end
// 超时间很多
// end
// ......
}
// end
}
five_chess_room.prototype.do_player_put_chess = function(p, block_x, block_y, ret_func) {
// 玩家是否已经是坐下在房间里面的
if (p != this.seats[p.seatid]) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// end
// 当前轮到不是你这个玩家
if (p.seatid != this.cur_seatid) {
write_err(Respones.NOT_YOUR_TURN, ret_func);
return;
}
// 当前房间或玩家不是游戏状态,
if (this.state != State.Playing || p.state != State.Playing) {
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
// end
// 块的参数的合法性
if (block_x < 0 || block_x > 14 || block_y < 0 || block_y > 14) {
write_err(Respones.INVALID_PARAMS, ret_func);
return;
}
var index = block_y * 15 + block_x;
if (this.chess_disk[index] != ChessType.NONE) { // 如果你已经下了这个棋了,
write_err(Respones.INVALIDI_OPT, ret_func);
return;
}
if(p.seatid == this.black_seatid) { // 黑
this.chess_disk[index] = ChessType.BLACK;
}
else {
this.chess_disk[index] = ChessType.WHITE;
}
// 广播给所有的人
var body = {
0: Respones.OK,
1: block_x,
2: block_y,
3: this.chess_disk[index],
};
this.room_broadcast(Stype.Game5Chess, Cmd.Game5Chess.PUT_CHESS, body, null);
var action_cmd = [utils.timestamp(), Stype.Game5Chess, Cmd.Game5Chess.PUT_CHESS, body];
this.round_data[1].push(action_cmd);
// end
// 取消超时定时器
if(this.action_timer !== null) {
clearTimeout(this.action_timer);
this.action_timer = null;
}
// end
// 结算, 下黑棋,那么就看黑棋是否赢了,下白棋,就看白棋是否赢
// 下满了,那么就是平局, 还可以继续,那么就进入下一个
var check_ret = this.check_game_over(this.chess_disk[index]);
if (check_ret != 0) { // 1 win, 2 平局
log.info("game over !!!!", this.chess_disk[index], " result", check_ret);
this.checkout_game(check_ret, p);
return;
}
// end
this.turn_to_next();
}
five_chess_room.prototype.turn_to_next = function() {
// 进入到下一个玩家
var next_seat = this.get_next_seat();
if (next_seat === -1) {
log.error("cannot find next_seat !!!!");
return;
}
// end
this.turn_to_player(next_seat);
}
// 断线重连
five_chess_room.prototype.do_reconnect = function(p) {
if(room.state != State.Playing && p.state != State.Playing) {
return;
}
// 其他玩家的座位数据
var seats_data = [];
for(var i = 0; i < GAME_SEAT; i ++) {
if (!this.seats[i] || this.seats[i] == p ||
this.seats[i].state != State.Playing) {
continue;
}
var arrived_data = this.get_user_arrived(this.seats[i]);
seats_data.push(arrived_data);
}
// end
// 获取开局信息
var round_start_info = this.get_round_start_info();
// end
// 游戏数据数据
// end
// 当前游戏进度的游戏信息
var game_ctrl = [
this.cur_seatid,
this.action_timeout_timestamp - utils.timestamp(), // 剩余的处理时间
];
// end
// 传玩家自己的数据
var body = {
0: p.seatid, // 玩家自己的数据,供玩家坐下使用,
1: seats_data, // 其他玩家的座位数据
2: round_start_info, // 开局信息
3: this.chess_disk, // 棋盘信息
4: game_ctrl, // 游戏控制进度信息
};
p.send_cmd(Stype.Game5Chess, Cmd.Game5Chess.RECONNECT, body);
// end
}
module.exports = five_chess_room;
玩家模块
该模块基于面向对象编程
var Respones = require("../Respones.js");
var redis_center = require("../../database/redis_center.js");
var redis_game = require("../../database/redis_game.js");
var mysql_game = require("../../database/mysql_game.js");
var utils = require("../../utils/utils.js");
var log = require("../../utils/log.js");
var Stype = require("../Stype.js");
var Cmd = require("../Cmd.js");
var proto_man = require("../../netbus/proto_man.js");
var State = require("./State.js");
function five_chess_player(uid) {
this.uid = uid;
this.uchip = 0;
this.uvip = 0;
this.uexp = 0;
this.unick = "";
this.usex = -1;
this.uface = 0;
this.zid = -1; // 玩家当前所在的区间
this.room_id = -1; // 玩家当前所在的房间ID号
this.seatid = -1; // 玩家当前在房间的座位号,没有坐下就是为-1;
this.session = null;
this.proto_type = -1;
this.state = State.InView; // 表示玩家是旁观状态
}
five_chess_player.prototype.init_ugame_info = function(ugame_info) {
this.uchip = ugame_info.uchip;
this.uvip = ugame_info.uvip;
this.uexp = ugame_info.uexp;
}
five_chess_player.prototype.init_uinfo = function(uinfo) {
this.unick = uinfo.unick;
this.usex = uinfo.usex;
this.uface = uinfo.uface;
}
five_chess_player.prototype.init_session = function(session, proto_type) {
this.session = session;
this.proto_type = proto_type;
}
five_chess_player.prototype.send_cmd = function(stype, ctype, body) {
if (!this.session) {
return;
}
// console.log(stype, ctype, body);
this.session.send_cmd(stype, ctype, body, this.uid, this.proto_type);
},
five_chess_player.prototype.enter_room = function(room) {
this.state = State.InView;
}
five_chess_player.prototype.exit_room = function(room) {
this.state = State.InView;
}
five_chess_player.prototype.sitdown = function(room) {
}
five_chess_player.prototype.standup = function(room) {
}
five_chess_player.prototype.do_ready = function() {
this.state = State.Ready;
}
five_chess_player.prototype.on_round_start = function() {
this.state = State.Playing;
}
// 如果要做机器人,那么机器人就可以继承这个chess_player,
// 重载这个turn_to_player, 能够在这里自己思考来下棋
five_chess_player.prototype.turn_to_player = function(room) {
}
// 玩家游戏结算
// 1, 有输赢,2就是平局
five_chess_player.prototype.checkout_game = function(room, ret, is_winner) {
this.state = State.CheckOut;
if (ret === 2) { // 平局
return;
}
// 有输赢
var chip = room.bet_chip;
// 更新数据库的金币, redis的金币
mysql_game.add_ugame_uchip(this.uid, chip, is_winner);
redis_game.add_ugame_uchip(this.uid, chip, is_winner);
// end
if (is_winner) {
this.uchip += chip;
}
else {
this.uchip -= chip;
}
}
// end
five_chess_player.prototype.on_checkout_over = function(room) {
this.state = State.InView; // 玩家变成旁观状态, 等待下一局的开始
}
module.exports = five_chess_player;
WebServer启动配置
1: 目前我们在客户端是直接连接的网关的地址,这样不利于我们的部署;
2: 我们改成使用域名访问webserver,请求网关连接地址的模式;
3: 网关部署的服务器可以任意改动,而不是写死在客户端;
4: 客户端用域名,域名可以通过重定向来搬迁webserver的地址;
5: 通过web请求,获取网关的地址;
6: http get来获取网关ip地址与端口;
7: 客户端,先访问获取网关地址然后再连接;
WebServer用途:
- 热更新
- 支付对接
- 下载配置文件
代码
var game_config = require("../game_config.js");
var express = require("express");
var path = require("path");
var fs = require("fs");
var log = require("../../utils/log.js");
var Cmd = require("../Cmd.js");
var Stype = require("../Stype.js");
/*
if (process.argv.length < 3) {
console.log("node webserver.js port");
return;
}
*/
var app = express();
var host = game_config.webserver.host;
var port = game_config.webserver.port;
// process.chdir("./apps/webserver");
// console.log(process.cwd());
if (fs.existsSync("www_root")) {
app.use(express.static(path.join(process.cwd(), "www_root")));
}
else {
log.warn("www_root is not exists!!!!!!!!!!!");
}
log.info("webserver started at port ", host, port);
// 获取客户端连接的服务器信息,
// http://127.0.0.1:10001/server_info
app.get("/server_info", function (request, respones) {
var data = {
host: game_config.gateway_config.host,
tcp_port: game_config.gateway_config.ports[0],
ws_port: game_config.gateway_config.ports[1],
};
var str_data = JSON.stringify(data);
respones.send(str_data);
});
app.listen(port);
PM2管理工具
PM2是一款基于Node.js开发的守护进程管理工具,它在管理和守护应用程序方面发挥着重要作用。以下是PM2管理工具的主要作用:
一、进程管理与守护
- 启动、停止与重启:PM2可以方便地启动、停止和重启应用程序进程。这对于维护应用服务的稳定性和可用性至关重要。
- 自动重启:当应用进程因错误或异常终止时,PM2能够自动重启进程,确保服务不中断。这一功能对于需要长时间稳定运行的应用尤为重要。
- 开机启动:PM2支持设置应用进程为开机启动,确保在系统重启后应用能够自动恢复运行。
二、进程状态监控
- 实时监控:PM2提供实时监控功能,可以实时查看应用的CPU、内存等性能指标,帮助开发者及时发现潜在的性能问题。
- 进程状态展示:通过PM2,用户可以轻松查看所有正在运行的进程的状态、进程ID、CPU使用率、内存使用量和重启次数等信息。
三、日志管理
- 日志收集与存储:PM2可以收集应用的输出和错误日志,并存储到指定文件中,方便后期审查和分析。
日志搜索与分析:PM2提供日志搜索和分析功能,帮助开发者快速定位问题。
四、负载均衡与集群模式 - 负载均衡:PM2支持集群模式,能够启动多个应用实例并自动分配负载,提高应用的可伸缩性和可用性。这对于处理大量并发请求的应用尤为重要。
- 0秒重载:在更新应用时,PM2支持0秒重载功能,可以在不停止服务的情况下更新应用代码,实现无缝升级。
安装和初步使用
1: 安装PM2 npm install pm2@latest -g
2: 启动服务:
pm2 start xxxx.js;
守护进程功能,挂掉以后还能重启;
3: 查看服务信息:
pm2 list;
4: 查看信息:
pm2 describe 0
5: 查看日志:
cat out log path
6: pm2 monit
7: 如果被杀掉了还会自动拉起来,有守护功能;
8:启动/停止服务
pm2 start/stop id
webserver集群部署
1: webserver的集群部署:
pm2 start app/webserver/webserver.js -i 几个
2: 查看端口占用
netstat -anp
基本命令
pm2 start app.js –name my-api # 命名进程
pm2 list # 显示所有进程状态
pm2 monit # 监视所有进程
pm2 logs # 显示所有进程日志
pm2 stop all # 停止所有进程
pm2 restart all # 重启所有进程
pm2 reload all # 0秒停机重载进程 (用于 NETWORKED 进程)
pm2 stop 0 # 停止指定的进程
pm2 restart 0 # 重启指定的进程
pm2 startup # 产生 init 脚本 保持进程活着
pm2 web # 运行健壮的 computer API endpoint(http://localhost:9615)
pm2 delete 0 # 杀死指定的进程
pm2 delete all # 杀死全部进程