在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言在 Cocos Creator 中发起一个 http 请求是比较简单的,但很多游戏希望能够和服务器之间保持长连接,以便服务端能够主动向客户端推送消息,而非总是由客户端发起请求,对于实时性要求较高的游戏更是如此。这里我们会设计一个通用的网络框架,可以方便地应用于我们的项目中。 使用websocket在实现这个网络框架之前,我们先了解一下 websocket。websocket 是一种基于 tcp 的全双工网络协议,可以让网页创建持久性的连接,进行双向的通讯。在 Cocos Creator 中使用 websocket 既可以用于 H5 网页游戏上,同样支持原生平台 Android 和 iOS。 构造 websocket 对象在使用 websocket 时,第一步应该创建一个 websocket 对象。websocket 对象的构造函数可以传入2个参数,第一个是 url 字符串,第二个是协议字符串或字符串数组,指定了可接受的子协议,服务端需要选择其中的一个返回,才会建立连接,但我们一般用不到。 url 参数非常重要,主要分为4部分:协议、地址、端口、资源。 比如 ws://echo.websocket.org:
websocket 的状态websocket 有4个状态,可以通过 readyState 属性查询:
websocket 的 APIwebsocket 只有2个 API,void send( data ) 发送数据和 void close( code, reason ) 关闭连接。 send 方法只接收一个参数——即要发送的数据,类型可以是以下4个类型的任意一种:string | ArrayBufferLike | Blob | ArrayBufferView。
在发送数据时,官方有2个建议:
close 方法接收2个可选的参数,code 表示错误码,我们应该传入 1000 或 3000~4999 之间的整数,reason 可以用于表示关闭的原因,长度不可超过 123 字节。 websocket 的回调websocket 提供了4个回调函数供我们绑定:
Echo 实例下面 websocket 官网的 echo demo 的代码,可以将其写入一个 html 文件中并用浏览器打开,打开后会自动创建 websocket 连接,在连接上时主动发送了一条消息“WebSocket rocks”,服务器会将该消息返回,触发 onMessage,将信息打印到屏幕上,然后关闭连接。具体可以参考:http://www.websocket.org/echo.html17
设计框架一个通用的网络框架,在通用的前提下还需要能够支持各种项目的差异需求,根据经验,常见的需求差异如下:
根据上面的这些需求,我们对功能模块进行拆分,尽量保证模块的高内聚,低耦合。 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(); } } NetworkTipsINetworkTips 提供了非常的接口,重连和请求的开关,框架会在合适的时机调用它们,我们可以继承 INetworkTips 并定制我们的网络相关提示信息,需要注意的是这些接口可能会被**多次调用**。 // 网络提示接口 export interface INetworkTips { connectTips(isShow: boolean): void; reconnectTips(isShow: boolean): void; requestTips(isShow: boolean): void; } NetNodeNetNode 是整个网络框架中最为关键的部分,一个 NetNode 实例表示一个完整的连接对象,基于 NetNode 我们可以方便地进行扩展,它的主要职责有: 连接维护
数据发送
数据接收
界面展示
接下来介绍网络相关的成员函数,首先看初始化与:
onConnected 方法在网络连接成功后调用,自动进入鉴权流程(如果设置了_connectedCallback),在鉴权完成后需要调用 onChecked 方法使 NetNode 进入可通讯的状态,在未鉴权的情况,我们不应该发送任何业务请求,但登录验证这类请求应该发送给服务器,这类请求可以通过带force参数强制发送给服务器。 接收到任何消息都会触发 onMessage,首先会对数据包进行校验,校验的规则可以在自己的 ProtocolHelper 中实现,如果是一个合法的数据包,我们会将心跳和超时计时器进行更新——重新计时,最后在 _requests 和 _listener 中找到该消息的处理函数,这里是通过 rspCmd 进行查找的,rspCmd 是从 ProtocolHelper 的 getPackageId 取出的,我们可以将协议的命令或者序号返回,由我们自己来决定请求和响应如何对应。 onError 和 onClosed 是网络出错和关闭时调用的,无论是否出错,最终都会调用 onClosed,在这里我们执行断线回调,以及做自动重连的处理。当然也可以调用 close来关闭套接字。close 与 closeSocket 的区别在于 closeSocket 只是关闭套接字——我仍然要使用当前的 NetNode,可能通过下一次 connect 恢复网络。而 close则是清除所有的状态。 发起网络请求有3种方式: send 方法,纯粹地发送数据,如果当前断网或者验证中会进入 _request 队列。 request 方法,在请求的时候即以闭包的方式传入回调,在该请求的响应回到时会执行回调,如果同时有多个相同的请求,那么这 N 个请求的响应会依次回到客户端,响应回调也会依次执行(每次只会执行一个回调)。 requestUnique 方法,如果我们不希望有多个相同的请求,可以使用 requestUnique 来确保每一种请求同时只会有一个。 这里确保没有重复之所以使用的是遍历 _requests,是因为我们不会积压大量的请求到 _requests中,超时或异常重发也不会导致 _requests 的积压,因为重发的逻辑是由 NetNode 控制的,而且在网络断开的情况下,我们理应屏蔽用户发起请求,此时一般会有一个全屏遮罩——网络出现波动之类的提示。 我们有2种回调,一种是前面的 request 回调,这种回调是临时性的,一般随着请求-响应-执行而立即清理,_listener 回调则是常驻的,需要我们手动管理的,比如打开某界面时监听、离开是关闭,或者在游戏一开始就进行监听。适合处理服务器的主动推送消息。 最后是心跳与超时相关的定时器,我们每隔 _heartTime 会发送一个心跳包,每隔 _receiveTime 检测如果没有收到服务器返回的包,则判断网络断开。 完整代码,大家可以进入源码查看! NetManagerNetManager 用于管理 NetNode,这是由于我们可能需要支持多个不同的连接对象,所以需要一个 NetManager 专门来管理 NetNode,同时,NetManager 作为一个单例,也可以方便我们调用网络。 export class NetManager { private static _instance: NetManager = null; protected _channels: { [key: number]: NetNode } = {}; public static getInstance(): NetManager { if (this._instance == null) { this._instance = new NetManager(); } return this._instance; } // 添加Node,返回ChannelID public setNetNode(newNode: NetNode, channelId: number = 0) { this._channels[channelId] = newNode; } // 移除Node public removeNetNode(channelId: number) { delete this._channels[channelId]; } // 调用Node连接 public connect(options: NetConnectOptions, channelId: number = 0): boolean { if (this._channels[channelId]) { return this._channels[channelId].connect(options); } return false; } // 调用Node发送 public send(buf: NetData, force: boolean = false, channelId: number = 0): boolean { let node = this._channels[channelId]; if (node) { return node.send(buf, force); } return false; } // 发起请求,并在在结果返回时调用指定好的回调函数 public request(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0) { let node = this._channels[channelId]; if (node) { node.request(buf, rspCmd, rspObject, showTips, force); } } // 同request,但在request之前会先判断队列中是否已有rspCmd,如有重复的则直接返回 public requestUnique(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0): boolean { let node = this._channels[channelId]; if (node) { return node.requestUnique(buf, rspCmd, rspObject, showTips, force); } return false; } // 调用Node关闭 public close(code ? : number, reason ? : string, channelId: number = 0) { if (this._channels[channelId]) { return this._channels[channelId].closeSocket(code, reason); } } 测试例子接下来我们用一个简单的例子来演示一下网络框架的基本使用,首先我们需要拼一个简单的界面用于展示,3个按钮(连接、发送、关闭),2个输入框(输入 url、输入要发送的内容),一个文本框(显示从服务器接收到的数据),如下图所示。
接下来,实现一个简单的 Component,这里新建了一个 NetExample.ts 文件,做的事情非常简单,在初始化的时候创建 NetNode、绑定默认接收回调,在接收回调中将服务器返回的文本显示到 msgLabel中。接着是连接、发送和关闭几个接口的实现: // 不关键的代码省略 @ccclassexport default class NetExample extends cc.Component { @property(cc.Label) textLabel: cc.Label = null; @property(cc.Label) urlLabel: cc.Label = null; @property(cc.RichText) msgLabel: cc.RichText = null; private lineCount: number = 0; onLoad() { let Node = new NetNode(); Node.init(new WebSock(), new DefStringProtocol()); Node.setResponeHandler(0, (cmd: number, data: NetData) => { if (this.lineCount > 5) { let idx = this.msgLabel.string.search("\n"); this.msgLabel.string = this.msgLabel.string.substr(idx + 1); } this.msgLabel.string += `${data}\n`; ++this.lineCount; }); NetManager.getInstance().setNetNode(Node); } onConnectClick() { NetManager.getInstance().connect({ url: this.urlLabel.string }); } onSendClick() { NetManager.getInstance().send(this.textLabel.string); } onDisconnectClick() { NetManager.getInstance().close(); } } 代码完成后,将其挂载到场景的 Canvas 节点下(其他节点也可以),然后将场景中的 Label 和 RichText 拖拽到我们的 NetExample 的属性面板中: 运行效果如下所示: 小结可以看到,Websocket 的使用很简单,我们在开发的过程中会碰到各种各样的需求和问题,要实现一个好的设计,快速地解决问题。 我们一方面需要对我们使用的技术本身有深入的理解,websocket 的底层协议传输是如何实现的?与 tcp、http 的区别在哪里?基于 websocket 能否使用 udp 进行传输呢?使用 websocket 发送数据是否需要自己对数据流进行分包(websocket 协议保证了包的完整)?数据的发送是否出现了发送缓存的堆积(查看 bufferedAmount)? 另外需要对我们的使用场景及需求本身的理解,对需求的理解越透彻,越能做出好的设计。哪些需求是项目相关的,哪些需求是通用的?通用的需求是必须的还是可选的?不同的变化我们应该封装成类或接口,使用多态的方式来实现呢?还是提供配置?回调绑定?事件通知? 我们需要设计出一个好的框架,来适用于下一个项目,并且在一个一个的项目中优化迭代,这样才能建立深厚的沉淀、提高效率。 以上就是CocosCreator通用框架设计之网络的详细内容,更多关于CocosCreator框架设计之网络的资料请关注极客世界其它相关文章! |
请发表评论