使用cocoscreator + node.js + websocket實現簡單的聊天服務
阿新 • • 發佈:2018-12-16
先上個效果圖:
使用cocoscreator 1.9.1 + node.js + websocket實現,沒有使用socket.io, 全部自己封裝,長連線進行封裝後可以和短連線使用方法一樣,使用簡單,方便以後開發網路遊戲。
1、客戶端:
主要就是聊天內容的顯示,自動換行和背景擴充套件,程式碼大概如下:
cc.Class({ extends: cc.Component, properties: { msgLabel: cc.Label, uidLabel: cc.Label, msgLayout: cc.Layout, msgBg: cc.Node, maxLen: 500, }, // LIFE-CYCLE CALLBACKS: // onLoad () {}, start () { this.node.runAction(cc.fadeTo(0.5, 255)) }, initMsg(msg, uid){ this.msgLabel.string = msg; this.uidLabel.string = uid; this.msgLabel.overflow = cc.Label.Overflow.NONE; // this.msgBg.width = this.msgLabel.node.width + 10; // this.msgBg.height = this.msgLabel.node.height + 10; // this.node.height = this.msgBg.height + 40; this.scheduleOnce((dt)=>{ if ( this.msgLabel.node.width >= this.maxLen){ this.msgLabel.overflow = cc.Label.Overflow.RESIZE_HEIGHT; this.msgLabel.node.width = this.maxLen; } this.msgBg.width = this.msgLabel.node.width + 10; this.msgBg.height = this.msgLabel.node.height + 10; this.node.height = this.msgBg.height + 40; }, 0); this.node.opacity = 0; } // update (dt) {}, });
網路部分分成了四層:
1、socket 封裝基礎的websocket, 這裡是最底層,也是真正連結的開始
2、network 控制socket連結層,實現各回調介面
3、netproxy 封裝各服務功能,把長連線變成短連線的請求方式
4、netprotocols 和伺服器協商,確定每個請求的請求體格式和回覆格式
各部分程式碼如下:
GameWebSocket.js:
/** * @enum {number} */ var GameWebSocketState = cc.Enum({ CONNECTING: 1, OPEN:2, CLOSING: 3, CLOSED: 4 }); /** * @interface */ var GameWebSocketDelegate = cc.Class({ onSocketOpen: function () { }, /** * 收到了訊息 * @param {string|Uint8Array} data */ onSocketMessage: function (data) { }, onSocketError: function () { }, /** * 連線關閉 * @param {string} reason */ onSocketClosed: function (reason) { } }); /** * @interface */ var GameWebSocketInterface = cc.Class({ connect: function () { }, send: function () { }, close: function () { }, getState: function () { } }); var GameWebSocket = cc.Class({ extends: GameWebSocketInterface, properties: { /** * @type {String} 伺服器地址 */ _address: null, /** * @type {GameWebSocketDelegate} */ _delegate: null, /** * @type {WebSocket} */ _webSocket: null, }, /** * @param {string} address 伺服器地址 * @param {GameWebSocketDelegate} delegate 回撥介面 */ init: function(address, delegate){ this._address = address; this._delegate = delegate; this._webSocket = null; }, connect: function () { cc.log('connect to '+ this._address); var ws = this._webSocket = new WebSocket(this._address); ws.onopen = this._delegate.onSocketOpen.bind(this._delegate); ws.onmessage = function (param) { this._delegate.onSocketMessage(param.data); }.bind(this); ws.onerror = this._delegate.onSocketError.bind(this._delegate); // function({code: Number, reason: String, wasClean: Boolean})} ws.onclose = function (param) { this._delegate.onSocketClosed(param.reason); }.bind(this); }, /** * 傳送資料 * @param {string|Uint8Array} stringOrBinary */ send: function (stringOrBinary) { this._webSocket.send(stringOrBinary); }, close: function () { if (!this._webSocket) { return; } try { this._webSocket.close(); } catch (err) { cc.log('error while closing webSocket', err.toString()); } this._webSocket = null; }, getState: function () { if (this._webSocket) { switch(this._webSocket.readyState){ case WebSocket.OPEN: return GameWebSocketState.OPEN; case WebSocket.CONNECTING: return GameWebSocketState.CONNECTING; case WebSocket.CLOSING: return GameWebSocketState.CLOSING; case WebSocket.CLOSED: return GameWebSocketState.CLOSED; } } return GameWebSocketState.CLOSED; } }); module.exports = { GameWebSocketState: GameWebSocketState, GameWebSocketDelegate: GameWebSocketDelegate, GameWebSocketInterface: GameWebSocketInterface, GameWebSocket: GameWebSocket };
GameNetwork.js
/** * Created by skyxu on 2018/10/9. */ "use strict"; let GameWebSocket = require("./GameWebSocket"); let GameProtocols = require("./GameProtocols"); /** * 伺服器回覆訊息狀態,判斷回覆訊息的各種問題 */ var response_state = { ERROR_OK : '0' }; /** * 請求回撥物件,收到伺服器回撥後的回撥方法 */ var NetworkCallback = cc.Class({ properties: { /** * @type {BaseRequest} request */ request: null, /** * 請求回撥對方法 */ callback: null }, /** * @param {BaseRequest} request * @param {function(BaseResponse): boolean} callback */ init: function (request, callback) { this.request = request; this.callback = callback; } }); let GameNetwork = cc.Class({ extends: GameWebSocket.GameWebSocketDelegate, ctor: function() { this._socket = null; this._delegate = null; /** * 每次傳送請求,都需要有一個唯一的編號 * @type {number} * @private */ this._requestSequenceId = 0; /** * 接受伺服器主動下發的response回撥 * key 表示BaseResponse.act * @type {Object.<string, function(object.<string, *>)>} */ this.pushResponseCallback = {}; /** * 根據seq儲存Request和其callback,以便在收到伺服器的響應後回撥 * @type {Object.<int, NetworkCallback>} * @private */ this._networkCallbacks = {}; }, setDelegate: function (delegate) { this._delegate = delegate; }, /** * 註冊伺服器主動推送的response 回撥 */ registerPushResponseCallback : function(act, callback){ this.pushResponseCallback[act] = callback; }, /** * 判斷socket已連線成功,可以通訊 * @returns {boolean} */ isSocketOpened: function(){ return (this._socket && this._socket.getState() == GameWebSocket.GameWebSocketState.OPEN); }, isSocketClosed: function () { return this._socket == null; }, /** * 啟動連線 */ connect: function (url) { cc.log("webSocketUrls=" + url); this._requestSequenceId = 0; this._socket = new GameWebSocket.GameWebSocket(); this._socket.init(url, this); this._socket.connect(); }, closeConnect: function () { if(this._socket){ this._socket.close(); } }, onSocketOpen: function () { cc.log('Socket:onOpen'); if(this._delegate && this._delegate.onNetworkOpen){ this._delegate.onNetworkOpen(); } }, onSocketError: function () { cc.log('Socket:onError'); }, onSocketClosed: function (reason) { cc.log('Socket:onClose', reason); if (this._socket) { this._socket.close(); } this._socket = null; if(this._delegate && this._delegate.onNetworkClose){ this._delegate.onNetworkClose(); } }, onSocketMessage: function (msg) { this._onResponse(msg); }, _onResponse: function(responseData){ cc.log('response->resp:', responseData); var responseJson = JSON.parse(responseData); var responseClass = GameProtocols.response_classes[responseJson.act]; /** * @type {object.<BaseResponse>} */ var response = new responseClass(); response.loadData(responseJson.data); response.act = responseJson.act; response.seq = responseJson.seq; response.err = responseJson.err; response.ts = responseJson.ts; // 如果指定了回撥函式,先回調 var ignoreError = false; if(response.seq != -1){ // 處理伺服器推送訊息 var pushCallback = this.pushResponseCallback[response.act]; if(pushCallback){ pushCallback(response); } // request回撥 var callbackObj = this._networkCallbacks[response.seq]; if(callbackObj){ ignoreError = callbackObj.callback(response); // try { // ignoreError = callbackObj.callback(response); // } catch (err) { // cc.log(err + " error in response callback of " + response.act); // } finally { // delete this._networkCallbacks[response.seq]; // } } } //有錯,且不忽略,則統一處理錯誤 if(response.err && response.err != response_state.ERROR_OK && !ignoreError){ if (response.is_async) { // 非同步請求,如果出錯了,應該需要重新登入 // todo 重新登入?或者重新同步資料? } else { // 同步請求,如果出錯了,需要顯示錯誤資訊 // todo 顯示錯誤 var msg = responseJson.msg; cc.log('server err ' + msg); } } }, /** * 向伺服器傳送請求。 * * 如果提供了callback,在收到response後會被回撥。如果response是一個錯誤(status!=ERR_OK),則需要決定由誰來負責處理錯誤。 * 如果callback中已經對錯誤進行了處理,應該返回true,這樣會忽略該錯誤。否則應該返回false,則負責處理該錯誤。 * * 特別注意:如果這是一個非同步(is_async)請求,且出錯,一般來講應該重新登入/同步。但是如果callback返回了true,不會進行 * 任何處理,也就是不會重新登入/同步。請小心確定返回值。 * * @param {object.<BaseRequest>} * @param {function(BaseResponse): boolean=} opt_callback 回撥函式。出錯的情況下,如果返回true,則不會再次處理錯誤。 */ sendRequest: function (request, opt_callback) { // 每個請求的seq應該唯一,且遞增 request.seq = ++this._requestSequenceId; //生成NetworkCallback物件,繫結請求seq和回撥方法 if(opt_callback){ this._networkCallbacks[request.seq] = new NetworkCallback(); this._networkCallbacks[request.seq].init(request, opt_callback); } this._sendSocketRequest(false, request); }, /** * sendRequest的不傳送data欄位 */ sendRequestNoData: function (request, opt_callback) { // 每個請求的seq應該唯一,且遞增 request.seq = ++this._requestSequenceId; //生成NetworkCallback物件,繫結請求seq和回撥方法 if(opt_callback){ this._networkCallbacks[request.seq] = new NetworkCallback(); this._networkCallbacks[request.seq].init(request, opt_callback); } this._sendSocketRequest(true, request); }, /** * @param {Boolean} isNoData * @param {object.<BaseRequest>} req */ _sendSocketRequest: function (isNoData, req) { cc.assert(this._socket); if (this.isSocketOpened()){ //通過json的方法生成請求字串 var msg = null; if(isNoData){ msg = JSON.stringify({seq:req.seq, act:req.act}); }else{ msg = JSON.stringify({seq:req.seq, act:req.act, data:req}); } cc.log("WebSocketDelegate::send->" + msg); this._socket.send(msg); } else{ // todo } } }); module.exports = GameNetwork;
GameProtocols.js
/** * Created by skyxu on 2018/10/9. */ "use strict"; /** * 訊息基類物件,請求訊息BaseRequest, 回撥訊息BaseResponse都繼承BaseProtocol */ let BaseProtocol = cc.Class({ ctor: function () { /** * 請求動作型別 */ this.act = ''; /** * 每個請求的sequence_id應該唯一 */ this.seq = 0; /** * 錯誤程式碼,0為正常 */ this.err = 0; /** * 是否需要等待伺服器回撥 */ this.is_async = false; } }); /** * 請求訊息基類,客戶端的請求都繼承這個類 */ let BaseRequest = cc.Class({ extends: BaseProtocol }); /** * 伺服器返回的訊息對應的物件,包含返回資料,一般和BaseRequest成對使用 * @class BaseResponse * @extends BaseProtocol */ let BaseResponse = cc.Class({ extends: BaseProtocol, /** * 讀取返回資料,設定BaseResponse物件 */ loadData: function (data) { var key; for (key in data) { if(!this.hasOwnProperty(key)){ continue; } if(data[key] !== undefined && data[key] !== null){ this[key] = data[key]; } } } }); let HeartRequest = cc.Class({ extends: BaseRequest, ctor(){ this.act = 'heart'; this.t = -1; // 傳送時間 } }); let HeartResponse = cc.Class({ extends: BaseResponse, ctor(){ this.act = 'heart'; this.t = -1; } }); let ChatRequest = cc.Class({ extends: BaseRequest, ctor(){ this.act = 'chat'; this.msg = ''; this.uid = ''; } }); let ChatResponse = cc.Class({ extends: BaseResponse, ctor(){ this.act = 'chat'; this.msg = ''; this.uid = ''; } }); let LoginRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = 'login'; /** * facebook使用者的accessToken,或遊客的UUID */ this.token = ''; /** * token來源,預設0:遊客,1:facebook */ this.origin = 0; /** * 平臺: 必須為以下幾種之一:android/ios/winphone/pc */ this.os = ''; /** * 平臺系統版本 */ this.osVersion = ''; /** * 裝置產品型號, 示例 iPhone8,2, SM-G 9280 */ this.deviceModel = ''; /** * 渠道ID */ this.channelId = 0; /** * Ios裝置廣告標示符 */ this.idfa = ''; /** * 安卓裝置id */ this.androidId = ''; /** * Google廣告平臺賬號,安裝了google play的裝置可取到 */ this.googleAid = ''; /** * 應用版本號 */ this.appVersion = ''; /** * 取package name或者bundle id */ this.packName = ''; /** * 裝置語言 * @type {string} */ this.language = ''; this.locale = ""; } }); let LoginResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'login'; /** * 遊客第一次登入時返回的token,需要客戶端儲存 */ this.token = ''; /** * 離體力下次恢復點的剩餘時間秒數 * @type {number} */ this.spStepLeftTime = 0; /** * 體力恢復週期 * @type {Number} */ this.spInterval = 0; /** * 農場每天產出量,產出未解鎖時為-1 * @type {number} */ this.farmDailyOut = -1; /** * 農場已產出量 * @type {number} */ this.farmCoins = 0; /** * 農場產出間隔 * @type {number} */ this.farmInterval = null; /** * 用json object表示的一個player物件,欄位說明參見player json物件 */ this.me = {}; /** * 建築資料陣列 * @type {Array} */ this.buildings = []; /** * 農民資料陣列 * @type {Array} */ this.farms = []; /** * 富豪資料 */ this.cashking = {}; /** * 行星配置 */ this.planetConf = {}; /** * 農民配置 */ this.farmConfList = []; /** * 其他配置 */ this.settingConf = {}; /** * 好友資料 */ this.friends = []; /** * 好友通緝的目標列表 */ this.helpWantList = []; /** * 郵件訊息列表 */ this.newsList = []; /** * 復仇列表 */ this.revengeList = []; /** * 商品資訊 * @type {Array} */ this.rechargeConfs = []; /** * 總島數 * @type {Number} */ this.planetConfListSize = 0; /** * 他人行星資訊物件,僅在轉到fire斷線重新登入時有效 * @type {Object} */ this.fireTarget = null; /** * 他人行星資訊物件列表,僅在轉到steal斷線重新登入時有效 * @type {Array} */ this.stealTarget = null; } }); let LogoutRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = 'logout'; } }); let LogoutResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'logout'; } }); /** * 繫結fb賬號 * @extends BaseRequest */ let BindFacebookRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = 'bindFb'; /** * facebook使用者的accessToken,或遊客的UUID */ this.token = ''; } }); /** * 繫結fb賬號 * @extends BaseResponse */ let BindFacebookResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'bindFb'; /** * fb資料 */ this.me = 0; /** * fb好友 */ this.friends = 0; } }); let SpinRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = 'spin'; /** * 倍數 * @type {Number} */ this.x = 1; } }); let SpinResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'spin'; /** * 搖中的轉盤ID */ this.hit = 0; /** * 轉到護盾,但護盾已滿時,存在 * @type {number} */ this.shieldfull = 0; /** * 玩家資料物件 */ this.me = {}; /** * 他人行星資訊物件,僅在轉到fire時有效 * @type {*} */ this.fireTarget = {}; /** * 偷取物件資料 */ this.stealTarget = []; /** * 離體力下次恢復點的剩餘時間秒數 * @type {number} */ this.spStepLeftTime = 0; /** * 體力恢復週期 * @type {Number} */ this.spInterval = 0; /** * 倍數 * @type {Number} */ this.x = 1; } }); /** * 獲取排名 * @extends BaseRequest */ let RankRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = 'rankboard'; /** * 請求動作型別{ 0全部,1本地,2好友 } * @type {int} */ this.type = 0; } }); /** * 獲取排名 * @extends BaseResponse */ let RankResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'rankboard'; /** * 我的排名 */ this.myRank = 0; /** * 排名玩家資料 */ this.men = []; } }); //push------------------------------------------------------------------------------ /** * 推送訊息 被攻擊 * @extends BaseResponse */ var PushAttackedResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'attacked'; /** * 玩家更新資料 */ this.me = null; /** * 建築資料 */ this.building = null; /** * 敵人 */ this.hatredman = null; /** * 訊息 */ this.news = null; } }); /** * 推送訊息 推送訊息好友已贈送體力 * @extends BaseResponse */ var PushSendSpResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'sendSpNotify'; /** * 好友物件 */ this.friend = null; } }); /** * 推送訊息 推送訊息好友已領取贈送的體力 * @extends BaseResponse */ var PushTakeSpResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'takeSpNotify'; /** * 好友物件 */ this.friend = null; } }); /** * 推送訊息 同步好友資訊 * @extends BaseResponse */ var PushSyncFriendInfo = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'friendInfoSync'; /** * 好友 */ this.friend = null; } }); /** * 推送訊息 新增好友 * @extends BaseResponse */ var PushAddNewFriend = cc.Class({ extends: BaseResponse, ctor: function () { this.act = 'newFriend'; /** * 好友 */ this.friend = null; /** * 訊息 */ this.news = null; } }); /** * debug回撥 * @extends BaseRequest */ let DebugChangeMeRequest = cc.Class({ extends: BaseRequest, ctor: function () { this.act = "cmdTest"; //請求動作型別 this.cmd = ""; // "player coins add 100", cmd格式:player field value 或者 player field add value // Building field [add] value where playerId value type value } }); /** * debug回撥 * @extends BaseResponse */ let DebugChangeMeResponse = cc.Class({ extends: BaseResponse, ctor: function () { this.act = "cmdTest"; /** * 玩家資料 * @type {Object} */ this.me = {}; /** * 體力恢復週期 * @type {Number} */ this.spInterval = null; /** * 體力恢復剩餘時間 * @type {Number} */ this.spStepLeftTime = null; /** * 存錢罐速度 * @type {Number} */ this.farmDailyOut = null; /** * 存錢罐可回收金幣 * @type {Number} */ this.farmCoins = null; /** * 存錢罐回收週期 * @type {Number} */ this.farmInterval = null; /** * 島嶼建築資料 * @type {Array} */ this.buildings = null; } }); let response_classes = { login: LoginResponse, logout: LogoutResponse, spin: SpinResponse, bindFb: BindFacebookResponse, rankboard: RankResponse, heart: HeartResponse, chat: ChatResponse, //push attacked: PushAttackedResponse, sendSpNotify: PushSendSpResponse, takeSpNotify: PushTakeSpResponse, newFriend: PushAddNewFriend, friendInfoSync: PushSyncFriendInfo, // debug cmdTest: DebugChangeMeResponse, }; module.exports = { LoginRequest: LoginRequest, LoginResponse: LoginResponse, LogoutRequest: LogoutRequest, LogoutResponse: LogoutResponse, SpinRequest: SpinRequest, SpinResponse: SpinResponse, BindFacebookRequest: BindFacebookRequest, BindFacebookResponse: BindFacebookResponse, RankRequest: RankRequest, RankResponse: RankResponse, HeartRequest: HeartRequest, HeartResponse: HeartResponse, ChatRequest: ChatRequest, ChatResponse: ChatResponse, // debug DebugChangeMeRequest: DebugChangeMeRequest, DebugChangeMeResponse: DebugChangeMeResponse, //push訊息 PushAttackedResponse: PushAttackedResponse, PushSendSpResponse: PushSendSpResponse, PushTakeSpResponse: PushTakeSpResponse, PushAddNewFriend: PushAddNewFriend, PushSyncFriendInfo: PushSyncFriendInfo, response_classes: response_classes };
NetProxy.js
/** * Created by skyxu on 2018/10/9. */"use strict";let GameNetwork = require("./GameNetwork");let GameProtocols = require("./GameProtocols");let GAME_SERVER_URL = 'ws://127.0.0.1:3000';// GAME_SERVER_URL = 'wss://echo.websocket.org';let NetProxy = cc.Class({ ctor: function () { this.network = null; this._cachePushCallback = []; }, init: function () { this.network = new GameNetwork(); this.network.setDelegate(this); this.initPushCallback(); }, connect: function () { this.network.connect(GAME_SERVER_URL); }, closeConnect: function () { this.network.closeConnect(); }, isNetworkOpened: function () { return this.network.isSocketOpened(); }, isNetworkClosed: function () { return this.network.isSocketClosed(); }, onNetworkOpen: function () { Global.eventMgr.emit(Global.config.EVENT_NETWORK_OPENED); }, onNetworkClose: function () { Global.eventMgr.emit(Global.config.EVENT_NETWORK_CLOSED); }, /** * 註冊push回撥介面 */ initPushCallback: function () { let self = this; let pushCallback = function (resp) { self.pushCallback(resp); }; this.network.registerPushResponseCallback('chat', pushCallback); // let pushCallback = function(response){ // if(Util.DNN(farm.game) && farm.game.loginSuccess){ // this.dealCachePush(); // this.pushCallback(response); // }else{ // this._cachePushCallback.push(response); // } // }.bind(this); // this.network.registerPushResponseCallback('attacked', pushCallback); // this.network.registerPushResponseCallback('acceptWantHelp', pushCallback); // this.network.registerPushResponseCallback('sendSpNotify', pushCallback); // this.network.registerPushResponseCallback('takeSpNotify', pushCallback); // this.network