1. 简介
开发了一款微信小游戏 《约战24点》是一款经典益智的数学游戏,它能在游戏中锻炼人们的心算,要求人们将四个数字进行加减乘除四则混合运算(允许使用括号)求得二十四,四个数是从扑克牌里面随机抽取的4张牌,该游戏既能单人玩也可以与好友们一起PK看谁算的更快更准。
游戏本身既支持单机版也支持多人对战玩,是一个连网游戏所以需要服务器,服务器使用的是 C++ 写的,由于
JaveScript 本身不能像 C++ 那样直接操作 tcp socket 编程,但它提供了 websocket 协议来实现与服务器之间的长连接。websocket 本质就是 TCP 协议上一次封装,C++ 接收 websocket 协议数据需要进行一层处理,参见《C++ 使用 websocket 协议》。
客户端与服务器之间消息协议使用的是 msgpack 格式的数据,采用该它主要是因为它会对数据进行压缩,比 json 格式的数据包小,而且进行了压缩后抓包看就相当于进行了一次加密处理,不是裸露的明文数据了。
2. 网络数据编解码处理
codec.js 对 msgpack 协议数据进行打包和解包处理
/**
* 网络数据的编解码层
*/
var md5 = require('md5');
var msgpack = require('msgpack');
// 定义一个接收消息回调的函数
var callbackFunc = null;
var NetCodec = {
/// 设置回调函数
setCallbackFun(cbfunc){
callbackFunc = cbfunc;
},
/// 解析服务器发送给客户端的数据
/// |-2 bytes(datalen)-|-2 bytes(checksum)-|-4 bytes(time)-|-4 bytes(messageid)-|-1 byte(isPacked)-|-N bytes(data)-|
/// |---------------------------Head(13 bytes)-----------------------------------------------------|----MsgPack----|
/// 最前面2个字节表示是MsgPack的长度,真正的数据包内容的长度了
decodeNetData : function(data){
if (data instanceof ArrayBuffer) {
var dv = new DataView(data);
if (dv.byteLength < 2){
console.log("length invaile.");
return;
}
var packLen = dv.getUint16(0, true);// true表示小端字节序
//console.log("packLen = " + packLen);
if (dv.byteLength < packLen + 13) {
console.log("length invaile.");
return;
}
var packFlag = dv.getUint8(12);
if (packFlag === 1) {
// 解析msgpack消息包
//console.log("-----------recv a msgpack, packLen = " + packLen );
var uint8array = new Uint8Array(data, 13, packLen);
var obj = msgpack.decode(uint8array);
//console.log(obj);
// 调用回调函数处理消息
callbackFunc(obj);
}else{
console.log("Not a msgpack, packFlag:" + packFlag);
}
}
},
/// 打包客户端发送服务器的数据
/// |----2 bytes(datalen)----|----------------------N bytes(data)------------------------|
/// |---------Header---------|-----------------Body(msgpack)包含md5校验值-----------------|
/// 最前面2个字节表示是MsgPack包的长度
encodeNetData : function(obj){
if (obj.hasOwnProperty("m_msgId")){
//console.log(obj.m_msgId);
//计算md5校验值
var md5seed = obj.m_msgId + "与服务器之间协定的密钥";
var hash = md5(md5seed) + "";
//console.log("md5:" + hash);
obj.m_md5 = hash;
// 把Body编码成msgpack格式
var mpack = msgpack.encode(obj);
// 申请一块 ArrayBuffer 字节数组类型的空间存放最终要发出去的整条数据
//console.log("packLen=" + mpack.length + ", encoded:" + mpack);
var headerLen = 4;//消息头长度
var buffer = new ArrayBuffer(mpack.length + headerLen);
var view = new DataView(buffer);
// 打包Header消息头
view.setUint32(0, mpack.length, true);
// 打包Body消息体
for (var j = headerLen; j < mpack.length+headerLen; j++) {
view.setUint8(j, mpack[j-headerLen]);
}
return buffer;
}
else {
console.log(obj + " has no m_msgId property.");
return null;
}
},
};
module.exports = NetCodec;
3. websocket 网络层处理
NetControl.js 对 websocket 协议的封装处理
/**
* 网络控制层,当网络上有事件到达时会发送相应的事件消息,使用的地方只要监听这些事件消息
*/
var onfire = require("onfire"); //一个很简单很小的事件分发js开源库
var codec = require('codec'); //数据的编解码处理
var NetControl={
/**
* readyState属性返回实例对象的当前状态,共有四种。
* CONNECTING:值为0,表示正在连接。
* OPEN:值为1,表示连接成功,可以通信了。
* CLOSING:值为2,表示连接正在关闭。
* CLOSED:值为3,表示连接已经关闭,或者打开连接失败
*/
WSConnState: cc.Enum({
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
}),
// 心跳计时器
HearBeatTimer: null,
_sock:{}, //当前的webSocket的对象
connect: function () {
//当前接口没有打开
if(this._sock.readyState !== this.WSConnState.OPEN){
//重新连接
this._sock = new WebSocket("ws://192.168.121.221:9901");//上线需要改成域名的方式
this._sock.onopen = this._onOpen.bind(this);
this._sock.onclose = this._onClose.bind(this);
this._sock.onmessage = this._onMessage.bind(this);
this._sock.onerror = this._onError.bind(this);
}
return this;
},
close: function () {
this._sock.close();
},
send:function(msg){
if (this._sock.readyState === this.WSConnState.OPEN) {
var pack = codec.encodeNetData(msg);
this._sock.send(pack);
}
},
recv:function(msg){
codec.decodeNetData(msg);
},
setMsgDispatcher: function (func) {
codec.setCallbackFun(func);
},
//////////////////////////////////////////////////////////////////////
_onOpen:function(){
//将 WebSocket 对象的二进制类型设置为arraybuffer,默认是blob
this._sock.binaryType = 'arraybuffer';
onfire.fire("onopen");
},
_onClose:function(err){
onfire.fire("onclose", err);
},
_onMessage:function(evt){
onfire.fire("onmessage", evt);
},
_onError:function(){
onfire.fire("onerror", this._sock.readyState);
},
};
module.exports = NetControl;
对于客户端来说网络层的处理相对简单很多,有了这两个工具的封装,业务层上定义一个消息分发的方法 msgDispatcher ,使用 setMsgDispatcher 方法把它设置成一个回调函数, 接到数据后根据不同的消息在 msgDispatcher 方法里面分发就好了。