在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>

参考

设计框架

一个通用的网络框架,在通用的前提下还需要能够支持各种项目的差异需求,根据经验,常见的需求差异如下:

  • 用户协议差异,游戏可能传输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

Cocos Creator 通用框架设计 —— 网络的更多相关文章

  1. Cocos Creator 通用框架设计 —— 资源管理优化

    接着<Cocos Creator 通用框架设计 -- 资源管理>聊聊资源管理框架后续的一些优化: 通过论坛和github的issue,收到了很多优化或bug的反馈,基本上抽空全部处理了,大 ...

  2. Cocos Creator 通用框架设计 —— 资源管理

    如果你想使用Cocos Creator制作一些规模稍大的游戏,那么资源管理是必须解决的问题,随着游戏的进行,你可能会发现游戏的内存占用只升不降,哪怕你当前只用到了极少的资源,并且有使用cc.loade ...

  3. Android通用框架设计与完整电商APP开发系列文章

    作者|傅猿猿 责编|Javen205 有福利 有福利 有福利 鸣谢 感谢@傅猿猿 邀请写此系列文章 Android通用框架设计与完整电商APP开发 课程介绍 [导学视频] [课程详细介绍] 以下是部分 ...

  4. 新编辑器Cocos Creator发布:对不起我来晚了!

    1月19日,由Cocos创始人王哲亲手撰写的一篇Cocos Creator新品发布稿件在朋友圈被行业人士疯狂转载,短短数小时阅读量突破五位数.Cocos Creator被誉为“注定将揭开Cocos开发 ...

  5. 赢友网络通用框架V10.0.0(WinuAppSoft) 基础框架设计表

    /* * 版权所有:赢友网络(http://www.winu.net/) * 开发人员:新生帝(JsonLei) * 设计名称:赢友网络通用框架V10.0.0(WinuAppSoft) * 设计时间: ...

  6. 痞子衡嵌入式:嵌入式里通用微秒(microseconds)计时函数框架设计与实现

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是嵌入式里通用微秒(microseconds)计时函数框架设计与实现. 在嵌入式软件开发里,计时可以说是非常基础的功能模块了,其应用也非常 ...

  7. C++通用框架和库

    C++通用框架和库 来源 https://www.cnblogs.com/skyus/articles/8524408.html 关于 C++ 框架.库和资源的一些汇总列表,内容包括:标准库.Web应 ...

  8. 触控的手牌—Cocos Creator

    科普 Cocos Creator是触控最新一代游戏工具链的名称.如果不太清楚的,可以先看一些新闻.   新编辑器Cocos Creator发布: 对不起我来晚了! http://ol.tgbus.co ...

  9. Android 程序框架设计

    这篇文章主要内容来自于之前我讲的一个PPT文档,现在将其整理如下.欢迎指正.以下的内容都是来自于我自身的经验,欢迎大家多提自己的建议. 1.一些概念 模式的定义: 每个模式都描述了一个在我们的环境中不 ...

随机推荐

  1. C++ STL vector的学习

    vector就是一个不定长数组,vector是动态数组,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素,使用vector之前,必须包含相应的头文件和命名空间. #include <vec ...

  2. git 中文乱码-一次被坑经历

    git log和gitcommit中文出现乱码,花了大半天的时间试了网上的各种方法,还是搞不定. 只好放大招. 卸载软件后重装,还是不行.然后git config --list 发现一些奇怪的配置信息 ...

  3. 并发之初章Java内存模型

    >>>>>>博客地址<<<<<< >>>>>>首发博客<<<<< ...

  4. 手写RPC框架指北另送贴心注释代码一套

    Angular8正式发布了,Java13再过几个月也要发布了,技术迭代这么快,框架的复杂度越来越大,但是原理是基本不变的.所以沉下心看清代码本质很重要,这次给大家带来的是手写RPC框架. 完整代码以及 ...

  5. 并发编程之线程创建到销毁、常用API

    在前面一篇介绍了线程的生命周期[并发编程之多线程概念],在本篇将正式介绍如何创建.中断线程,以及线程是如何销毁的.最后,我们会讲解一些常见的线程API. 线程创建 Java 5 以前,实现线程有两种方 ...

  6. Android入门学习教程PDF免费下载

    场景 CSDN: https://blog.csdn.net/badao_liumang_qizhi 博客园: https://www.cnblogs.com/badaoliumangqizhi/ 哔 ...

  7. linux常见报错

    零.目录 一. 文件和目录类 File exist 文件已经存在 No such file or directory 没有这个文件或目录(这个东西不存在) command not found 命令找不 ...

  8. 【python】requests模块初探(一)

    一.写在前面 Requests 是用Python语言编写,基于 urllib,采用 Apache2 Licensed 开源协议的 HTTP 库.它比 urllib 更加方便,可以节约我们大量的工作,完 ...

  9. WebStorm2017.3.4版本 注册码

    http://idea.singee77.com http://im.js.cn:8888

  10. RxSwift 中的调度器

    与 ReactiveCocoa 相比,Rx 的一大优势就是更丰富的并发模型.提到并发,就不得不提多线程.在 RxSwift 中,与线程对应的概念就是调度器,本文就调度器做些介绍,包括并发调度器.串行调 ...