在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的网址。

  1. <!DOCTYPE html>
  2. <meta charset="utf-8" />
  3. <title>WebSocket Test</title>
  4. <script language="javascript" type="text/javascript">
  5. var wsUri = "ws://echo.websocket.org/";
  6. var output;
  7. function init() {
  8. output = document.getElementById("output");
  9. testWebSocket();
  10. }
  11. function testWebSocket() {
  12. // 初始化websocket,绑定回调
  13. websocket = new WebSocket(wsUri);
  14. websocket.onopen = onOpen;
  15. websocket.onclose = onClose;
  16. websocket.onmessage = onMessage;
  17. websocket.onerror = onError;
  18. }
  19. function onOpen(evt) {
  20. writeToScreen("CONNECTED");
  21. doSend("WebSocket rocks");
  22. }
  23. function onClose(evt) {
  24. writeToScreen("DISCONNECTED");
  25. }
  26. function onMessage(evt) {
  27. writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
  28. websocket.close();
  29. }
  30. function onError(evt) {
  31. writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  32. }
  33. function doSend(message) {
  34. writeToScreen("SENT: " + message);
  35. websocket.send(message);
  36. }
  37. function writeToScreen(message) {
  38. var pre = document.createElement("p");
  39. pre.style.wordWrap = "break-word";
  40. pre.innerHTML = message;
  41. output.appendChild(pre);
  42. }
  43. // 加载时调用init方法,初始化websocket
  44. window.addEventListener("load", init, false);
  45. </script>
  46. <h2>WebSocket Test</h2>
  47. <div id="output"></div>

参考

设计框架

一个通用的网络框架,在通用的前提下还需要能够支持各种项目的差异需求,根据经验,常见的需求差异如下:

  • 用户协议差异,游戏可能传输json、protobuf、flatbuffer或者自定义的二进制协议
  • 底层协议差异,我们可能使用websocket、或者微信小游戏的wx.websocket、甚至在原生平台我们希望使用tcp/udp/kcp等协议
  • 登陆认证流程,在使用长连接之前我们理应进行登陆认证,而不同游戏登陆认证的方式不同
  • 网络异常处理,比如超时时间是多久,超时后的表现是怎样的,请求时是否应该屏蔽UI等待服务器响应,网络断开后表现如何,自动重连还是由玩家点击重连按钮进行重连,重连之后是否重发断网期间的消息?等等这些。
  • 多连接的处理,某些游戏可能需要支持多个不同的连接,一般不会超过2个,比如一个主连接负责处理大厅等业务消息,一个战斗连接直接连战斗服务器,或者连接聊天服务器。

根据上面的这些需求,我们对功能模块进行拆分,尽量保证模块的高内聚,低耦合。

  • ProtocolHelper协议处理模块——当我们拿到一块buffer时,我们可能需要知道这个buffer对应的协议或者id是多少,比如我们在请求的时候就传入了响应的处理回调,那么常用的做法可能会用一个自增的id来区别每一个请求,或者是用协议号来区分不同的请求,这些是开发者需要实现的。我们还需要从buffer中获取包的长度是多少?包长的合理范围是多少?心跳包长什么样子等等。
  • Socket模块——实现最基础的通讯功能,首先定义Socket的接口类ISocket,定义如连接、关闭、数据接收与发送等接口,然后子类继承并实现这些接口。
  • NetworkTips网络显示模块——实现如连接中、重连中、加载中、网络断开等状态的显示,以及ui的屏蔽。
  • NetNode网络节点——所谓网络节点,其实主要的职责是将上面的功能串联起来,为用户提供一个易用的接口。
  • NetManager管理网络节点的单例——我们可能有多个网络节点(多条连接),所以这里使用单例来进行管理,使用单例来操作网络节点也会更加方便。

ProtocolHelper

在这里定义了一个IProtocolHelper的简单接口,如下所示:

  1. export type NetData = (string | ArrayBufferLike | Blob | ArrayBufferView);
  2. // 协议辅助接口
  3. export interface IProtocolHelper {
  4. getHeadlen(): number; // 返回包头长度
  5. getHearbeat(): NetData; // 返回一个心跳包
  6. getPackageLen(msg: NetData): number; // 返回整个包的长度
  7. checkPackage(msg: NetData): boolean; // 检查包数据是否合法
  8. getPackageId(msg: NetData): number; // 返回包的id或协议类型
  9. }

Socket

在这里定义了一个ISocket的简单接口,如下所示:

  1. // Socket接口
  2. export interface ISocket {
  3. onConnected: (event) => void; // 连接回调
  4. onMessage: (msg: NetData) => void; // 消息回调
  5. onError: (event) => void; // 错误回调
  6. onClosed: (event) => void; // 关闭回调
  7. connect(ip: string, port: number); // 连接接口
  8. send(buffer: NetData); // 数据发送接口
  9. close(code?: number, reason?: string); // 关闭接口
  10. }

接下来我们实现一个WebSock,继承于ISocket,我们只需要实现connect、send和close接口即可。send和close都是对websocket对简单封装,connect则需要根据传入的ip、端口等参数构造一个url来创建websocket,并绑定websocket的回调。

  1. export class WebSock implements ISocket {
  2. private _ws: WebSocket = null; // websocket对象
  3. onConnected: (event) => void = null;
  4. onMessage: (msg) => void = null;
  5. onError: (event) => void = null;
  6. onClosed: (event) => void = null;
  7. connect(options: any) {
  8. if (this._ws) {
  9. if (this._ws.readyState === WebSocket.CONNECTING) {
  10. console.log("websocket connecting, wait for a moment...")
  11. return false;
  12. }
  13. }
  14. let url = null;
  15. if(options.url) {
  16. url = options.url;
  17. } else {
  18. let ip = options.ip;
  19. let port = options.port;
  20. let protocol = options.protocol;
  21. url = `${protocol}://${ip}:${port}`;
  22. }
  23. this._ws = new WebSocket(url);
  24. this._ws.binaryType = options.binaryType ? options.binaryType : "arraybuffer";
  25. this._ws.onmessage = (event) => {
  26. this.onMessage(event.data);
  27. };
  28. this._ws.onopen = this.onConnected;
  29. this._ws.onerror = this.onError;
  30. this._ws.onclose = this.onClosed;
  31. return true;
  32. }
  33. send(buffer: NetData) {
  34. if (this._ws.readyState == WebSocket.OPEN)
  35. {
  36. this._ws.send(buffer);
  37. return true;
  38. }
  39. return false;
  40. }
  41. close(code?: number, reason?: string) {
  42. this._ws.close();
  43. }
  44. }

NetworkTips

INetworkTips提供了非常的接口,重连和请求的开关,框架会在合适的时机调用它们,我们可以继承INetworkTips并定制我们的网络相关提示信息,需要注意的是这些接口可能会被多次调用

  1. // 网络提示接口
  2. export interface INetworkTips {
  3. connectTips(isShow: boolean): void;
  4. reconnectTips(isShow: boolean): void;
  5. requestTips(isShow: boolean): void;
  6. }

NetNode

NetNode是整个网络框架中最为关键的部分,一个NetNode实例表示一个完整的连接对象,基于NetNode我们可以方便地进行扩展,它的主要职责有:

  • 连接维护

    • 连接的建立与鉴权(是否鉴权、如何鉴权由用户的回调决定)
    • 断线重连后的数据重发处理
    • 心跳机制确保连接有效(心跳包间隔由配置,心跳包的内容由ProtocolHelper定义)
    • 连接的关闭
  • 数据发送
    • 支持断线重传,超时重传
    • 支持唯一发送(避免同一时间重复发送)
  • 数据接收
    • 支持持续监听
    • 支持request-respone模式
  • 界面展示
    • 可自定义网络延迟、短线重连等状态的表现

以下是NetNode的完整代码:

  1. export enum NetTipsType {
  2. Connecting,
  3. ReConnecting,
  4. Requesting,
  5. }
  6. export enum NetNodeState {
  7. Closed, // 已关闭
  8. Connecting, // 连接中
  9. Checking, // 验证中
  10. Working, // 可传输数据
  11. }
  12. export interface NetConnectOptions {
  13. host?: string, // 地址
  14. port?: number, // 端口
  15. url?: string, // url,与地址+端口二选一
  16. autoReconnect?: number, // -1 永久重连,0不自动重连,其他正整数为自动重试次数
  17. }
  18. export class NetNode {
  19. protected _connectOptions: NetConnectOptions = null;
  20. protected _autoReconnect: number = 0;
  21. protected _isSocketInit: boolean = false; // Socket是否初始化过
  22. protected _isSocketOpen: boolean = false; // Socket是否连接成功过
  23. protected _state: NetNodeState = NetNodeState.Closed; // 节点当前状态
  24. protected _socket: ISocket = null; // Socket对象(可能是原生socket、websocket、wx.socket...)
  25. protected _networkTips: INetworkTips = null; // 网络提示ui对象(请求提示、断线重连提示等)
  26. protected _protocolHelper: IProtocolHelper = null; // 包解析对象
  27. protected _connectedCallback: CheckFunc = null; // 连接完成回调
  28. protected _disconnectCallback: BoolFunc = null; // 断线回调
  29. protected _callbackExecuter: ExecuterFunc = null; // 回调执行
  30. protected _keepAliveTimer: any = null; // 心跳定时器
  31. protected _receiveMsgTimer: any = null; // 接收数据定时器
  32. protected _reconnectTimer: any = null; // 重连定时器
  33. protected _heartTime: number = 10000; // 心跳间隔
  34. protected _receiveTime: number = 6000000; // 多久没收到数据断开
  35. protected _reconnetTimeOut: number = 8000000; // 重连间隔
  36. protected _requests: RequestObject[] = Array<RequestObject>(); // 请求列表
  37. protected _listener: { [key: number]: CallbackObject[] } = {} // 监听者列表
  38. /********************** 网络相关处理 *********************/
  39. public init(socket: ISocket, protocol: IProtocolHelper, networkTips: any = null, execFunc : ExecuterFunc = null) {
  40. console.log(`NetNode init socket`);
  41. this._socket = socket;
  42. this._protocolHelper = protocol;
  43. this._networkTips = networkTips;
  44. this._callbackExecuter = execFunc ? execFunc : (callback: CallbackObject, buffer: NetData) => {
  45. callback.callback.call(callback.target, 0, buffer);
  46. }
  47. }
  48. public connect(options: NetConnectOptions): boolean {
  49. if (this._socket && this._state == NetNodeState.Closed) {
  50. if (!this._isSocketInit) {
  51. this.initSocket();
  52. }
  53. this._state = NetNodeState.Connecting;
  54. if (!this._socket.connect(options)) {
  55. this.updateNetTips(NetTipsType.Connecting, false);
  56. return false;
  57. }
  58. if (this._connectOptions == null) {
  59. options.autoReconnect = options.autoReconnect;
  60. }
  61. this._connectOptions = options;
  62. this.updateNetTips(NetTipsType.Connecting, true);
  63. return true;
  64. }
  65. return false;
  66. }
  67. protected initSocket() {
  68. this._socket.onConnected = (event) => { this.onConnected(event) };
  69. this._socket.onMessage = (msg) => { this.onMessage(msg) };
  70. this._socket.onError = (event) => { this.onError(event) };
  71. this._socket.onClosed = (event) => { this.onClosed(event) };
  72. this._isSocketInit = true;
  73. }
  74. protected updateNetTips(tipsType: NetTipsType, isShow: boolean) {
  75. if (this._networkTips) {
  76. if (tipsType == NetTipsType.Requesting) {
  77. this._networkTips.requestTips(isShow);
  78. } else if (tipsType == NetTipsType.Connecting) {
  79. this._networkTips.connectTips(isShow);
  80. } else if (tipsType == NetTipsType.ReConnecting) {
  81. this._networkTips.reconnectTips(isShow);
  82. }
  83. }
  84. }
  85. // 网络连接成功
  86. protected onConnected(event) {
  87. console.log("NetNode onConnected!")
  88. this._isSocketOpen = true;
  89. // 如果设置了鉴权回调,在连接完成后进入鉴权阶段,等待鉴权结束
  90. if (this._connectedCallback !== null) {
  91. this._state = NetNodeState.Checking;
  92. this._connectedCallback(() => { this.onChecked() });
  93. } else {
  94. this.onChecked();
  95. }
  96. console.log("NetNode onConnected! state =" + this._state);
  97. }
  98. // 连接验证成功,进入工作状态
  99. protected onChecked() {
  100. console.log("NetNode onChecked!")
  101. this._state = NetNodeState.Working;
  102. // 关闭连接或重连中的状态显示
  103. this.updateNetTips(NetTipsType.Connecting, false);
  104. this.updateNetTips(NetTipsType.ReConnecting, false);
  105. // 重发待发送信息
  106. console.log(`NetNode flush ${this._requests.length} request`)
  107. if (this._requests.length > 0) {
  108. for (var i = 0; i < this._requests.length;) {
  109. let req = this._requests[i];
  110. this._socket.send(req.buffer);
  111. if (req.rspObject == null || req.rspCmd <= 0) {
  112. this._requests.splice(i, 1);
  113. } else {
  114. ++i;
  115. }
  116. }
  117. // 如果还有等待返回的请求,启动网络请求层
  118. this.updateNetTips(NetTipsType.Requesting, this.request.length > 0);
  119. }
  120. }
  121. // 接收到一个完整的消息包
  122. protected onMessage(msg): void {
  123. // console.log(`NetNode onMessage status = ` + this._state);
  124. // 进行头部的校验(实际包长与头部长度是否匹配)
  125. if (!this._protocolHelper.check P a c ka ge(msg)) {
  126. console.error(`NetNode checkHead Error`);
  127. return;
  128. }
  129. // 接受到数据,重新定时收数据计时器
  130. this.resetReceiveMsgTimer();
  131. // 重置心跳包发送器
  132. this.resetHearbeatTimer();
  133. // 触发消息执行
  134. let rspCmd = this._protocolHelper.getPackageId(msg);
  135. console.log(`NetNode onMessage rspCmd = ` + rspCmd);
  136. // 优先触发request队列
  137. if (this._requests.length > 0) {
  138. for (let reqIdx in this._requests) {
  139. let req = this._requests[reqIdx];
  140. if (req.rspCmd == rspCmd) {
  141. console.log(`NetNode execute request rspcmd ${rspCmd}`);
  142. this._callbackExecuter(req.rspObject, msg);
  143. this._requests.splice(parseInt(reqIdx), 1);
  144. break;
  145. }
  146. }
  147. console.log(`NetNode still has ${this._requests.length} request watting`);
  148. if (this._requests.length == 0) {
  149. this.updateNetTips(NetTipsType.Requesting, false);
  150. }
  151. }
  152. let listeners = this._listener[rspCmd];
  153. if (null != listeners) {
  154. for (const rsp of listeners) {
  155. console.log(`NetNode execute listener cmd ${rspCmd}`);
  156. this._callbackExecuter(rsp, msg);
  157. }
  158. }
  159. }
  160. protected onError(event) {
  161. console.error(event);
  162. }
  163. protected onClosed(event) {
  164. this.clearTimer();
  165. // 执行断线回调,返回false表示不进行重连
  166. if (this._disconnectCallback && !this._disconnectCallback()) {
  167. console.log(`disconnect return!`)
  168. return;
  169. }
  170. // 自动重连
  171. if (this.isAutoReconnect()) {
  172. this.updateNetTips(NetTipsType.ReConnecting, true);
  173. this._reconnectTimer = setTimeout(() => {
  174. this._socket.close();
  175. this._state = NetNodeState.Closed;
  176. this.connect(this._connectOptions);
  177. if (this._autoReconnect > 0) {
  178. this._autoReconnect -= 1;
  179. }
  180. }, this._reconnetTimeOut);
  181. } else {
  182. this._state = NetNodeState.Closed;
  183. }
  184. }
  185. public close(code?: number, reason?: string) {
  186. this.clearTimer();
  187. this._listener = {};
  188. this._requests.length = 0;
  189. if (this._networkTips) {
  190. this._networkTips.connectTips(false);
  191. this._networkTips.reconnectTips(false);
  192. this._networkTips.requestTips(false);
  193. }
  194. if (this._socket) {
  195. this._socket.close(code, reason);
  196. } else {
  197. this._state = NetNodeState.Closed;
  198. }
  199. }
  200. // 只是关闭Socket套接字(仍然重用缓存与当前状态)
  201. public closeSocket(code?: number, reason?: string) {
  202. if (this._socket) {
  203. this._socket.close(code, reason);
  204. }
  205. }
  206. // 发起请求,如果当前处于重连中,进入缓存列表等待重连完成后发送
  207. public send(buf: NetData, force: boolean = false): boolean {
  208. if (this._state == NetNodeState.Working || force) {
  209. console.log(`socket send ...`);
  210. return this._socket.send(buf);
  211. } else if (this._state == NetNodeState.Checking ||
  212. this._state == NetNodeState.Connecting) {
  213. this._requests.push({
  214. buffer: buf,
  215. rspCmd: 0,
  216. rspObject: null
  217. });
  218. console.log("NetNode socket is busy, push to send buffer, current state is " + this._state);
  219. return true;
  220. } else {
  221. console.error("NetNode request error! current state is " + this._state);
  222. return false;
  223. }
  224. }
  225. // 发起请求,并进入缓存列表
  226. public request(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false) {
  227. if (this._state == NetNodeState.Working || force) {
  228. this._socket.send(buf);
  229. }
  230. console.log(`NetNode request with timeout for ${rspCmd}`);
  231. // 进入发送缓存列表
  232. this._requests.push({
  233. buffer: buf, rspCmd, rspObject
  234. });
  235. // 启动网络请求层
  236. if (showTips) {
  237. this.updateNetTips(NetTipsType.Requesting, true);
  238. }
  239. }
  240. // 唯一request,确保没有同一响应的请求(避免一个请求重复发送,netTips界面的屏蔽也是一个好的方法)
  241. public requestUnique(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false): boolean {
  242. for (let i = 0; i < this._requests.length; ++i) {
  243. if (this._requests[i].rspCmd == rspCmd) {
  244. console.log(`NetNode requestUnique faile for ${rspCmd}`);
  245. return false;
  246. }
  247. }
  248. this.request(buf, rspCmd, rspObject, showTips, force);
  249. return true;
  250. }
  251. /********************** 回调相关处理 *********************/
  252. public setResponeHandler(cmd: number, callback: NetCallFunc, target?: any): boolean {
  253. if (callback == null) {
  254. console.error(`NetNode setResponeHandler error ${cmd}`);
  255. return false;
  256. }
  257. this._listener[cmd] = [{ target, callback }];
  258. return true;
  259. }
  260. public addResponeHandler(cmd: number, callback: NetCallFunc, target?: any): boolean {
  261. if (callback == null) {
  262. console.error(`NetNode addResponeHandler error ${cmd}`);
  263. return false;
  264. }
  265. let rspObject = { target, callback };
  266. if (null == this._listener[cmd]) {
  267. this._listener[cmd] = [rspObject];
  268. } else {
  269. let index = this.getNetListenersIndex(cmd, rspObject);
  270. if (-1 == index) {
  271. this._listener[cmd].push(rspObject);
  272. }
  273. }
  274. return true;
  275. }
  276. public removeResponeHandler(cmd: number, callback: NetCallFunc, target?: any) {
  277. if (null != this._listener[cmd] && callback != null) {
  278. let index = this.getNetListenersIndex(cmd, { target, callback });
  279. if (-1 != index) {
  280. this._listener[cmd].splice(index, 1);
  281. }
  282. }
  283. }
  284. public cleanListeners(cmd: number = -1) {
  285. if (cmd == -1) {
  286. this._listener = {}
  287. } else {
  288. this._listener[cmd] = null;
  289. }
  290. }
  291. protected getNetListenersIndex(cmd: number, rspObject: CallbackObject): number {
  292. let index = -1;
  293. for (let i = 0; i < this._listener[cmd].length; i++) {
  294. let iterator = this._listener[cmd][i];
  295. if (iterator.callback == rspObject.callback
  296. && iterator.target == rspObject.target) {
  297. index = i;
  298. break;
  299. }
  300. }
  301. return index;
  302. }
  303. /********************** 心跳、超时相关处理 *********************/
  304. protected resetReceiveMsgTimer() {
  305. if (this._receiveMsgTimer !== null) {
  306. clearTimeout(this._receiveMsgTimer);
  307. }
  308. this._receiveMsgTimer = setTimeout(() => {
  309. console.warn("NetNode recvieMsgTimer close socket!");
  310. this._socket.close();
  311. }, this._receiveTime);
  312. }
  313. protected resetHearbeatTimer() {
  314. if (this._keepAliveTimer !== null) {
  315. clearTimeout(this._keepAliveTimer);
  316. }
  317. this._keepAliveTimer = setTimeout(() => {
  318. console.log("NetNode keepAliveTimer send Hearbeat")
  319. this.send(this._protocolHelper.getHearbeat());
  320. }, this._heartTime);
  321. }
  322. protected clearTimer() {
  323. if (this._receiveMsgTimer !== null) {
  324. clearTimeout(this._receiveMsgTimer);
  325. }
  326. if (this._keepAliveTimer !== null) {
  327. clearTimeout(this._keepAliveTimer);
  328. }
  329. if (this._reconnectTimer !== null) {
  330. clearTimeout(this._reconnectTimer);
  331. }
  332. }
  333. public isAutoReconnect() {
  334. return this._autoReconnect != 0;
  335. }
  336. public rejectReconnect() {
  337. this._autoReconnect = 0;
  338. this.clearTimer();
  339. }
  340. }

NetManager

NetManager用于管理NetNode,这是由于我们可能需要支持多个不同的连接对象,所以需要一个NetManager专门来管理NetNode,同时,NetManager作为一个单例,也可以方便我们调用网络。

  1. export class NetManager {
  2. private static _instance: NetManager = null;
  3. protected _channels: { [key: number]: NetNode } = {};
  4. public static getInstance(): NetManager {
  5. if (this._instance == null) {
  6. this._instance = new NetManager();
  7. }
  8. return this._instance;
  9. }
  10. // 添加Node,返回ChannelID
  11. public setNetNode(newNode: NetNode, channelId: number = 0) {
  12. this._channels[channelId] = newNode;
  13. }
  14. // 移除Node
  15. public removeNetNode(channelId: number) {
  16. delete this._channels[channelId];
  17. }
  18. // 调用Node连接
  19. public connect(options: NetConnectOptions, channelId: number = 0): boolean {
  20. if (this._channels[channelId]) {
  21. return this._channels[channelId].connect(options);
  22. }
  23. return false;
  24. }
  25. // 调用Node发送
  26. public send(buf: NetData, force: boolean = false, channelId: number = 0): boolean {
  27. let node = this._channels[channelId];
  28. if(node) {
  29. return node.send(buf, force);
  30. }
  31. return false;
  32. }
  33. // 发起请求,并在在结果返回时调用指定好的回调函数
  34. public request(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0) {
  35. let node = this._channels[channelId];
  36. if(node) {
  37. node.request(buf, rspCmd, rspObject, showTips, force);
  38. }
  39. }
  40. // 同request,但在request之前会先判断队列中是否已有rspCmd,如有重复的则直接返回
  41. public requestUnique(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false, channelId: number = 0): boolean {
  42. let node = this._channels[channelId];
  43. if(node) {
  44. return node.requestUnique(buf, rspCmd, rspObject, showTips, force);
  45. }
  46. return false;
  47. }
  48. // 调用Node关闭
  49. public close(code?: number, reason?: string, channelId: number = 0) {
  50. if (this._channels[channelId]) {
  51. return this._channels[channelId].closeSocket(code, reason);
  52. }
  53. }

测试例子

接下来我们用一个简单的例子来演示一下网络框架的基本使用,首先我们需要拼一个简单的界面用于展示,3个按钮(连接、发送、关闭),2个输入框(输入url、输入要发送的内容),一个文本框(显示从服务器接收到的数据),如下图所示。

该例子连接的是websocket官方的echo.websocket.org地址,这个服务器会将我们发送给它的所有消息都原样返回给我们。

接下来,实现一个简单的Component,这里新建了一个NetExample.ts文件,做的事情非常简单,在初始化的时候创建NetNode、绑定默认接收回调,在接收回调中将服务器返回的文本显示到msgLabel中。接着是连接、发送和关闭几个接口的实现:

  1. // 不关键的代码省略
  2. @ccclass
  3. export default class NetExample extends cc.Component {
  4. @property(cc.Label)
  5. textLabel: cc.Label = null;
  6. @property(cc.Label)
  7. urlLabel: cc.Label = null;
  8. @property(cc.RichText)
  9. msgLabel: cc.RichText = null;
  10. private lineCount: number = 0;
  11. onLoad() {
  12. let Node = new NetNode();
  13. Node.init(new WebSock(), new DefStringProtocol());
  14. Node.setResponeHandler(0, (cmd: number, data: NetData) => {
  15. if (this.lineCount > 5) {
  16. let idx = this.msgLabel.string.search("\n");
  17. this.msgLabel.string = this.msgLabel.string.substr(idx + 1);
  18. }
  19. this.msgLabel.string += `${data}\n`;
  20. ++this.lineCount;
  21. });
  22. NetManager.getInstance().setNetNode(Node);
  23. }
  24. onConnectClick() {
  25. NetManager.getInstance().connect({ url: this.urlLabel.string });
  26. }
  27. onSendClick() {
  28. NetManager.getInstance().send(this.textLabel.string);
  29. }
  30. onDisconnectClick() {
  31. NetManager.getInstance().close();
  32. }
  33. }

代码完成后,将其挂载到场景的Canvas节点下(其他节点也可以),然后将场景中的Label和RichText拖拽到我们的NetExample的属性面板中:

运行效果如下所示:

小结

可以看到,Websocket的使用很简单,我们在开发的过程中会碰到各种各样的需求和问题,要实现一个好的设计,快速地解决问题。

我们一方面需要对我们使用的技术本身有深入的理解,websocket的底层协议传输是如何实现的?与tcp、http的区别在哪里?基于websocket能否使用udp进行传输呢?使用websocket发送数据是否需要自己对数据流进行分包(websocket协议保证了包的完整)?数据的发送是否出现了发送缓存的堆积(查看bufferedAmount)?

另外需要对我们的使用场景及需求本身的理解,对需求的理解越透彻,越能做出好的设计。哪些需求是项目相关的,哪些需求是通用的?通用的需求是必须的还是可选的?不同的变化我们应该封装成类或接口,使用多态的方式来实现呢?还是提供配置?回调绑定?事件通知?

我们需要设计出一个好的框架,来适用于下一个项目,并且在一个一个的项目中优化迭代,这样才能建立深厚的沉淀、提高效率。

接下来的一段时间会将之前的一些经验整理为一个开源易用的cocos creator框架:https://github.com/wyb10a10/cocos_creator_framework

Cocos Creator 通用框架设计 —— 网络的更多相关文章

  1. Cocos Creator 通用框架设计 —— 资源管理优化

    接着<Cocos Creator 通用框架设计 -- 资源管理>聊聊资源管理框架后续的一些优化: 通过论坛和github的issue,收到了很多优化或bug的反馈,基本上抽空全部处理了,大 ...

  2. Cocos Creator 通用框架设计 —— 资源管理

    如果你想使用Cocos Creator制作一些规模稍大的游戏,那么资源管理是必须解决的问题,随着游戏的进行,你可能会发现游戏的内存占用只升不降,哪怕你当前只用到了极少的资源,并且有使用cc.loade ...

  3. Android通用框架设计与完整电商APP开发系列文章

    作者|傅猿猿 责编|Javen205 有福利 有福利 有福利 鸣谢 感谢@傅猿猿 邀请写此系列文章 Android通用框架设计与完整电商APP开发 课程介绍 [导学视频] [课程详细介绍] 以下是部分 ...

  4. 新编辑器Cocos Creator发布:对不起我来晚了!

    1月19日,由Cocos创始人王哲亲手撰写的一篇Cocos Creator新品发布稿件在朋友圈被行业人士疯狂转载,短短数小时阅读量突破五位数.Cocos Creator被誉为“注定将揭开Cocos开发 ...

  5. 赢友网络通用框架V10.0.0(WinuAppSoft) 基础框架设计表

    /* * 版权所有:赢友网络(http://www.winu.net/) * 开发人员:新生帝(JsonLei) * 设计名称:赢友网络通用框架V10.0.0(WinuAppSoft) * 设计时间: ...

  6. 痞子衡嵌入式:嵌入式里通用微秒(microseconds)计时函数框架设计与实现

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是嵌入式里通用微秒(microseconds)计时函数框架设计与实现. 在嵌入式软件开发里,计时可以说是非常基础的功能模块了,其应用也非常 ...

  7. C++通用框架和库

    C++通用框架和库 来源 https://www.cnblogs.com/skyus/articles/8524408.html 关于 C++ 框架.库和资源的一些汇总列表,内容包括:标准库.Web应 ...

  8. 触控的手牌—Cocos Creator

    科普 Cocos Creator是触控最新一代游戏工具链的名称.如果不太清楚的,可以先看一些新闻.   新编辑器Cocos Creator发布: 对不起我来晚了! http://ol.tgbus.co ...

  9. Android 程序框架设计

    这篇文章主要内容来自于之前我讲的一个PPT文档,现在将其整理如下.欢迎指正.以下的内容都是来自于我自身的经验,欢迎大家多提自己的建议. 1.一些概念 模式的定义: 每个模式都描述了一个在我们的环境中不 ...

随机推荐

  1. Linux开机启动过程(个人理解)

    简述Linux启动过程 1)BIOS开机自检 2)MBR引导 3)启动引导程序菜单(GRUB) 4)加载内核 5)加载虚拟文件系统加载函数模块 6)启动系统进程 /sbin/init --->/ ...

  2. 51NOD---逆序对(树状数组 + 归并排序)

    1019 逆序数  基准时间限制:1 秒 空间限制:131072 KB 分值: 0 难度:基础题  收藏  关注 在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称 ...

  3. Spring源码分析之-加载IOC容器

    本文接上一篇文章 SpringIOC 源码,控制反转前的处理(https://mp.weixin.qq.com/s/9RbVP2ZQVx9-vKngqndW1w) 继续进行下面的分析 首先贴出 Spr ...

  4. Redis的最常被问到知识点总结

    1.什么是redis? Redis 是一个基于内存的高性能key-value数据库. 2.Reids的特点 Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库 ...

  5. IDEA中工程上传到SVN

    1.先在IDEA上集成SVN 2.查看SVN仓库:连接SVN ①此时应该先去SVN服务器中新建一个SVN服务: 点击下一步 点击下一步 点击create ②查看SVN仓库 先复制SVN的地址 把复制的 ...

  6. Oracle 实用SQL

    start with connect by prior 递归查询用法 select * from 表名 aa start with aa.id = 'xxx' connect by prior aa. ...

  7. sql 单表distinct/多表group by查询去除重复记录

    单表distinct 多表group by group by 必须放在 order by 和 limit之前,不然会报错 下面先来看看例子: table   id name   1 a   2 b   ...

  8. ExpandableListView 可折叠的下拉listview

    ExpandableListView用法如下 1.定义布局文件main.xml文件 <?xml version="1.0" encoding="utf-8" ...

  9. Winform中实现更改DevExpress的RadioGroup的选项时更改其他控件(TextEdit、ColorPickEdit)的值

    场景 Winform中实现读取xml配置文件并动态配置ZedGraph的RadioGroup的选项: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article ...

  10. sql字段为逗号分开的字符串值的关联查询

    1.TREE表: [strID] [int] IDENTITY(1,1) NOT NULL,[strName] [nvarchar](50) NOT NULL, 2.SubInfo CREATE TA ...