本文版權歸 OSChina jsongo0 全部,轉載請標明出處,以示尊重!javascript
原文:https://my.oschina.net/jsongo/blog/757871css
爲何須要websocket?
傳統的實時交互的遊戲,或服務器主動發送消息的行爲(如推送服務),若是想作在微信上,可能你會使用輪詢的方式進行,不過這太消耗資源,大量的請求也加劇了服務器的負擔,並且延遲問題比較嚴重。若是是本身開發的app,爲了解決這些問題,不少團隊會自建socket,使用tcp長連接、自定協議的方式與服務器進行相對實時的數據交互。有能力的團隊,採用這種方式天然沒什麼大問題。不太小團隊可能就要花費不少時間去調試,要解決不少難題,這個在成本上就划不來。html
H5引入了webSocket來解決網頁端的長連接問題,而微信小程序也支持websocket。這是一個很是重要的特性,因此本系列的文章會專門拿出一篇來討論websocket。
webSocket本質上也是TCP鏈接,它提供全雙工的數據傳輸。一方面能夠避免輪詢帶來的鏈接頻繁創建與斷開的性能損耗,另外一方面數據能夠是比較實時的進行雙向傳輸(由於是長連接),並且WebSocket容許跨域通訊(這裏有個潛在的跨域安全的問題,得靠服務端來解決)。目前除IE外的瀏覽器已經對webSocket支持得很好了,微信小程序再推一把以後,它會變得更加流行。前端
咱們來設計一個新的demo,一個比較有趣的小遊戲,多人版掃雷,準確地講,多人版挖黃金。
後臺代碼:https://github.com/jsongo/mime-server
前端代碼:https://github.com/jsongo/wx-mimejava
遊戲規則是這樣的:把雷換成金子,挖到金子加一分,每人輪流一次(A挖完輪到B,B挖完A才能再點擊),點中金子就算你的,也不會炸,遊戲繼續,直到把場上全部的金子都挖完遊戲才結束。跟掃雷同樣,數字也是表示周邊有幾個金子,而後用戶根據場上已經翻出來的數字來猜哪一格可能有金子。python
這種交互的遊戲難點在於,用戶的點擊操做都要傳到服務器上,並且服務器要實時的推送到其它玩家的應用上。另外用戶本身也要接收對方操做時實時傳過來的數據,這樣纔不至於重複點中同一個格子。簡單講,就是你要上報操做給服務器,而服務器也要實時給你推消息。爲了簡化整個模型,咱們規定玩家必須輪流來點擊,玩家A點完後,才能輪到玩家B,玩家B操做完,玩家A才能點。
咱們分幾步來實現這個功能。git
1、實現思路github
一、第一步,咱們要先生成掃雷的地圖場景
這個算法比較簡單,簡述一下。隨機取某行某列就能夠定位一個格子,標記成金子(-1表示金子)。mimeCnt表示要生成的金子的數量,用一樣的方式循環標記mimeCnt個隨機格子。生成完後,再用一個循環去掃描這些-1的格子,把它周邊的格子都加1,固然必須是非金子的格子才加1。代碼放在這裏。web
其中increaseArround用來把這格金子周邊的格子都加1,實現也比較簡單:
執行genMimeArr(),隨機生成結果以下:
-1表示金子。看了下貌似沒什麼問題。接下去,咱們就要接入webSocket了。
(這個是js版本的,其實生成地圖場景的工做是在後臺生成,這個js版本只是一個演示,不過算法是同樣的。)算法
二、咱們須要一個支持webSocket的服務端
本例子中,咱們使用python的tornado框架來實現(tornado提供了tornado.websocket模塊)。固然讀者也可使用socket.io,專爲webSocket設計的js語言的服務端,用起來很是簡單,它也對不支持webSocket的瀏覽器提供了兼容(flash或comet實現)。
筆者本人比較喜歡使用tornado,作了幾年後臺開發,使用最多的框架之一的就是它,NIO模型,並且很是輕量級,一樣的rps,java可能須要700-800M的內存,tornado只要30-40M,因此在一臺4G內存的機子上能夠跑上百個tornado服務,而java,對不起,只能跑3個虛擬機。微服務的時代,這一點對小公司很重要。固然若是讀者本人對java比較熟悉的話,也能夠選擇netty框架嘗試一下。
webSocket用tornado的另外一個好處是,它能夠在同一個服務(端口)上同時支持webSocket及http兩種協議。tornado的官方demo代碼中展現了怎麼實現同時使用兩種協議。在本遊戲中,能夠這麼用:用戶進入首頁,用http協議去拉取當前的房間號及數據。由於首頁是打開最多的,進了首頁的用戶不必定會玩遊戲。因此首頁還不必創建webSocket連接,webSocket連接主要用來解決頻繁請求及推送的操做。首頁只有一個請求操做。選了房間號後,進去下一個遊戲頁面再開始創建webSocket連接。
三、客戶端
使用微信小程序開發工具,直接鏈接是會報域名安全錯誤的,由於工具內部作了限制,對安全域名纔會容許鏈接。因此一樣的,這裏咱們也繼續改下工具的源碼,把相關的行改掉就行修改方式以下:
找到asdebug.js的這一行,把它改爲: if(false)便可。
if (!i(r, "webscoket"))
注:筆者目前使用IDE版本爲 0.11.112301 修改代碼部分爲 if(!(0,l.checkUrl)(o,"webscoket")) ===》 if(false&&!(0,l.checkUrl)(o,"webscoket")) 便可
懶得修改的讀者能夠直接使用我破解過的IDE。 發起一個websocket連接的代碼也比較簡單:
wx.connectSocket({ url: webSocketUrl, });
在調用這個請求代碼以前,先添加下事件監聽,這樣才知道有沒有鏈接成功:
wx.onSocketOpen(function(res){ console.log('websocket opened.'); });
鏈接失敗的事件:
wx.onSocketError(function(res){ console.log('websocket fail'); })
收到服務器的消息時觸發的事件:
wx.onSocketMessage(function(res){ console.log('received msg: ' + res.data); })
當連接創建以後,發送消息的方法以下:
wx.sendSocketMessage({ data:msg })
消息發送
因爲創建連接是須要幾回握手,須要必定的時間,因此在wx.connectSocket成功以前,若是直接wx.sendSocketMessage發送消息會報錯,這裏作一個兼容,若是鏈接還沒創建成功,則用一個數組來保存要發送的信息;而連接第一次創建時,把數據遍歷一遍,把消息拿出來一個個補發。這個邏輯咱們封裝成一個send方法,以下:
function sendSocketMessage(msg) { if (typeof(msg) === 'object') { // 只能發送string msg = JSON.stringify(msg); } if (socketOpened) { // socketOpened變量在wx.onSocketOpen時設置爲true wx.sendSocketMessage({ data:msg }); } else { // 發送的時候,連接還沒創建 socketMsgQueue.push(msg); } }
2、demo功能解析
一、首頁entry
爲了簡化模型,把重點放在webSocket上,咱們把首頁作成本身填寫房間號的形式。讀者若是本身有時間和能力的話,能夠把首頁作成一個房間列表,並顯示每一個房間有多少人在玩,只有一人的能夠進去跟他玩。甚至後面還能夠加上觀看模式,點擊別人的房間進去觀看別人怎麼玩。
填寫房間號的input組件,添加一個事件,取得它的值event.detail.value後setData到本page裏面。
點擊「開始遊戲」,再把房間號存入app的globalData裏面,而後wx.navigateTo到主遊戲頁面index。
這個頁面比較簡單。
二、主遊戲頁面
咱們封裝一個websocket/connect.js模塊,專門用來處理websocket連接。主要有兩個方法,connect發起webSocket連接,send用來發送數據。
index主頁面:
初始化狀態,9x9的格子,每一格子其實都是一個button按鈕。咱們生成的地圖場景數據,分別對應着每一格。好比1表示周邊的1個金子,0表示周邊沒有金子,-1表示這格是個金子,咱們的目標就是找到這些-1。找得越多得分越高。
這裏討論一個安全性問題。相信一句話:在前端作的安全措施大都是不靠譜的。上圖中的矩陣,每一個格子背後的數據,不該該放在前端,由於js代碼是能夠調試的,能夠下斷點在相應的變量上,就能夠看到整個矩陣數據,而後就知道哪些格子是金子,就能夠做弊,這是很是不公平的。因此最好的方法是把這些矩陣數據存在後端,每次用戶操做的時候,把用戶點擊的座標發到後臺,後臺再判斷相應的座標是什麼數據,再返回給前端。這個看似有不少數據傳輸的交互方式,實際上是不會浪費資源,由於用戶的每一個點擊操做,原本就要上報到後臺,這樣遊戲的另外一玩家才知道你點了哪一個格子。反正都是要傳數據的,因此確定要傳座標,這樣前端就徹底沒有必要知道哪一個格子是什麼數據,由於後臺的推送消息會告訴你。
這樣咱們就繞過了前端存矩陣數據的問題。可是咱們仍是須要一個數組來存儲當前矩陣狀態的,好比哪一個格子已經被翻開,裏面是什麼數據,也就是說要存儲場上已經被打開的格子。因此在後臺,咱們要存儲兩個數據,一個是全部的矩陣數據,也就是地圖場景數據;另外一個是當前狀態的數據,這個要用來同步雙方的界面。
三、結束頁面
遊戲結束的判斷條件,就是場上全部的金子都被挖完了。這個條件也是在後臺判斷的。
在每次用戶挖到金子的時候,後臺都會多一個判斷邏輯,就是看這個金子是不是最後一個。若是是的話,就發送一個over類型的消息給遊戲的全部玩家。
玩家終端接收到這個消息時,就會結束當前的遊戲,並跳到結束頁面。
沒有專門的設計師,隨便網上偷了張圖片貼上去,界面比較醜。下方顯示本身的得分和當前的房間號。
一、代碼結構
前端代碼,分了幾個模塊:pages放全部的頁面,common放通用的模塊,mime放挖金子的主邏輯(暫時沒用到),res放資源文件,websocket放webSocket相關的處理邏輯。
後臺代碼,讀者稍微瞭解一下就好了,不討論太多。裏面我放了docker文件,熟悉docker的讀者能夠直接一個命令跑起整個服務端。筆者在本身的服務器上跑了這個webSocket服務,ip和端口已經寫在前端代碼裏,讀者輕虐。可能放不久,讀者能夠本身把這個服務跑起來。
二、消息收發
(1)消息協議
咱們簡單地定義下,消息的格式以下。 發送消息:
{type: 'dig', …}
type是必帶字段。
服務器返回的消息:
{errCode: 0, data: {type: 'dig', …} }
errCode爲0的時候,表示請求處理成功,後面帶上data字段表示返回的數據,裏面的type也是必帶字段,表示的是什麼類型的消息。
由於webSocket類型的消息跟傳統的http請求不太同樣,http請求沒有狀態,一個請求過去,一下子就返回,返回的數據確定是針對這個請求的。而webSocket的模型是這樣的:客戶端發過去不少請求,而後也不知道服務器返回的數據哪一個是對應哪一個請求,因此須要一個字段來把全部的返回分紅多種類型,並進行相應的處理。
(2)發送消息
發送消息就比較容易了,上面咱們定義了一個send方法及未鏈接成功時的簡單的消息列表。
(3)接收消息
讀者在閱讀代碼的時候,可能會有一個疑惑,websocket/connect.js裏只有send發送方法,而沒有接收推送消息的處理,那接收消息的處理在哪?怎麼關聯起來的?
websocket/目錄裏面還有另外一個文件,msgHandler.js,它就是用來處理接收消息的主要處理模塊:
從服務器推送過來的消息,主要有這三種類型:1挖金子操做,多是本身的操做,也多是對方的操做,裏面有一個字段isMe來表示是不是本身的操做。接收到這類消息時,會翻轉地圖上相應的格子,並顯示出挖的結果。2建立或進入房間的操做,一個房間有兩個用戶玩,建立者先開始。3遊戲結束的消息,當應用接收到這類消息時,會直接跳轉到結束頁面。
這個處理邏輯,是在websocket/connect.js的wx.onSocketMessage回調裏關聯上的。
在消息的收發過程當中,每一個消息交互,調試工具都會記錄下來。能夠在調試工具裏看到,在NetWork->WS裏就能夠看到:
三、前端挖金子
代碼以下:
var websocket = require('../../websocket/connect.js'); var msgReceived = require('../../websocket/msgHandler.js'); Page({ data: { mimeMap: null, leftGolds: 0, // 總共有多少金子 score: 0, // 個人得分 roomNo: 0 // 房間號 }, x: 0, // 用戶點中的列 y: 0, // 用戶點中的行 onLoad: function () { var roomNo = app.getRoomNo(); this.setData({ roomNo: roomNo }); // test // websocket.send('before connection'); if (!websocket.socketOpened) { // setMsgReceiveCallback websocket.setReceiveCallback(msgReceived, this); // connect to the websocket websocket.connect(); websocket.send({ type: 'create' }); } else { websocket.send({ type: 'create', no: roomNo }); } }, digGold: function(event) { // 不直接判斷,而把座標傳給後臺判斷 // 被開過的就無論了 if (event.target.dataset.value < 9) { return; } // 取到這格的座標 this.x = parseInt(event.target.dataset.x); this.y = parseInt(event.target.dataset.y); console.log(this.x, this.y); // 上報座標 this.reportMyChoice(); }, reportMyChoice: function() { roomNo = app.getRoomNo(); websocket.send({ type: 'dig', x: this.x, y: this.y, no: roomNo }); }, });
在page的onLoad事件裏,先更新界面上的房間號信息。而後開始咱們的重點,websocket.connect發起webSocket連接,websocket是咱們封裝的模塊。而後把咱們msgHandler.js處理邏輯設置到服務端推送消息回調裏面。接着,發送一個create消息來建立或加入房間。服務端會對這個消息作出響應,返回本房間的當前狀態數據。
digGold是每一個格子的點擊事件處理函數。這兒有一個邏輯,一個格子周邊最多有8個格子,因此每一個格子的數據最大不可能大於8,上面代碼中能夠看到有一個9,這實際上是爲了跟0區分,用來表示場上目前的還沒被翻開的格子的數據,用9來表示,固然你也能夠用10,100都行。
wxml的矩陣數據綁定代碼以下:
<view wx:for="{{mimeMap}}" wx:for-item="row" wx:for-index="i" class="flex-container"> <button wx:for="{{row}}" wx:for-item="cell" wx:for-index="j" class="flex-item {{cell<0?'gold':''}} {{cell<9?'open':''}}" bindtap="digGold" data-x="{{j}}" data-y="{{i}}" data-value="{{cell}}"> {{cell<9?(cell<0?'*':cell):"-"}} </button> </view>
這個比較複雜些。兩層for,第一層遍歷行,第二層遍歷行裏的每一個格子的數據。wx:for-item指定裏面用到的item的名字,wx:for-index指定裏面用到的key的名字。每一個格子是一個button,class值作了兩次判斷,若是這個格子的數據小於0,表示它是金子,加上gold這個class,主要是爲了給它加樣式。而當數據是非9的時候,表示這個格子被翻開了,這時就加上open樣式,把顏色設置成橙色。data-x和data-y用來記錄格子的座標,這樣的話,用戶點擊的時候,就能夠直接從dataset裏取出座標值,再把這個值發到服務端進行判斷。
四、服務端實現
簡單的提一下就好,由於後臺不是本系列文章的重點,雖然這個demo的開發也花了大半的時候在寫後臺。先後端的消息交互,藉助了webSocket通道,傳輸咱們本身定義格式的內容。上面有個截圖顯示了後臺代碼目錄的結構,劃分得比較隨意,handlers裏存放了的是主要的處理邏輯。webSocketHandler是入口,在它的on_message裏,對收到的客戶端的消息,根據類型進行分發,dig類型,分發到answerHandler去處理,create類型,分發到roomHandler裏去處理。
還有一點稍微提一下,本例子中的後臺webSocket消息處理也跟傳統的http處理流程有一點不同。就是在最後返回的時候,不是直接返回的,而是廣播的形式,把消息發送給全部的人。好比用戶A點擊了格子,後臺收到座標後,會把這個座標及座標裏的數據一塊兒發送給房間裏的全部人,而不是單獨返回給上報座標的人。只是會有一個isMe字段來告訴客戶端是不是本身的操做。
總之,在作webSocket開發的時候,上面提到的,先後端均可能會有一些地方跟傳統的http接口開發不太同樣。讀者嘗試在作webSocket項目的時候,轉換一下思惟。
最後提下一個注意點:微信小程序的websocket連接是全局只能有一個,官方提示:「一個微信小程序同時只能有一個 WebSocket 鏈接,若是當前已存在一個 WebSocket 鏈接,會自動關閉該鏈接,並從新建立一個 WebSocket 鏈接。」
本文首發地址:http://www.jsongo.com/post/js/2016/weapp-7/
oschina 上同步發佈。