概念
本教程不讲解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库。