Cocos Creator 通用框架設(shè)計——網(wǎng)絡(luò)

編者按

本文來自于“Cocos 榮耀講師”征稿活動第1期,最先發(fā)表于 Cocos 中文社區(qū),作者,寶爺。寶爺是光子工作室的開發(fā)工程師,謙稱自己為一枚碼農(nóng),是一個熱愛游戲、熱愛開發(fā)、熱愛學(xué)習(xí)并堅持沉淀知識的開發(fā)者,曾寫過《精通 Cocos2d-x 游戲開發(fā)》基礎(chǔ)卷與進(jìn)階卷,感謝寶爺為社區(qū)所做的貢獻(xiàn)!

在 Cocos Creator 中發(fā)起一個 http 請求是比較簡單的,但很多游戲希望能夠和服務(wù)器之間保持長連接,以便服務(wù)端能夠主動向客戶端推送消息,而非總是由客戶端發(fā)起請求,對于實時性要求較高的游戲更是如此。這里我們會設(shè)計一個通用的網(wǎng)絡(luò)框架,可以方便地應(yīng)用于我們的項目中。

項目源碼

https://github.com/wyb10a10/cocos_creator_framework本項目在不斷完善中,包含 bug修改和代碼更新,下文所展示的代碼請以源碼為準(zhǔn)。

使用websocket

在實現(xiàn)這個網(wǎng)絡(luò)框架之前,我們先了解一下 websocket。websocket 是一種基于 tcp 的全雙工網(wǎng)絡(luò)協(xié)議,可以讓網(wǎng)頁創(chuàng)建持久性的連接,進(jìn)行雙向的通訊。在 Cocos Creator 中使用 websocket 既可以用于 H5 網(wǎng)頁游戲上,同樣支持原生平臺 Android 和 iOS。

構(gòu)造 websocket 對象

在使用 websocket 時,第一步應(yīng)該創(chuàng)建一個 websocket 對象。websocket 對象的構(gòu)造函數(shù)可以傳入2個參數(shù),第一個是 url 字符串,第二個是協(xié)議字符串或字符串?dāng)?shù)組,指定了可接受的子協(xié)議,服務(wù)端需要選擇其中的一個返回,才會建立連接,但我們一般用不到。

url 參數(shù)非常重要,主要分為4部分:協(xié)議、地址、端口、資源。

比如 ws://echo.websocket.org

  • 協(xié)議:必選項,默認(rèn)是 ws 協(xié)議,如果需要安全加密則使用 wss。

  • 地址:必選項,可以是 ip 或域名,當(dāng)然建議使用域名。

  • 端口:可選項,在不指定的情況下,ws 的默認(rèn)端口為 80,wss 的默認(rèn)端口為 443。

  • 資源:可選性,一般是跟在域名后某資源路徑,我們基本不需要它。

websocket 的狀態(tài)

websocket 有4個狀態(tài),可以通過 readyState 屬性查詢:

  • 0 CONNECTING 尚未建立連接。

  • 1 OPEN WebSocket連接已建立,可以進(jìn)行通信。

  • 2 CLOSING 連接正在進(jìn)行關(guān)閉握手,或者該close()方法已被調(diào)用。

  • 3 CLOSED 連接已關(guān)閉。

websocket 的 API

websocket 只有2個 API,void send( data ) 發(fā)送數(shù)據(jù)和 void close( code, reason ) 關(guān)閉連接。

send 方法只接收一個參數(shù)——即要發(fā)送的數(shù)據(jù),類型可以是以下4個類型的任意一種:string | ArrayBufferLike | Blob | ArrayBufferView。

如果要發(fā)送的數(shù)據(jù)是二進(jìn)制,我們可以通過 websocket 對象的 binaryType 屬性來指定二進(jìn)制的類型,binaryType 只可以被設(shè)置為“blob”或“arraybuffer”,默認(rèn)為“blob”。如果我們要傳輸?shù)氖俏募@樣較為固定的、用于寫入到磁盤的數(shù)據(jù),使用 blob。而你希望傳輸?shù)膶ο笤趦?nèi)存中進(jìn)行處理則使用較為靈活的 arraybuffer。如果要從其他非 blob 對象和數(shù)據(jù)構(gòu)造一個 blob,需要使用 blob 的構(gòu)造函數(shù)。

在發(fā)送數(shù)據(jù)時,官方有2個建議:

  • 檢測 websocket 對象的 readyState 是否為 OPEN,是才進(jìn)行 send。

  • 檢測 websocket 對象的 bufferedAmount 是否為0,是才進(jìn)行 send(為了避免消息堆積,該屬性表示調(diào)用 send 后堆積在 websocket 緩沖區(qū)的還未真正發(fā)送出去的數(shù)據(jù)長度)。

close 方法接收2個可選的參數(shù),code 表示錯誤碼,我們應(yīng)該傳入 1000 或 3000~4999 之間的整數(shù),reason 可以用于表示關(guān)閉的原因,長度不可超過 123 字節(jié)。

websocket 的回調(diào)

websocket 提供了4個回調(diào)函數(shù)供我們綁定:

  • onopen:連接成功后調(diào)用。

  • onmessage:有消息過來時調(diào)用:傳入的對象有 data 屬性,可能是字符串、blob 或 arraybuffer。

  • onerror:出現(xiàn)網(wǎng)絡(luò)錯誤時調(diào)用:傳入的對象有 data 屬性,通常是錯誤描述的字符串。

  • onclose:連接關(guān)閉時調(diào)用:傳入的對象有 code、reason、wasClean 等屬性。

注意:當(dāng)網(wǎng)絡(luò)出錯時,會先調(diào)用 onerror 再調(diào)用 onclose,無論何種原因的連接關(guān)閉,onclose 都會被調(diào)用。

Echo 實例

下面 websocket 官網(wǎng)的 echo demo 的代碼,可以將其寫入一個 html 文件中并用瀏覽器打開,打開后會自動創(chuàng)建 websocket 連接,在連接上時主動發(fā)送了一條消息“WebSocket rocks”,服務(wù)器會將該消息返回,觸發(fā) onMessage,將信息打印到屏幕上,然后關(guān)閉連接。具體可以參考:http://www.websocket.org/echo.html17

默認(rèn)的 url 前綴是wss,由于 wss 抽風(fēng),使用 ws 才可以連接上,如果 ws 也抽風(fēng),可以試試連這個地址ws://121.40.165.18:8800,這是國內(nèi)的一個免費測試 websocket 的網(wǎng)址。

參考鏈接

設(shè)計框架

一個通用的網(wǎng)絡(luò)框架,在通用的前提下還需要能夠支持各種項目的差異需求,根據(jù)經(jīng)驗,常見的需求差異如下:

  • 用戶協(xié)議差異,游戲可能傳輸 json、protobuf、flatbuffer 或者自定義的二進(jìn)制協(xié)議。

  • 底層協(xié)議差異,我們可能使用 websocket、或者微信小游戲的 wx.websocket、甚至在原生平臺我們希望使用 tcp/udp/kcp 等協(xié)議。

  • 登陸認(rèn)證流程,在使用長連接之前我們理應(yīng)進(jìn)行登陸認(rèn)證,而不同游戲登陸認(rèn)證的方式不同。

  • 網(wǎng)絡(luò)異常處理,比如超時時間是多久,超時后的表現(xiàn)是怎樣的,請求時是否應(yīng)該屏蔽 UI 等待服務(wù)器響應(yīng),網(wǎng)絡(luò)斷開后表現(xiàn)如何,自動重連還是由玩家點擊重連按鈕進(jìn)行重連,重連之后是否重發(fā)斷網(wǎng)期間的消息?等等這些。

  • 多連接的處理,某些游戲可能需要支持多個不同的連接,一般不會超過2個,比如一個主連接負(fù)責(zé)處理大廳等業(yè)務(wù)消息,一個戰(zhàn)斗連接直接連戰(zhàn)斗服務(wù)器,或者連接聊天服務(wù)器。

根據(jù)上面的這些需求,我們對功能模塊進(jìn)行拆分,盡量保證模塊的高內(nèi)聚,低耦合。

ProtocolHelper 協(xié)議處理模塊——當(dāng)我們拿到一塊 buffer時,我們可能需要知道這個 buffer 對應(yīng)的協(xié)議或者 id 是多少,比如我們在請求的時候就傳入了響應(yīng)的處理回調(diào),那么常用的做法可能會用一個自增的 id 來區(qū)別每一個請求,或者是用協(xié)議號來區(qū)分不同的請求,這些是開發(fā)者需要實現(xiàn)的。我們還需要從 buffer 中獲取包的長度是多少?包長的合理范圍是多少?心跳包長什么樣子等等。

Socket 模塊——實現(xiàn)最基礎(chǔ)的通訊功能,首先定義 Socket 的接口類 ISocket,定義如連接、關(guān)閉、數(shù)據(jù)接收與發(fā)送等接口,然后子類繼承并實現(xiàn)這些接口。

NetworkTips 網(wǎng)絡(luò)顯示模塊——實現(xiàn)如連接中、重連中、加載中、網(wǎng)絡(luò)斷開等狀態(tài)的顯示,以及 UI 的屏蔽。

NetNode 網(wǎng)絡(luò)節(jié)點——所謂網(wǎng)絡(luò)節(jié)點,其實主要的職責(zé)是將上面的功能串聯(lián)起來,為用戶提供一個易用的接口。

NetManager 管理網(wǎng)絡(luò)節(jié)點的單例——我們可能有多個網(wǎng)絡(luò)節(jié)點(多條連接),所以這里使用單例來進(jìn)行管理,使用單例來操作網(wǎng)絡(luò)節(jié)點也會更加方便。

ProtocolHelper

在這里定義了一個 IProtocolHelper 的簡單接口,如下所示:

export type NetData = (string | ArrayBufferLike | Blob | ArrayBufferView);

Socket

在這里定義了一個 ISocket 的簡單接口,如下所示:

// Socket接口

接下來我們實現(xiàn)一個 WebSock,繼承于 ISocket,我們只需要實現(xiàn) connect、send 和 close 接口即可。send 和 close 都是對 websocket 對簡單封裝,connect 則需要根據(jù)傳入的 ip、端口等參數(shù)構(gòu)造一個 url 來創(chuàng)建 websocket,并綁定 websocket 的回調(diào)。

export class WebSock implements ISocket {

NetworkTips

INetworkTips 提供了非常的接口,重連和請求的開關(guān),框架會在合適的時機(jī)調(diào)用它們,我們可以繼承 INetworkTips 并定制我們的網(wǎng)絡(luò)相關(guān)提示信息,需要注意的是這些接口可能會被多次調(diào)用。

// 網(wǎng)絡(luò)提示接口

NetNode

NetNode 是整個網(wǎng)絡(luò)框架中最為關(guān)鍵的部分,一個 NetNode 實例表示一個完整的連接對象,基于 NetNode 我們可以方便地進(jìn)行擴(kuò)展,它的主要職責(zé)有:

連接維護(hù)

  • 連接的建立與鑒權(quán)(是否鑒權(quán)、如何鑒權(quán)由用戶的回調(diào)決定)

  • 斷線重連后的數(shù)據(jù)重發(fā)處理

  • 心跳機(jī)制確保連接有效(心跳包間隔由配置,心跳包的內(nèi)容由ProtocolHelper定義)

  • 連接的關(guān)閉

數(shù)據(jù)發(fā)送

  • 支持?jǐn)嗑€重傳,超時重傳

  • 支持唯一發(fā)送(避免同一時間重復(fù)發(fā)送)

數(shù)據(jù)接收

  • 支持持續(xù)監(jiān)聽

  • 支持request-respone模式

界面展示

  • 可自定義網(wǎng)絡(luò)延遲、短線重連等狀態(tài)的表現(xiàn)

首先我們定義了 NetTipsType、NetNodeState 兩個枚舉,以及 NetConnectOptions 結(jié)構(gòu)供 NetNode 使用。

接下來是 NetNode 的成員變量,NetNode 的變量可以分為以下幾類:

  • NetNode 自身的狀態(tài)變量,如 ISocket 對象、當(dāng)前狀態(tài)、連接參數(shù)等等。

  • 各種回調(diào),包括連接、斷開連接、協(xié)議處理、網(wǎng)絡(luò)提示等回調(diào)。

  • 各種定時器,如心跳、重連相關(guān)的定時器。

  • 請求列表與監(jiān)聽列表,都是用于接收到的消息處理。

接下來介紹網(wǎng)絡(luò)相關(guān)的成員函數(shù),首先看初始化與:

  • init 方法用于初始化 NetNode,主要是指定 Socket 與協(xié)議等處理對象。

  • connect 方法用于連接服務(wù)器。

  • initSocket 方法用于綁定 Socket 的回調(diào)到 NetNode 中。

  • updateNetTips 方法用于刷新網(wǎng)絡(luò)提示。

onConnected 方法在網(wǎng)絡(luò)連接成功后調(diào)用,自動進(jìn)入鑒權(quán)流程(如果設(shè)置了_connectedCallback),在鑒權(quán)完成后需要調(diào)用 onChecked 方法使 NetNode 進(jìn)入可通訊的狀態(tài),在未鑒權(quán)的情況,我們不應(yīng)該發(fā)送任何業(yè)務(wù)請求,但登錄驗證這類請求應(yīng)該發(fā)送給服務(wù)器,這類請求可以通過帶force參數(shù)強(qiáng)制發(fā)送給服務(wù)器。

接收到任何消息都會觸發(fā) onMessage,首先會對數(shù)據(jù)包進(jìn)行校驗,校驗的規(guī)則可以在自己的 ProtocolHelper 中實現(xiàn),如果是一個合法的數(shù)據(jù)包,我們會將心跳和超時計時器進(jìn)行更新——重新計時,最后在 _requests 和 _listener 中找到該消息的處理函數(shù),這里是通過 rspCmd 進(jìn)行查找的,rspCmd 是從 ProtocolHelper 的 getPackageId 取出的,我們可以將協(xié)議的命令或者序號返回,由我們自己來決定請求和響應(yīng)如何對應(yīng)。

onError 和 onClosed 是網(wǎng)絡(luò)出錯和關(guān)閉時調(diào)用的,無論是否出錯,最終都會調(diào)用 onClosed,在這里我們執(zhí)行斷線回調(diào),以及做自動重連的處理。當(dāng)然也可以調(diào)用 close來關(guān)閉套接字。close 與 closeSocket 的區(qū)別在于 closeSocket 只是關(guān)閉套接字——我仍然要使用當(dāng)前的 NetNode,可能通過下一次 connect 恢復(fù)網(wǎng)絡(luò)。而 close則是清除所有的狀態(tài)。

發(fā)起網(wǎng)絡(luò)請求有3種方式:

send 方法,純粹地發(fā)送數(shù)據(jù),如果當(dāng)前斷網(wǎng)或者驗證中會進(jìn)入 _request 隊列。

request 方法,在請求的時候即以閉包的方式傳入回調(diào),在該請求的響應(yīng)回到時會執(zhí)行回調(diào),如果同時有多個相同的請求,那么這 N 個請求的響應(yīng)會依次回到客戶端,響應(yīng)回調(diào)也會依次執(zhí)行(每次只會執(zhí)行一個回調(diào))。

requestUnique 方法,如果我們不希望有多個相同的請求,可以使用 requestUnique 來確保每一種請求同時只會有一個。

這里確保沒有重復(fù)之所以使用的是遍歷 _requests,是因為我們不會積壓大量的請求到 _requests中,超時或異常重發(fā)也不會導(dǎo)致 _requests 的積壓,因為重發(fā)的邏輯是由 NetNode 控制的,而且在網(wǎng)絡(luò)斷開的情況下,我們理應(yīng)屏蔽用戶發(fā)起請求,此時一般會有一個全屏遮罩——網(wǎng)絡(luò)出現(xiàn)波動之類的提示。

我們有2種回調(diào),一種是前面的 request 回調(diào),這種回調(diào)是臨時性的,一般隨著請求-響應(yīng)-執(zhí)行而立即清理,_listener 回調(diào)則是常駐的,需要我們手動管理的,比如打開某界面時監(jiān)聽、離開是關(guān)閉,或者在游戲一開始就進(jìn)行監(jiān)聽。適合處理服務(wù)器的主動推送消息。

最后是心跳與超時相關(guān)的定時器,我們每隔 _heartTime 會發(fā)送一個心跳包,每隔 _receiveTime 檢測如果沒有收到服務(wù)器返回的包,則判斷網(wǎng)絡(luò)斷開。

完整代碼,大家可以進(jìn)入源碼查看!

NetManager

NetManager 用于管理 NetNode,這是由于我們可能需要支持多個不同的連接對象,所以需要一個 NetManager 專門來管理 NetNode,同時,NetManager 作為一個單例,也可以方便我們調(diào)用網(wǎng)絡(luò)。

export class NetManager {

測試?yán)?/strong>

接下來我們用一個簡單的例子來演示一下網(wǎng)絡(luò)框架的基本使用,首先我們需要拼一個簡單的界面用于展示,3個按鈕(連接、發(fā)送、關(guān)閉),2個輸入框(輸入 url、輸入要發(fā)送的內(nèi)容),一個文本框(顯示從服務(wù)器接收到的數(shù)據(jù)),如下圖所示。

該例子連接的是 websocket 官方的 echo.websocket.org 地址,這個服務(wù)器會將我們發(fā)送給它的所有消息都原樣返回給我們。

接下來,實現(xiàn)一個簡單的 Component,這里新建了一個 NetExample.ts 文件,做的事情非常簡單,在初始化的時候創(chuàng)建 NetNode、綁定默認(rèn)接收回調(diào),在接收回調(diào)中將服務(wù)器返回的文本顯示到 msgLabel中。接著是連接、發(fā)送和關(guān)閉幾個接口的實現(xiàn):

// 不關(guān)鍵的代碼省略

代碼完成后,將其掛載到場景的 Canvas 節(jié)點下(其他節(jié)點也可以),然后將場景中的 Label 和 RichText 拖拽到我們的 NetExample 的屬性面板中:

運行效果如下所示:

小結(jié)

可以看到,Websocket 的使用很簡單,我們在開發(fā)的過程中會碰到各種各樣的需求和問題,要實現(xiàn)一個好的設(shè)計,快速地解決問題。

我們一方面需要對我們使用的技術(shù)本身有深入的理解,websocket 的底層協(xié)議傳輸是如何實現(xiàn)的?與 tcp、http 的區(qū)別在哪里?基于 websocket 能否使用 udp 進(jìn)行傳輸呢?使用 websocket 發(fā)送數(shù)據(jù)是否需要自己對數(shù)據(jù)流進(jìn)行分包(websocket 協(xié)議保證了包的完整)?數(shù)據(jù)的發(fā)送是否出現(xiàn)了發(fā)送緩存的堆積(查看 bufferedAmount)?

另外需要對我們的使用場景及需求本身的理解,對需求的理解越透徹,越能做出好的設(shè)計。哪些需求是項目相關(guān)的,哪些需求是通用的?通用的需求是必須的還是可選的?不同的變化我們應(yīng)該封裝成類或接口,使用多態(tài)的方式來實現(xiàn)呢?還是提供配置?回調(diào)綁定?事件通知?

我們需要設(shè)計出一個好的框架,來適用于下一個項目,并且在一個一個的項目中優(yōu)化迭代,這樣才能建立深厚的沉淀、提高效率。

接下來的一段時間,我會將之前的一些經(jīng)驗整理為一個開源易用的 Cocos Creator 框架。源碼鏈接:https://github.com/wyb10a10/cocos_creator_framework

———— / END / ————

再次向作者寶爺?shù)姆窒碇乱灾x意!歡迎大家點擊文末【閱讀原文】進(jìn)入原貼與作者及廣大開發(fā)者一同交流,為作者點贊!

Cocos 第1期征稿活動已結(jié)束,詳細(xì)獲獎名單詳見今日推送第2條,獲獎文章將陸續(xù)在 Cocos 公眾號上進(jìn)行轉(zhuǎn)載/發(fā)布,內(nèi)容原創(chuàng)版權(quán)歸原作者所有,轉(zhuǎn)載請聯(lián)系原作者!

更多精彩

Cocos Creator v2.2 正式發(fā)布

Cocos Creator 3D v1.0 正式發(fā)布

Cocos Creator 3D 物理模塊介紹

Cocos Creator v2.2 自定義渲染組件及材質(zhì)介紹

小姐姐,你的發(fā)絲高光怎么用 Creator 3D 實現(xiàn)?

極限開發(fā)《TheCode》和《Shoot the F》創(chuàng)作筆記

獨立游戲 5000 萬下載百萬 DAU 竟不花一分錢?

Cocos 與騰訊云宣布戰(zhàn)略合作,把游戲開發(fā)門檻降到極致

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容