在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的網址。
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
var wsUri = "ws://echo.websocket.org/";
var output;
function init() {
output = document.getElementById("output");
testWebSocket();
}
function testWebSocket() {
// 初始化websocket,綁定回調
websocket = new WebSocket(wsUri);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
websocket.onerror = onError;
}
function onOpen(evt) {
writeToScreen("CONNECTED");
doSend("WebSocket rocks");
}
function onClose(evt) {
writeToScreen("DISCONNECTED");
}
function onMessage(evt) {
writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
websocket.close();
}
function onError(evt) {
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}
function doSend(message) {
writeToScreen("SENT: " + message);
websocket.send(message);
}
function writeToScreen(message) {
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
output.appendChild(pre);
}
// 加載時調用init方法,初始化websocket
window.addEventListener("load", init, false);
</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
參考
- 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個,比如一個主連接配接負責處理大廳等業務消息,一個戰鬥連接配接直接連戰鬥伺服器,或者連接配接聊天伺服器。
根據上面的這些需求,我們對功能子產品進行拆分,盡量保證子產品的高内聚,低耦合。

- 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<RequestObject>(); // 請求清單
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);
// 優先觸發request隊列
if (this._requests.length > 0) {
for (let reqIdx in this._requests) {
let req = this._requests[reqIdx];
if (req.rspCmd == rspCmd) {
console.log(`NetNode execute request rspcmd ${rspCmd}`);
this._callbackExecuter(req.rspObject, msg);
this._requests.splice(parseInt(reqIdx), 1);
break;
}
}
console.log(`NetNode still has ${this._requests.length} request watting`);
if (this._requests.length == 0) {
this.updateNetTips(NetTipsType.Requesting, false);
}
}
let listeners = this._listener[rspCmd];
if (null != listeners) {
for (const rsp of listeners) {
console.log(`NetNode execute listener cmd ${rspCmd}`);
this._callbackExecuter(rsp, msg);
}
}
}
protected onError(event) {
console.error(event);
}
protected onClosed(event) {
this.clearTimer();
// 執行斷線回調,傳回false表示不進行重連
if (this._disconnectCallback && !this._disconnectCallback()) {
console.log(`disconnect return!`)
return;
}
// 自動重連
if (this.isAutoReconnect()) {
this.updateNetTips(NetTipsType.ReConnecting, true);
this._reconnectTimer = setTimeout(() => {
this._socket.close();
this._state = NetNodeState.Closed;
this.connect(this._connectOptions);
if (this._autoReconnect > 0) {
this._autoReconnect -= 1;
}
}, this._reconnetTimeOut);
} else {
this._state = NetNodeState.Closed;
}
}
public close(code?: number, reason?: string) {
this.clearTimer();
this._listener = {};
this._requests.length = 0;
if (this._networkTips) {
this._networkTips.connectTips(false);
this._networkTips.reconnectTips(false);
this._networkTips.requestTips(false);
}
if (this._socket) {
this._socket.close(code, reason);
} else {
this._state = NetNodeState.Closed;
}
}
// 隻是關閉Socket套接字(仍然重用緩存與目前狀态)
public closeSocket(code?: number, reason?: string) {
if (this._socket) {
this._socket.close(code, reason);
}
}
// 發起請求,如果目前處于重連中,進入緩存清單等待重連完成後發送
public send(buf: NetData, force: boolean = false): boolean {
if (this._state == NetNodeState.Working || force) {
console.log(`socket send ...`);
return this._socket.send(buf);
} else if (this._state == NetNodeState.Checking ||
this._state == NetNodeState.Connecting) {
this._requests.push({
buffer: buf,
rspCmd: 0,
rspObject: null
});
console.log("NetNode socket is busy, push to send buffer, current state is " + this._state);
return true;
} else {
console.error("NetNode request error! current state is " + this._state);
return false;
}
}
// 發起請求,并進入緩存清單
public request(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false) {
if (this._state == NetNodeState.Working || force) {
this._socket.send(buf);
}
console.log(`NetNode request with timeout for ${rspCmd}`);
// 進入發送緩存清單
this._requests.push({
buffer: buf, rspCmd, rspObject
});
// 啟動網絡請求層
if (showTips) {
this.updateNetTips(NetTipsType.Requesting, true);
}
}
// 唯一request,確定沒有同一響應的請求(避免一個請求重複發送,netTips界面的屏蔽也是一個好的方法)
public requestUnique(buf: NetData, rspCmd: number, rspObject: CallbackObject, showTips: boolean = true, force: boolean = false): boolean {
for (let i = 0; i < this._requests.length; ++i) {
if (this._requests[i].rspCmd == rspCmd) {
console.log(`NetNode requestUnique faile for ${rspCmd}`);
return false;
}
}
this.request(buf, rspCmd, rspObject, showTips, force);
return true;
}
/********************** 回調相關處理 *********************/
public setResponeHandler(cmd: number, callback: NetCallFunc, target?: any): boolean {
if (callback == null) {
console.error(`NetNode setResponeHandler error ${cmd}`);
return false;
}
this._listener[cmd] = [{ target, callback }];
return true;
}
public addResponeHandler(cmd: number, callback: NetCallFunc, target?: any): boolean {
if (callback == null) {
console.error(`NetNode addResponeHandler error ${cmd}`);
return false;
}
let rspObject = { target, callback };
if (null == this._listener[cmd]) {
this._listener[cmd] = [rspObject];
} else {
let index = this.getNetListenersIndex(cmd, rspObject);
if (-1 == index) {
this._listener[cmd].push(rspObject);
}
}
return true;
}
public removeResponeHandler(cmd: number, callback: NetCallFunc, target?: any) {
if (null != this._listener[cmd] && callback != null) {
let index = this.getNetListenersIndex(cmd, { target, callback });
if (-1 != index) {
this._listener[cmd].splice(index, 1);
}
}
}
public cleanListeners(cmd: number = -1) {
if (cmd == -1) {
this._listener = {}
} else {
this._listener[cmd] = null;
}
}
protected getNetListenersIndex(cmd: number, rspObject: CallbackObject): number {
let index = -1;
for (let i = 0; i < this._listener[cmd].length; i++) {
let iterator = this._listener[cmd][i];
if (iterator.callback == rspObject.callback
&& iterator.target == rspObject.target) {
index = i;
break;
}
}
return index;
}
/********************** 心跳、逾時相關處理 *********************/
protected resetReceiveMsgTimer() {
if (this._receiveMsgTimer !== null) {
clearTimeout(this._receiveMsgTimer);
}
this._receiveMsgTimer = setTimeout(() => {
console.warn("NetNode recvieMsgTimer close socket!");
this._socket.close();
}, this._receiveTime);
}
protected resetHearbeatTimer() {
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
this._keepAliveTimer = setTimeout(() => {
console.log("NetNode keepAliveTimer send Hearbeat")
this.send(this._protocolHelper.getHearbeat());
}, this._heartTime);
}
protected clearTimer() {
if (this._receiveMsgTimer !== null) {
clearTimeout(this._receiveMsgTimer);
}
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
if (this._reconnectTimer !== null) {
clearTimeout(this._reconnectTimer);
}
}
public isAutoReconnect() {
return this._autoReconnect != 0;
}
public rejectReconnect() {
this._autoReconnect = 0;
this.clearTimer();
}
}
NetManager
NetManager用于管理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、輸入要發送的内容),一個文本框(顯示從伺服器接收到的資料),如下圖所示。
該例子連接配接的是websocket官方的echo.websocket.org位址,這個伺服器會将我們發送給它的所有消息都原樣傳回給我們。
接下來,實作一個簡單的Component,這裡建立了一個NetExample.ts檔案,做的事情非常簡單,在初始化的時候建立NetNode、綁定預設接收回調,在接收回調中将伺服器傳回的文本顯示到msgLabel中。接着是連接配接、發送和關閉幾個接口的實作:
// 不關鍵的代碼省略
@ccclass
export 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)?
另外需要對我們的使用場景及需求本身的了解,對需求的了解越透徹,越能做出好的設計。哪些需求是項目相關的,哪些需求是通用的?通用的需求是必須的還是可選的?不同的變化我們應該封裝成類或接口,使用多态的方式來實作呢?還是提供配置?回調綁定?事件通知?
我們需要設計出一個好的架構,來适用于下一個項目,并且在一個一個的項目中優化疊代,這樣才能建立深厚的沉澱、提高效率。
接下來的一段時間會将之前的一些經驗整理為一個開源易用的cocos creator架構:https://github.com/wyb10a10/cocos_creator_framework