在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

WebSocket Test

参考 https://www.w3.org/TR/websockets/ https://developer.mozilla.org/en-US/docs/Web/API/Blob http://www.websocket.org/echo.html http://www.websocket-test.com/ 设计框架 一个通用的网络框架,在通用的前提下还需要能够支持各种项目的差异需求,根据经验,常见的需求差异如下: 用户协议差异,游戏可能传输json、protobuf、flatbuffer或者自定义的二进制协议 底层协议差异,我们可能使用websocket、或者微信小游戏的wx.websocket、甚至在原生平台我们希望使用tcp/udp/kcp等协议 登陆认证流程,在使用长连接之前我们理应进行登陆认证,而不同游戏登陆认证的方式不同 网络异常处理,比如超时时间是多久,超时后的表现是怎样的,请求时是否应该屏蔽UI等待服务器响应,网络断开后表现如何,自动重连还是由玩家点击重连按钮进行重连,重连之后是否重发断网期间的消息?等等这些。 多连接的处理,某些游戏可能需要支持多个不同的连接,一般不会超过2个,比如一个主连接负责处理大厅等业务消息,一个战斗连接直接连战斗服务器,或者连接聊天服务器。 根据上面的这些需求,我们对功能模块进行拆分,尽量保证模块的高内聚,低耦合。 image ProtocolHelper协议处理模块——当我们拿到一块buffer时,我们可能需要知道这个buffer对应的协议或者id是多少,比如我们在请求的时候就传入了响应的处理回调,那么常用的做法可能会用一个自增的id来区别每一个请求,或者是用协议号来区分不同的请求,这些是开发者需要实现的。我们还需要从buffer中获取包的长度是多少?包长的合理范围是多少?心跳包长什么样子等等。 Socket模块——实现最基础的通讯功能,首先定义Socket的接口类ISocket,定义如连接、关闭、数据接收与发送等接口,然后子类继承并实现这些接口。 NetworkTips网络显示模块——实现如连接中、重连中、加载中、网络断开等状态的显示,以及ui的屏蔽。 NetNode网络节点——所谓网络节点,其实主要的职责是将上面的功能串联起来,为用户提供一个易用的接口。 NetManager管理网络节点的单例——我们可能有多个网络节点(多条连接),所以这里使用单例来进行管理,使用单例来操作网络节点也会更加方便。 ProtocolHelper 在这里定义了一个IProtocolHelper的简单接口,如下所示: export type NetData = (string | ArrayBufferLike | Blob | ArrayBufferView); // 协议辅助接口 export interface IProtocolHelper { getHeadlen(): number; // 返回包头长度 getHearbeat(): NetData; // 返回一个心跳包 getPackageLen(msg: NetData): number; // 返回整个包的长度 checkPackage(msg: NetData): boolean; // 检查包数据是否合法 getPackageId(msg: NetData): number; // 返回包的id或协议类型 } Socket 在这里定义了一个ISocket的简单接口,如下所示: // Socket接口 export interface ISocket { onConnected: (event) => void; // 连接回调 onMessage: (msg: NetData) => void; // 消息回调 onError: (event) => void; // 错误回调 onClosed: (event) => void; // 关闭回调 connect(ip: string, port: number); // 连接接口 send(buffer: NetData); // 数据发送接口 close(code?: number, reason?: string); // 关闭接口 } 接下来我们实现一个WebSock,继承于ISocket,我们只需要实现connect、send和close接口即可。send和close都是对websocket对简单封装,connect则需要根据传入的ip、端口等参数构造一个url来创建websocket,并绑定websocket的回调。 export class WebSock implements ISocket { private _ws: WebSocket = null; // websocket对象 onConnected: (event) => void = null; onMessage: (msg) => void = null; onError: (event) => void = null; onClosed: (event) => void = null; connect(options: any) { if (this._ws) { if (this._ws.readyState === WebSocket.CONNECTING) { console.log("websocket connecting, wait for a moment...") return false; } } let url = null; if(options.url) { url = options.url; } else { let ip = options.ip; let port = options.port; let protocol = options.protocol; url = `${protocol}://${ip}:${port}`; } this._ws = new WebSocket(url); this._ws.binaryType = options.binaryType ? options.binaryType : "arraybuffer"; this._ws.onmessage = (event) => { this.onMessage(event.data); }; this._ws.onopen = this.onConnected; this._ws.onerror = this.onError; this._ws.onclose = this.onClosed; return true; } send(buffer: NetData) { if (this._ws.readyState == WebSocket.OPEN) { this._ws.send(buffer); return true; } return false; } close(code?: number, reason?: string) { this._ws.close(); } } NetworkTips INetworkTips提供了非常的接口,重连和请求的开关,框架会在合适的时机调用它们,我们可以继承INetworkTips并定制我们的网络相关提示信息,需要注意的是这些接口可能会被多次调用。 // 网络提示接口 export interface INetworkTips { connectTips(isShow: boolean): void; reconnectTips(isShow: boolean): void; requestTips(isShow: boolean): void; } NetNode NetNode是整个网络框架中最为关键的部分,一个NetNode实例表示一个完整的连接对象,基于NetNode我们可以方便地进行扩展,它的主要职责有: 连接维护 连接的建立与鉴权(是否鉴权、如何鉴权由用户的回调决定) 断线重连后的数据重发处理 心跳机制确保连接有效(心跳包间隔由配置,心跳包的内容由ProtocolHelper定义) 连接的关闭 数据发送 支持断线重传,超时重传 支持唯一发送(避免同一时间重复发送) 数据接收 支持持续监听 支持request-respone模式 界面展示 可自定义网络延迟、短线重连等状态的表现 以下是NetNode的完整代码: export enum NetTipsType { Connecting, ReConnecting, Requesting, } export enum NetNodeState { Closed, // 已关闭 Connecting, // 连接中 Checking, // 验证中 Working, // 可传输数据 } export interface NetConnectOptions { host?: string, // 地址 port?: number, // 端口 url?: string, // url,与地址+端口二选一 autoReconnect?: number, // -1 永久重连,0不自动重连,其他正整数为自动重试次数 } export class NetNode { protected _connectOptions: NetConnectOptions = null; protected _autoReconnect: number = 0; protected _isSocketInit: boolean = false; // Socket是否初始化过 protected _isSocketOpen: boolean = false; // Socket是否连接成功过 protected _state: NetNodeState = NetNodeState.Closed; // 节点当前状态 protected _socket: ISocket = null; // Socket对象(可能是原生socket、websocket、wx.socket...) protected _networkTips: INetworkTips = null; // 网络提示ui对象(请求提示、断线重连提示等) protected _protocolHelper: IProtocolHelper = null; // 包解析对象 protected _connectedCallback: CheckFunc = null; // 连接完成回调 protected _disconnectCallback: BoolFunc = null; // 断线回调 protected _callbackExecuter: ExecuterFunc = null; // 回调执行 protected _keepAliveTimer: any = null; // 心跳定时器 protected _receiveMsgTimer: any = null; // 接收数据定时器 protected _reconnectTimer: any = null; // 重连定时器 protected _heartTime: number = 10000; // 心跳间隔 protected _receiveTime: number = 6000000; // 多久没收到数据断开 protected _reconnetTimeOut: number = 8000000; // 重连间隔 protected _requests: RequestObject[] = Array(); // 请求列表 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); // 优