概念
本教程不講解TCP/IP協議,Socket屬於哪層,消息包體怎么設計等,主講 egret.WebSocket 使用示例 與 protobuf 使用示例。
在使用egret.WebSocket之前需要簡單討論了解目前幾種通信模式。
HTTP

網站中常見的一種傳輸協議,用於訪問頁面或資源時,向頁面所在的服務器發送一個 HTTP 請求。服務器識別請求,返回響應數據並關閉連接。這過程中客戶端不請求,服務器不能主動推送消息到客戶端。早些的游戲通過輪訓以及 AJAX 實現了不需要手動刷新程序內部輪訓請求的偽的長連接。這顯然是一個非常不明智的方式。可以想象一下聊天室或人物移動場景中,如果我們使用 HTTP 會是一種什么情況。大量的請求與響應報頭額外的數據、延遲不斷發生、傳輸帶寬壓力不斷增加,這對於ARPG等類型游戲是致命的。主要適合對即時性要求不高的游戲類型。
Socket

端游中常見的一種傳輸協議,套鏈接。需要了解 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 使用示例
早期參與或制作游戲項目,對下圖一定不陌生,定義消息長度位、消息號以及消息讀取規范,客戶端根據協議規范以字節形式讀取包體:

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庫。
