天天看點

深入了解使用egret.WebSocket

概念

本教程不講解TCP/IP協定,Socket屬于哪層,消息包體怎麼設計等,主講 egret.WebSocket 使用示例 與 protobuf 使用示例。

在使用egret.WebSocket之前需要簡單讨論了解目前幾種通信模式。

HTTP
深入了解使用egret.WebSocket

網站中常見的一種傳輸協定,用于通路頁面或資源時,向頁面所在的伺服器發送一個 HTTP 請求。伺服器識别請求,傳回響應資料并關閉連接配接。這過程中用戶端不請求,伺服器不能主動推送消息到用戶端。早些的遊戲通過輪訓以及 AJAX 實作了不需要手動重新整理程式内部輪訓請求的僞的長連接配接。這顯然是一個非常不明智的方式。可以想象一下聊天室或人物移動場景中,如果我們使用 HTTP 會是一種什麼情況。大量的請求與響應報頭額外的資料、延遲不斷發生、傳輸帶寬壓力不斷增加,這對于ARPG等類型遊戲是緻命的。主要适合對即時性要求不高的遊戲類型。

Socket
深入了解使用egret.WebSocket

端遊中常見的一種傳輸協定,套連結。需要了解 Socket 的同學百度一下,它是一個長連接配接的協定。在完成握手後,連接配接會一直開着,直到用戶端或伺服器明确予以關閉。在這過程中,伺服器能主動的推送消息到用戶端,消息格式可以是進制流以及自定義格式等。後期由于FLASH的興起,頁遊中絕大多數都在使用。可以想象一下聊天室或人物移動場景中,我們使用 socket 會是一種什麼情況。沒有額外的資料、主動的消息推送、低延遲等等。

WebScoket

早期 HTML 中并沒有提供 socket 的支援,大型頁遊項目依靠于 Flash 提供的 Socket API 。随着 HTML5 的制定與完善,WebSocket 被各大浏覽器廠商所支援。

WebScoket 與 Socket 的差別在于前者提供了完善的API以及握手的機制,而後者是抽象出來的一種概念,具體的實作對于各種語言都可能不同,例如:我們需要自定義協定體,控制緩存區,連接配接确認方式等。而在 WebSocket 中,每個消息的傳輸規範都是定義好的,如消息以 0x00 位元組開頭,以 0xff 結尾,中間資料采用 UTF-8 編碼格式,第一次握手必須使用 ws://xxx 或 wss://xxx 進行,在握手成功後将協定更新為 WebSocket 協定,進行雙工的通信。第一次請求走的是 HTTP 請求。由于各種規範的定義與實作,舊有的伺服器 Socket 并不适用于 WebSocket 。

實際上,許多語言、架構和伺服器都提供了 WebSocket 支援,例如:

  • 基于 C 的 libwebsocket.org
  • 基于 Node.js 的 Socket.io
  • 基于 Python 的 ws4py
  • 基于 C++ 的 WebSocket++
  • Apache 對 WebSocket 的支援: Apache Module mod_proxy_wstunnel
  • Nginx 對 WebSockets 的支援: NGINX as a WebSockets Proxy 、 NGINX Announces Support for WebSocket Protocol 、WebSocket proxying
  • lighttpd 對 WebSocket 的支援:mod_websocket

egret.WebSocket 使用示例

早期參與或制作遊戲項目,對下圖一定不陌生,定義消息長度位、消息号以及消息讀取規範,用戶端根據協定規範以位元組形式讀取包體:

深入了解使用egret.WebSocket

HML5 的 WebSocket 傳輸中,并沒有定義進制流的傳送讀取。 egret.WebSocket 中對 HTML5 中 WebSocket 進行封裝,實作了對于進制流的傳輸。

egret.WebSocket 預設是字元串形式接受資料,建立一個 egret.WebSocket 非常簡單,由于 egret.WebSocket 對位元組流傳輸的實作,伺服器與用戶端舊有的協定非常友善移植。以下示例示範了建立 egret.WebSocket :

1.修改項目檔案 egretProperties.json 中的 modules ,增加 {"name": "socket"}

2.在項目所在目錄執行一次編譯引擎 egret build -e

this.socket = new egret.WebSocket();
//設定資料格式為二進制,預設為字元串
this.socket.type = egret.WebSocket.TYPE_BINARY;
//添加收到資料偵聽,收到資料會調用此方法
this.socket.addEventListener(egret.ProgressEvent.SOCKET_DATA, 	this.onReceiveMessage, this);
//添加連結打開偵聽,連接配接成功會調用此方法
this.socket.addEventListener(egret.Event.CONNECT, this.onSocketOpen, this);
//添加連結關閉偵聽,手動關閉或者伺服器關閉連接配接會調用此方法
this.socket.addEventListener(egret.Event.CLOSE, this.onSocketClose, this);
//添加異常偵聽,出現異常會調用此方法
this.socket.addEventListener(egret.IOErrorEvent.IO_ERROR, this.onSocketError, this);
//連接配接伺服器
this.socket.connect("echo.websocket.org", 80);
           

當觸發 egret.Event.CONNECT 偵聽方法 onSocketOpen 時連接配接伺服器成功,可以進行資料發送接收。我們建立一個位元組數組,通過writeType寫入字元串類型,布爾類型,整形,設定指針為開始0,調用 this.socket.writeBytes 寫入資料進行資料發送:

var byte:egret.ByteArray = new egret.ByteArray();
byte.writeUTF("Hello Egret WebSocket");
byte.writeBoolean(false);
byte.writeInt(123);
byte.position = 0;
this.socket.writeBytes(byte, 0, byte.bytesAvailable);
this.socket.flush();
           

當觸發 egret.ProgressEvent.SOCKET_DATA 偵聽方法 onReceiveMessage() 時資料接收成功,建立一個位元組數組并将 socket 中目前資料讀入其中,與發送方式類似,接收使用 readType :

var byte:egret.ByteArray = new egret.ByteArray();
this.socket.readBytes(byte);
var msg:string = byte.readUTF();
var boo:boolean = byte.readBoolean();
var num:number = byte.readInt();
           

protobuf 使用示例

百度百科 protocolbuffer 介紹,protocolbuffer(以下簡稱PB)是google 的一種資料交換的格式,它獨立于語言,獨立于平台。google 提供了三種語言的實作:java、c++ 和 python,每一種實作都包含了相應語言的編譯器以及庫檔案。由于它是一種二進制的格式,比使用 xml 進行資料交換快許多。可以把它用于分布式應用之間的資料通信或者異構環境下的資料交換。作為一種效率和相容性都很優秀的二進制資料傳輸格式,可以用于諸如網絡傳輸、配置檔案、資料存儲等諸多領域。

protobuf 被适用于非常多的生産環境中,也出現了各種語言的版本,友善了資料的移植與可維護性。它在部分語言項目中有一定缺陷,如随着項目的不斷疊代會産生較多的資料結構類機器碼增加項目體積。

這裡以第三庫的形式加入對 protobufjs 的支援。想了解第三方內建的同學點選:內建第三方JavaScript庫

示例下載下傳見教程尾部:

1.拷貝示例項目 libs 目錄下 protobuf 目錄到新項目所在 libs 目錄。

2.拷貝 libsrc 目錄下 protobuf 目錄到新項目所在 protobuf 目錄。

3.項目 egretProperties.json 中增加相關内容。

egretProperties.json:
{
"document_class": "Main",
"modules": [
	{
		"name": "core"
	},
	{
		"name": "version"
	},
	{
		"name": "res"
	},
	{
		"name": "socket"
	},
	{
		"name": "protobuf",
		"path": "libsrc/protobuf"
	}
],
"egret_version": "2.0.2"
}
           

編譯引擎,完成對protobuf配置。

在 resource\assets\proto下 ,建立資料檔案并命名為 common.proto 。在其中定義我們需要傳輸的類對象。這個檔案在實際生産環境中是服務端用戶端公用的,可以有單個或多個根據具體項目而定。通過工具生産對應語言的通路類,如name.ts,并引入項目中,通過 new 或其他方式建立執行個體。可惜的是目前還沒有egret語言所使用的生成工具。

首先在 common.proto 内定義結構體,了解文法點選這裡 。我們定義一個簡單結構,如:

message Common {
    required uint32 id = 1;
    required string text = 2;
}
           

在 resource.js 中我們引入 common.proto 檔案,為了友善,在初始化進行加載。也可以使用 RESDepot 工具進行導入。

當檔案被加載後,進行資料設定之前需要四步:

1.擷取資源資料檔案。

2.解碼并建立對象構造器。

3.建立需要的資料結構類。

4.執行個體化資料結構類。

設定與讀取示例,如下代碼:

var proto: string = RES.getRes("common_proto");
var builder:any = dcodeIO.ProtoBuf.loadProto(proto);   
var clazz:any = builder.build("Common");                      
var data:any = new clazz();                                           		
data.set("id",1);//可以使用data.id=1;
data.set("text","oops");//可以使用data.text=oops; 

console.log("id=" + data.get("id"));
console.log("oops=" + data.get("text"));
           

我想我寫這到這裡,不隻是為了建立一個檔案,序列化資料,反序列化資料,然後建立個執行個體吧。 好吧,我們繼續往下講,下面就是我們具體使用 egret.WebSocket 發送資料。 這是我們使用它的關鍵。

在使用上例中 builder.build("Common") 得到對象構造器中提供了序列化的方法 toArrayBuffer() 通過 egret.ByteArray 寫入序列化進行傳輸,在實際的環境中,還需要涉及到一些長度位,校驗,消息号等這裡不做讨論。發送示例,如下代碼:

var arraybuffer: ArrayBuffer = data.toArrayBuffer();
var len: number = arraybuffer.byteLength;
var btyearray:egret.ByteArray=new egret.ByteArray(arraybuffer);
if(len > 0)
{ 
    this.socket.writeBytes(btyearray);
    this.socket.flush();
}
           

接收資料, 我們代碼中一直出現 ArrayBuffer 這是JS中一種用于二進制資料存儲的類型,與我們的 ByteAarry 相似(ByteAarry封裝了ArrayBuffer) 通過 DataView 提供的接口,轉換為我們可以使用的 ByteAarray 資料,如下代碼:

var msgBuff: ArrayBuffer;
var btyearray: egret.ByteArray = new egret.ByteArray();
this.socket.readBytes(btyearray);
var len = btyearray.buffer.byteLength;
var dataView = new DataView(btyearray.buffer);
var pbView = new DataView(new ArrayBuffer(len));
for(var i = 0;i < len;i++) {
    pbView.setInt8(i,dataView.getInt8(i));
}
msgBuff = pbView.buffer;

var proto: string = RES.getRes("common_proto");
var builder:any = dcodeIO.ProtoBuf.loadProto(proto); 
var clazz:any = builder.build("Common");
        
var data: any = clazz.decode(msgBuff);    
console.log("decodeData id=" + data.get("id"));
console.log("decodeData oops=" + data.get("text"));
           

項目示例:下載下傳

最後,感謝董剛同學提供的protobuf庫。