Cocos Creator 通用框架設計 —— 網路
在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