編者按
本文來自于“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é)議、地址、端口、資源。
協(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 3D v1.0 正式發(fā)布
Cocos Creator 3D 物理模塊介紹
Cocos Creator v2.2 自定義渲染組件及材質(zhì)介紹
小姐姐,你的發(fā)絲高光怎么用 Creator 3D 實現(xiàn)?