Cocos Creator 通用框架设计 —— 网络
在Creator中发起一个http请求是比较简单的,但很多游戏希望能够和服务器之间保持长连接,以便服务端能够主动向客户端推送消息,而非总是由客户端发起请求,对于实时性要求较高的游戏更是如此。这里我们会设计一个通用的网络框架,可以方便地应用于我们的项目中。
使用websocket
在实现这个网络框架之前,我们先了解一下websocket,websocket是一种基于tcp的全双工网络协议,可以让网页创建持久性的连接,进行双向的通讯。在Cocos Creator中使用websocket既可以用于h5网页游戏上,同样支持原生平台Android和iOS。
构造websocket对象
在使用websocket时,第一步应该创建一个websocket对象,websocket对象的构造函数可以传入2个参数,第一个是url字符串,第二个是协议字符串或字符串数组,指定了可接受的子协议,服务端需要选择其中的一个返回,才会建立连接,但我们一般用不到。
url参数非常重要,主要分为4部分协议://地址:端口/资源,比如ws://echo.websocket.org:
协议:必选项,默认是ws协议,如果需要安全加密则使用wss。
地址:必选项,可以是ip或域名,当然建议使用域名。
端口:可选项,在不指定的情况下,ws的默认端口为80,wss的默认端口为443。
资源:可选性,一般是跟在域名后某资源路径,我们基本不需要它。
websocket的状态
websocket有4个状态,可以通过readyState属性查询:
0 CONNECTING 尚未建立连接。
1 OPEN WebSocket连接已建立,可以进行通信。
2 CLOSING 连接正在进行关闭握手,或者该close()方法已被调用。
3 CLOSED 连接已关闭。
websocket的API
websocket只有2个API,void send( data ) 发送数据和void close( code, reason ) 关闭连接。
send方法只接收一个参数——即要发送的数据,类型可以是以下4个类型的任意一种string | ArrayBufferLike | Blob | ArrayBufferView。
如果要发送的数据是二进制,我们可以通过websocket对象的binaryType属性来指定二进制的类型,binaryType只可以被设置为“blob”或“arraybuffer”,默认为“blob”。如果我们要传输的是文件这样较为固定的、用于写入到磁盘的数据,使用blob。而你希望传输的对象在内存中进行处理则使用较为灵活的arraybuffer。如果要从其他非blob对象和数据构造一个blob,需要使用Blob的构造函数。
在发送数据时官方有2个建议:
检测websocket对象的readyState是否为OPEN,是才进行send。
检测websocket对象的bufferedAmount是否为0,是才进行send(为了避免消息堆积,该属性表示调用send后堆积在websocket缓冲区的还未真正发送出去的数据长度)。
close方法接收2个可选的参数,code表示错误码,我们应该传入1000或3000~4999之间的整数,reason可以用于表示关闭的原因,长度不可超过123字节。
websocket的回调
websocket提供了4个回调函数供我们绑定:
onopen:连接成功后调用。
onmessage:有消息过来时调用:传入的对象有data属性,可能是字符串、blob或arraybuffer。
onerror:出现网络错误时调用:传入的对象有data属性,通常是错误描述的字符串。
onclose:连接关闭时调用:传入的对象有code、reason、wasClean等属性。
注意:当网络出错时,会先调用onerror再调用onclose,无论何种原因的连接关闭,onclose都会被调用。
Echo实例
下面websocket官网的echo demo的代码,可以将其写入一个html文件中并用浏览器打开,打开后会自动创建websocket连接,在连接上时主动发送了一条消息“WebSocket rocks”,服务器会将该消息返回,触发onMessage,将信息打印到屏幕上,然后关闭连接。具体可以参考 http://www.websocket.org/echo.html 。
默认的url前缀是wss,由于wss抽风,使用ws才可以连接上,如果ws也抽风,可以试试连这个地址ws://121.40.165.18:8800,这是国内的一个免费测试websocket的网址。
WebSocket Test
(); // 请求列表
protected _listener: { [key: number]: CallbackObject[] } = {} // 监听者列表
/********************** 网络相关处理 *********************/
public init(socket: ISocket, protocol: IProtocolHelper, networkTips: any = null, execFunc : ExecuterFunc = null) {
console.log(`NetNode init socket`);
this._socket = socket;
this._protocolHelper = protocol;
this._networkTips = networkTips;
this._callbackExecuter = execFunc ? execFunc : (callback: CallbackObject, buffer: NetData) => {
callback.callback.call(callback.target, 0, buffer);
}
}
public connect(options: NetConnectOptions): boolean {
if (this._socket && this._state == NetNodeState.Closed) {
if (!this._isSocketInit) {
this.initSocket();
}
this._state = NetNodeState.Connecting;
if (!this._socket.connect(options)) {
this.updateNetTips(NetTipsType.Connecting, false);
return false;
}
if (this._connectOptions == null) {
options.autoReconnect = options.autoReconnect;
}
this._connectOptions = options;
this.updateNetTips(NetTipsType.Connecting, true);
return true;
}
return false;
}
protected initSocket() {
this._socket.onConnected = (event) => { this.onConnected(event) };
this._socket.onMessage = (msg) => { this.onMessage(msg) };
this._socket.onError = (event) => { this.onError(event) };
this._socket.onClosed = (event) => { this.onClosed(event) };
this._isSocketInit = true;
}
protected updateNetTips(tipsType: NetTipsType, isShow: boolean) {
if (this._networkTips) {
if (tipsType == NetTipsType.Requesting) {
this._networkTips.requestTips(isShow);
} else if (tipsType == NetTipsType.Connecting) {
this._networkTips.connectTips(isShow);
} else if (tipsType == NetTipsType.ReConnecting) {
this._networkTips.reconnectTips(isShow);
}
}
}
// 网络连接成功
protected onConnected(event) {
console.log("NetNode onConnected!")
this._isSocketOpen = true;
// 如果设置了鉴权回调,在连接完成后进入鉴权阶段,等待鉴权结束
if (this._connectedCallback !== null) {
this._state = NetNodeState.Checking;
this._connectedCallback(() => { this.onChecked() });
} else {
this.onChecked();
}
console.log("NetNode onConnected! state =" + this._state);
}
// 连接验证成功,进入工作状态
protected onChecked() {
console.log("NetNode onChecked!")
this._state = NetNodeState.Working;
// 关闭连接或重连中的状态显示
this.updateNetTips(NetTipsType.Connecting, false);
this.updateNetTips(NetTipsType.ReConnecting, false);
// 重发待发送信息
console.log(`NetNode flush ${this._requests.length} request`)
if (this._requests.length > 0) {
for (var i = 0; i < this._requests.length;) {
let req = this._requests[i];
this._socket.send(req.buffer);
if (req.rspObject == null || req.rspCmd <= 0) {
this._requests.splice(i, 1);
} else {
++i;
}
}
// 如果还有等待返回的请求,启动网络请求层
this.updateNetTips(NetTipsType.Requesting, this.request.length > 0);
}
}
// 接收到一个完整的消息包
protected onMessage(msg): void {
// console.log(`NetNode onMessage status = ` + this._state);
// 进行头部的校验(实际包长与头部长度是否匹配)
if (!this._protocolHelper.check P a c ka ge(msg)) {
console.error(`NetNode checkHead Error`);
return;
}
// 接受到数据,重新定时收数据计时器
this.resetReceiveMsgTimer();
// 重置心跳包发送器
this.resetHearbeatTimer();
// 触发消息执行
let rspCmd = this._protocolHelper.getPackageId(msg);
console.log(`NetNode onMessage rspCmd = ` + rspCmd);
// 优