為什麼需要websocket?
傳統的即時互動的遊戲,或伺服器主動發送訊息的行為(如推播服務),如果想做在微信上,可能你會使用輪詢的方式進行,不過這太消耗資源,大量的請求也加重了伺服器的負擔,而且延遲問題比較嚴重。如果是自己開發的app,為了解決這些問題,很多團隊會自建socket,使用tcp長連結、自訂協定的方式與伺服器進行相對即時的資料互動。有能力的團隊,採用這種方式自然沒什麼大問題。不過小團隊可能要花很多時間去調試,要解決很多難題,這個在成本上就劃不來。
H5引進了webSocket來解決網頁端的長連結問題,而微信小程式也支援websocket。這是一個非常重要的特性,所以本系列的文章會特別拿出一篇來討論websocket。
webSocket本質上也是TCP連接,它提供全雙工的資料傳輸。一方面可以避免輪詢帶來的連接頻繁建立與斷開的性能損耗,另一方面數據可以是比較實時的進行雙向傳輸(因為是長鏈接),而且WebSocket允許跨域通信(這裡有個潛在的跨域安全性的問題,得靠服務端來解決)。目前除IE外的瀏覽器已經對webSocket支援得很好了,微信小程式再推一把之後,它會變得更加流行。
我們來設計一個新的demo,一個比較有趣的小遊戲,多人版掃雷,準確地講,多人版挖黃金。
遊戲規則是這樣的:把雷換成金子,挖到金子加一分,每人輪流一次(A挖完輪到B,B挖完A才能再點擊),點中金子就算你的,也不會炸,遊戲繼續,直到把場上所有的金子都挖完遊戲才結束。跟掃雷一樣,數字也是表示週邊有幾個金子,然後用戶根據場上已經翻出來的數字來猜哪一格可能有金子。
這種互動的遊戲困難在於,使用者的點擊操作都要傳到伺服器上,而且伺服器要即時的推送到其它玩家的應用程式上。另外用戶自己也要接收對方操作時即時傳過來的數據,這樣才不至於重複點中同一個格子。簡單講,就是你要上報操作給伺服器,而伺服器也要即時給你推訊息。為了簡化整個模型,我們規定玩家必須輪流點擊,玩家A點完後,才能輪到玩家B,玩家B操作完,玩家A才能點。
我們分幾步來實現這個功能。
1、第一步,我們要先生成掃雷的地圖場景
這個演算法比較簡單,簡述一下。隨機取某行某列就可以定位一個格子,標記成金子(-1表示金子)。 mimeCnt表示要產生的金子的數量,用同樣的方式循環標記mimeCnt個隨機格子。生成完後,再用一個循環去掃描這些-1的格子,把它週邊的格子都加1,當然必須是非金子的格子才加1。代碼放在 這裡 。
其中increaseArround用來把這格金子週邊的格子都加1,實作也比較簡單:
執行genMimeArr(),隨機產生結果如下:
-1表示金子。看了下貌似沒什麼問題。接下去,我們就要接入webSocket了。
(這個是js版本的,其實生成地圖場景的工作是在後台生成,這個js版本只是一個演示,不過演算法是一樣的。)
2、我們需要一個支援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連結。
3、客戶端
使用微信小程式開發工具,直接連線是會報網域安全錯誤的,因為工具內部做了限制,對安全網域才會允許連線。所以同樣的,這裡我們也繼續改下工具的源碼,把相關的行改掉就行修改方式如下:
#找到asdebug.js的這一行,把它改成: if(false)即可。
`if (!i(r, "webscoket"))
懶得修改的讀者可以直接使用我破解過的IDE。 發起一個websocket連結的程式碼也比較簡單:
`wx.connectSocket({ url: webSocketUrl, });
在调用这个请求代码之前,先添加下事件监听,这样才知道有没有连接成功:<br /> `
wx.onSocketOpen(function(res){ console.log('websocket opened.'); });
rreee`连接失败的事件:
wx.onSocketError(function(res){ console.log('websocket fail'); }) `
` wx.onSocketMessage(function(res){ console.log('received msg: ' + res.data); })
当链接建立之后,发送消息的方法如下:<br /> `
為了簡化模型,把重點放在webSocket上,我們把首頁做成自己填寫房間號碼的形式。讀者如果自己有時間和能力的話,可以把首頁做成一個房間列表,並顯示每個房間有多少人在玩,只有一人的可以進去跟他玩。甚至後面還可以加上觀看模式,點擊別人的房間進去觀看別人怎麼玩。
填入房間號碼的input元件,新增一個事件,取得它的值event.detail.value後setData到本page裡面。
點擊“開始遊戲”,再把房間號碼存入app的globalData裡面,然後wx.navigateTo到主遊戲頁面index。
這個頁面比較簡單。
2、主遊戲頁面
我們封裝一個websocket/connect.js模組,專門用來處理websocket連結。主要有兩個方法,connect發起webSocket鏈接,send用來發送資料。
index主頁:
初始化狀態,9x9的格子,每一個格子其實都是button按鈕。我們產生的地圖場景數據,分別對應每一格。例如1表示週邊的1個金子,0表示週邊沒有金子,-1表示這格是個金子,我們的目標就是找到這些-1。找越多得分越高。
這裡討論一個安全性問題。相信一句話:在前端做的安全措施大都是不靠譜的。上圖中的矩陣,每個格子背後的數據,不應該放在前端,因為js程式碼是可以調試的,可以下斷點在相應的變數上,就可以看到整個矩陣數據,然後就知道哪些格子是金子,就可以作弊,這是非常不公平的。所以最好的方法是把這些矩陣資料存在後端,每次使用者操作的時候,把使用者點擊的座標發到後台,後台再判斷對應的座標是什麼數據,再回到前端。這個看似有很多資料傳輸的互動方式,其實是不會浪費資源,因為用戶的每個點擊操作,本來就要上報到後台,這樣遊戲的另一玩家才知道你點了哪個格子。反正都是要傳數據的,所以一定要傳座標,這樣前端就完全沒有必要知道哪個格子是什麼數據,因為後台的推播訊息會告訴你。
這樣我們就繞過了前端存矩陣資料的問題。但是我們還是需要一個數組來儲存當前矩陣狀態的,例如哪個格子已經被翻開,裡面是什麼數據,也就是說要儲存場上已經被打開的格子。所以在後台,我們要儲存兩個數據,一個是所有的矩陣數據,也就是地圖場景資料;另一個是目前狀態的數據,這個要用來同步雙方的介面。
3、結束頁面
遊戲結束的判斷條件,就是場上所有的金子都被挖完了。這個條件也是在後台判斷的。
在每次使用者挖到金子的時候,後台都會多一個判斷邏輯,就是看這個金子是否是最後一個。如果是的話,就發送一個over類型的訊息給遊戲的所有玩家。
當玩家終端接收到這個訊息時,就會結束目前的遊戲,並跳到結束頁面。
没有专门的设计师,随便网上偷了张图片贴上去,界面比较丑。下方显示自己的得分和当前的房间号。
1、代码结构
前端代码,分了几个模块:pages放所有的页面,common放通用的模块,mime放挖金子的主逻辑(暂时没用到),res放资源文件,websocket放webSocket相关的处理逻辑。
后台代码,读者稍微了解一下就行了,不讨论太多。里面我放了docker文件,熟悉docker的读者可以直接一个命令跑起整个服务端。笔者在自己的服务器上跑了这个webSocket服务,ip和端口已经写在前端代码里,读者轻虐。可能放不久,读者可以自己把这个服务跑起来。
2、消息收发
(1)消息协议
我们简单地定义下,消息的格式如下。 发送消息:
`{type: 'dig', …}
服务器返回的消息:
{errCode: 0, data: {type: 'dig', …} }
因为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里就可以看到:
3、前端挖金子
代码如下:
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>
4、服务端实现
简单的提一下就好,因为后台不是本系列文章的重点,虽然这个demo的开发也花了大半的时候在写后台。前后端的消息交互,借助了webSocket通道,传输我们自己定义格式的内容。上面有个截图显示了后台代码目录的结构,划分得比较随意,handlers里存放了的是主要的处理逻辑。webSocketHandler是入口,在它的on_message里,对收到的客户端的消息,根据类型进行分发,dig类型,分发到answerHandler去处理,create类型,分发到roomHandler里去处理。
还有一点稍微提一下,本例子中的后台webSocket消息处理也跟传统的http处理流程有一点不一样。就是在最后返回的时候,不是直接返回的,而是广播的形式,把消息发送给所有的人。比如用户A点击了格子,后台收到坐标后,会把这个坐标及坐标里的数据一起发送给房间里的所有人,而不是单独返回给上报坐标的人。只是会有一个isMe字段来告诉客户端是否是自己的操作。
总之,在做webSocket开发的时候,上面提到的,前后端都可能会有一些地方跟传统的http接口开发不太一样。读者尝试在做webSocket项目的时候,转换一下思维。
最后提下一个注意点:微信小程序的websocket链接是全局只能有一个,官方提示:“ 一个微信小程序同时只能有一个 WebSocket 连接,如果当前已存在一个 WebSocket 连接,会自动关闭该连接,并重新创建一个 WebSocket 连接。 ”
以上是微信小程式開發之websocket實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!