In 網(wǎng)頁重構(gòu) on 2014-12-09 14:54:36 by 鬼爪手
原本是想在寫這文章之前,給大家來個二維碼,讓大家來感受一下我那個狂拽酷炫叼炸天的實時互動小游戲,無奈一直沒有找到一臺足以hold住其氣場的服務(wù)器。所以,此處可能需要大家跟隨我的描述,腦補一下那高端大氣上檔次的畫面及低調(diào)奢華有內(nèi)涵交互設(shè)計:
1、登錄界面
(此處省略4.33W字)
2、房間列表頁
(此處省略3.75W字)
3、游戲界面
(此處省略5.83W字)
真不是我故意這樣的,實在是人類的語言已無法將其形容,過份的修飾描述只怕是有損其光輝閃耀的形象。此時的我,更是懷著對其滿滿的敬意,忐忑第敲打著鍵盤,為大家介紹其狂拽酷炫叼炸天是怎樣形成的。
從文章的標題上,我們不難看出,這個游戲是基于websocket。那么我就先從websocket的作用以及其優(yōu)點這兩個方面,給大家簡單介紹一下websocket:
Websocket的作用
其實websocket的作用,個人感覺可以簡單地用一句話來概括:
構(gòu)建實時的Web應(yīng)用
比如:
1、聊天室/在線客服
2、在線游戲
3、股票走勢
4、多屏互動
5、...
在日常的使用web的過程中,這種功能非常常見,比如:新浪微博的WebIM、WebQQ、大智慧網(wǎng)頁版等等,我們在處理日常的一些專題中,適當?shù)丶尤胍恍┒嗥粱?,也能很好地增加用戶的參與度,增強一些現(xiàn)場的互動,如:斗戰(zhàn)誅天營救悟空、神秘站等
在websocket出來之前,如果我們想實現(xiàn)上述類型的功能,我們通常采用的是以下幾種方式:
1、輪詢
2、長輪詢
3、長連接
4、Flash
我就先通過比較以上幾種方式的優(yōu)缺點,讓大家更為清楚地了解websocket牛B之處
輪詢:
定時向服務(wù)器發(fā)送請求,服務(wù)器響應(yīng)請求并返回數(shù)據(jù)
優(yōu)點:后端服務(wù)器不需要特殊設(shè)置
缺點:易產(chǎn)生大量無效請求,浪費服務(wù)器資源,且消息有延遲
這種輪詢的方式,在日常的網(wǎng)頁應(yīng)用中其實應(yīng)用也比較多,但是只適合一些實時性要求并不是很高的那種應(yīng)用,比如說微博的新消息提醒,每隔一段向服務(wù)器請求,看看是否有新的微博/粉絲/@等
長輪詢
客戶端向服務(wù)器發(fā)送Ajax請求,服務(wù)器保持該請求不中斷,一直等有新的數(shù)據(jù)(或超時)需要處理才返回響應(yīng)信息并關(guān)閉連接,客戶端處理完成后,重新發(fā)起ajax請求
PS:兩張圖找不同的游戲已開始,請注意看右側(cè)服務(wù)器端部分的差異
優(yōu)點:相比輪詢,減少了無效請求次數(shù),消息的實時性得到提升
缺點:保持連接同樣造成服務(wù)器資源浪費
就目前而言,大多數(shù)兼容低版本瀏覽器的聊天室(聊天室貌似基本玩完了)、在線客服,采用的都還是這種方式
長連接
請求一直不中斷,服務(wù)器端可不斷地向客戶端輸出數(shù)據(jù)
優(yōu)點:消息實時,不會產(chǎn)生無效請求
缺點:對服務(wù)器開銷較大,單向接收數(shù)據(jù)還成,客戶端如果想要提交數(shù)據(jù),一樣需要斷開連接后重新發(fā)送請求
Flash
基于socket,服務(wù)器可客戶端可隨時進行雙向通信
優(yōu)點:socket協(xié)議
缺點:需要安裝flash player,對移動端(特別是IOS,貌似高級的安卓也在放棄flash)不友好
通過對上面四種傳統(tǒng)方式的分析,我們不難發(fā)現(xiàn),其實前面的三種方式都是傳統(tǒng)意義上的HTTP請求(PS:那些個亂七八糟的握手什么,就不在這里探討了),然后你就會發(fā)現(xiàn),每次的請求都會有一堆類似下面的這些個步驟
當然了,牛B的你可能會說DNS有緩存,并不會需要那么多DNS Lookup,嗯,那么其他的呢?看看那些頭信息(cookie已打碼)
這些信息可是會伴隨每次請求,來回地穿梭在服務(wù)器和客戶端之間。浪費你大量的服務(wù)器資源,當然了,弊端包括但不限于此。那么,是時候看看Websocket的優(yōu)勢
Websocket的優(yōu)點
說到這個優(yōu)點,我只想讓大家看一個websocket官網(wǎng)上的一個圖表
通過這么一個圖表,我們會發(fā)現(xiàn),請求量越大的情況下,Websocket的表現(xiàn)就越是勇猛。與此同時,這么一個勇猛的外表下,臟著的確是一顆少女般的心。別誤會,值的是學(xué)習起她來很簡單。
下面就從websocket服務(wù)器及其api兩個方面來簡單介紹一下:
Webscoket服務(wù)器的搭建
本次所講述的websocket是基于nodejs服務(wù)器來完成整套部署的。所以,我們需要先在服務(wù)器上搭建一個nodejs環(huán)境
Nodejs安裝
直接從http://nodejs.org 這個網(wǎng)站上下載后直接安裝就成,應(yīng)該是沒什么難度的
安裝完成之后,我們可以在命令行工具中運行 node -v來檢測安裝是否成功
如果正常地顯示出了版本號,那么說明nodejs安裝成功,接下來我們就需要安裝websocket模塊了
Websocket模塊安裝
Nodejs安裝完成之后,其默認就給安裝好了nodejs包管理工具npm,通過使用npm命令,我們就可以來安裝/卸載/更新nodejs的包。
一切正常的話,我們就可以通過使用命令
npm install ws
來安裝websocket模塊
websocket的服務(wù)器環(huán)境基本搭建完成,接下來我們通過幾行簡單地代碼就可以把一個websocket服務(wù)器啟動起來
var cons = new Array();
var ws = require('ws').Server;
var server = new ws({host:"127.0.0.1",port:8808});
server.on('connection',function(ws){
console.log('new connection founded successfully');
cons.push(ws);
ws.on('message',function(data){
for(var i=0;i<cons.length;i++){
cons[i].send(data);
}
});
ws.on('close',function(){
for(var i=0;i<cons.length;i++){
if(cons[i] == ws) cons.splice(i,1);
}
});
});
console.log('websocket-server running...');
保存文件名為app.js,在命令行中運行
node app.js
到此為止,服務(wù)端的部署完成,接下來,就可以看看websocket是如何在瀏覽器上跑起來的。
在客戶端,僅需要一條語句,就算是建立起了客戶端和服務(wù)器端的鏈接
var ws = new WebSocket('ws://127.0.0.1:8808/');
PS:所傳遞參數(shù)中的地址需要服務(wù)器上配置的一致
然后就可以通過各種事件/方法來完成客戶端和服務(wù)器之間的數(shù)據(jù)交互,這個也就是我接下來要介紹的
Websocket API簡介
當然,我這里的介紹包括了事件及方法
常用的事件和方法,總共為一下6個
onopen 和服務(wù)器連接成功
onmessage 接收服務(wù)器的消息
onclose 斷開和服務(wù)器的鏈接
onerror 錯誤處理
send 向服務(wù)器發(fā)送消息
close 斷開和服務(wù)器的鏈接
用法大致如下
//建立服務(wù)器連接
ws.onopen = function(){
systemInfo.innerHTML = '<p>和websocket服務(wù)器連接成功</p>';
}
//接收到服務(wù)器返回的數(shù)據(jù)
ws.onmessage = function(e){
systemInfo.innerHTML += '<p>'+e.data+'</p>';
}
//斷開服務(wù)器連接
ws.onclose = function(){
systemInfo.innerHTML += '<p>WebSocket服務(wù)器連接關(guān)閉</p>';
}
//ws發(fā)生錯誤
ws.onerror = function(e){
console.log(e);
systemInfo.innerHTML += '<p>WebSocket發(fā)生錯誤</p>';
}
testForm.onsubmit = function(){
//發(fā)送數(shù)據(jù)給服務(wù)器
ws.send(username.value+":"+msg.value);
return false;
}
close.addEventListener('click', function(){
ws.close();
}, false);
該完整demo可以點擊此處下載
由此可見,websocket用起來真的很簡單。但是這個功能相對來說非常單一,在實際的項目過程中,我們所涉及到的業(yè)務(wù)邏輯可能會相對來說復(fù)雜很多,比如說某些消息只想被某個特定的范圍里面的用戶接收,同時,至少在天朝,使用低版本IE瀏覽器或者其相同內(nèi)核(Trident)的用戶所占比例還是不少,沒理由把這批用戶放棄,為了解決這個問題,socket.io組件便孕育而生了
Socket.io
Socket.io作為nodejs的一個模塊,其安裝方法和ws的完全一致
npm install socket.io
Socket.io同樣的簡單
在服務(wù)端只需要起一個HTTP server,然后在啟動socket.io即可
var app = require('http').createServer(handler)
var io = require('socket.io')(app);
Handler函數(shù)自己YY一下吧,
客戶端的話,比使用原生的websocket稍微多一步,需要在頁面上引入一個socket.io.js文件
<script src="/socket.io/socket.io.js"></script>
然后在通過運行
var socket = io();
即建立起了socket連接
有的童鞋可能會奇怪,在自己的代碼目錄中并沒有socket.io.js這個文件,設(shè)置在網(wǎng)站根目錄下socket.io這個目錄都沒有,其原因是這個請求被rewrite了,所以~~僅僅使用的話..你可以不用在意這個細節(jié),如果你只是想去看看這個文件的代碼,可以直接去訪問那個路徑即可。
對于socket.io,我們只需要掌握兩個功能函數(shù),即可以完成基本的websocket功能了。
這兩個函數(shù)分別為
on 事件監(jiān)聽
emit 觸發(fā)事件
常用的事件
connect 建立連接
disconnect 斷開連接
error 出錯
實時聯(lián)機小游戲
好吧,前面介紹了一堆,現(xiàn)在馬上回到那個狂拽酷炫叼炸天的游戲上來,介紹這個游戲的實現(xiàn),我會從4個方面來進行。
記得每個頁面都需要引入socket.io.js文件
<script src="/socket.io/socket.io.js"></script>
1、用戶注冊/登錄
2、創(chuàng)建房間
3、加入房間
4、對戰(zhàn)(實時排行榜)
用戶注冊/登錄
本游戲是沒有進行嚴格意義上的用戶授權(quán)驗證,各位就莫要糾結(jié)這些。
注冊/登錄顧名思義,頁面上肯定就是一個表單,讓用戶填寫一些用戶名之類的。當然了,我絕對不會因為這種頁面簡單,就隨便設(shè)計下敷衍了事。一個偉大的產(chǎn)品,在這些細節(jié)把握上,做得那都是非常到位。(作者此處忍住了,未用人類的語言損害其光輝閃耀的形象)
用傳統(tǒng)的方式去完成這種注冊/登錄的話,就兩部:
1、客戶端填好信息后,post相關(guān)信息到某個接口文件,在服務(wù)器上完成了相應(yīng)的操作之后,反饋給客戶端一些信息。
2、客戶端接收到服務(wù)器返回的信息后,給出相應(yīng)的操作或者是相關(guān)的錯誤提示信息
用socket的方式,步驟和這個基本一致,只不過是這個減少了一些請求的發(fā)送,其步驟也同樣是兩部
1、客戶端填好信息后,通過指定事件將這些數(shù)據(jù)發(fā)送到服務(wù)器端,服務(wù)端通過監(jiān)聽這個指定的事件,去完成相應(yīng)的操作。完成之后,同樣通過一個指定的事件,將消息發(fā)送回客戶端。
2、客戶端監(jiān)聽到服務(wù)器所觸發(fā)的那個事件后,給出相應(yīng)的操作或者是錯誤提示信息
在我們的這個案例中,關(guān)鍵代碼如下
客戶端向服務(wù)器發(fā)送注冊事件(寫在客戶端)
socket.emit('registe', userName); //事件名可自定義
服務(wù)器監(jiān)聽registe事件(寫在服務(wù)端)
socket.on('registe', function(userName){
//完成一些重名判斷,寫入數(shù)據(jù)之類的
//上述步驟完成之后,需要向客戶端發(fā)送事件,事件名可自定義
socket.emit('registe', {
userInfo : userInfo,
msg : 'registe successed',
code : 0
})
});
客戶端監(jiān)聽服務(wù)器上發(fā)送的那個事件
socket.on('registe', function (data) {
//根據(jù)服務(wù)器給回的數(shù)據(jù)進行相應(yīng)的操作
});
創(chuàng)建房間
創(chuàng)建房間的流程和注冊的流程一致,重新定義個事件名基本上就OK了。但是真當你按照上面的那些流程去操作的時候,你會發(fā)現(xiàn)當你停留在房間列表頁的時候,你只能看到你自己剛創(chuàng)建的房間被動態(tài)插入到列表中。在你停留在房間列表的時候,其他用戶創(chuàng)建的房間,你看不見。同樣的,你的房間也不會實時刷新到其他用戶的房間列表中。除非你手動刷新你頁面。
如果,因為這點,你覺得socket也就不過如此的話,那么你就是真的是小瞧socket.io了,socket.io發(fā)送消息,默認情況下是只發(fā)送給當前連接的socket,但是它也是可以把消息發(fā)送給所有人的。我們只需要修改一點代碼即可達成實時更新所有用戶房間列表的功能
下面的這幾行代碼是服務(wù)端創(chuàng)建房間的關(guān)鍵代碼
socket.on('create', function (data) {
//完成一些重名判斷,寫入數(shù)據(jù)之類的
//關(guān)鍵代碼在此,注意和上面注冊的代碼相比較
io.sockets.emit('create', {
roomInfo : roomInfo,
msg : 'create successed',
code : 0
})
});
上面的注冊/登錄我們在服務(wù)器向客戶端發(fā)送消息時,用到的是
socket.emit
在創(chuàng)建房間列表時,用到的是
io.sockets.emit
通過使用下面的這種方式,我們就可以實現(xiàn)想所有連接的socket發(fā)送消息的功能
加入房間
通過上面兩個功能點的講解,也許你馬上就想到了加入房間功能應(yīng)該如何實現(xiàn)了,客戶端發(fā)送一個加入房間的事件到服務(wù)器端,服務(wù)器給當前的這個用戶一個標識,標識當前這個用戶所進入的房間,然后通知到客戶端就好了。確實,你這樣實現(xiàn)也確實可以實現(xiàn)基本的加入房間功能,但是你別因此就關(guān)閉了我這篇文章,搞不好這里還能給你提供一個更優(yōu)雅的實現(xiàn)方式呢?。ǖ谝淮慰吹缴厦婢完P(guān)了,第二次才看到這里的朋友,你也是幸運的)
沒錯,這里就是要給大家提供一個更優(yōu)雅的方式,如果你按照上面的那個思路往下進行,你會發(fā)現(xiàn)代碼寫起來似乎越來越費勁。這里需要給大家介紹的就是另外一個API:
socket.join
從字面上我們似乎就發(fā)現(xiàn)了,這個API簡直就是為加入房間而生了。沒錯,用他來實現(xiàn)加入房間,很完美。但是個人還是建議你把他理解成為加入某個分組。相信這樣,我才不會固化了大家伙的思維。
如果是用這種方法,那么加入房間就會變得異常輕松
socket.on('enter', function(data){
//加入房間
socket.join(data.room);
//加入成功之后通知客戶端
socket.emit('enter', userInfo[data.user]);
})
到此為止,似乎采用這種join的方式,優(yōu)勢也并不是那么特別的明顯。那么,在接下來的對戰(zhàn)頁面中。你就能發(fā)現(xiàn)其牛B之處
對戰(zhàn)(實時排行榜)
所謂實時排行榜,就肯定是服務(wù)器上有數(shù)據(jù)發(fā)生變化時,需要通知客戶端去更新。前面我給大家介紹過兩種發(fā)送數(shù)據(jù)的方式
socket.emit //向當前連接的socket
以及
io.sockets.emit //向所有連接的socket發(fā)送信息
但是,在實際的這種加入房間的游戲?qū)?zhàn)中,似乎這兩種發(fā)送消息的方式都不滿足。第一種范圍太小,光自己看到不頂用;第二種范圍又太大,很容易騷擾到其他房間的用戶。我們需要第三種:消息只能被指定房間中的用戶接收。很不巧的是,socket.io還真提供了這種API:
io.sockets.in(roomID).emit
roomID也就是我們上面socket.join方法中傳遞的參數(shù),那么此時,我們的代碼僅需要如此:
io.sockets.in(roomID).emit('update scroce', {
player : roomInfo[roonName].player,
userInfo : userInfo
})
同樣的,游戲倒計時也可以使用這種方法。
socket.io提供的消息發(fā)送方式,不僅僅為以上三種方式,其包含有如下幾種:
socket.emit() //發(fā)送消息給當前請求的socket
io.sockets.emit() //發(fā)送消息給所有連接socket
socket.broadcase.emit() //發(fā)送消息給當前請求之外的所有的socket
io.sockets.in(foo).emit() //向指定的分組發(fā)送消息
socket.broadcase.to(foo).emit() //向指定的分組發(fā)送消息,除當前請求的socket
io.sockets.socket(socketid).emit() //通過socketid向特定有效的socket發(fā)送消息
好了,到此為止,這個實時對戰(zhàn)小游戲的功能基本上介紹完畢了。當然了,并不是所有的人設(shè)計感都像我那么強,可以把這么一個小游戲真正做得和我的那個一樣高端大氣上檔次,低調(diào)奢華有內(nèi)涵是吧...。
什么?當時你參加了大講堂?體驗過我的那個小游戲?
大俠饒命,我保證以后不裝逼了行不?你得保證不砍死我。
胡扯到此結(jié)束,功能實現(xiàn)悉數(shù)奉上,一個好的產(chǎn)品確實是離不開一個好的創(chuàng)意和一個好的設(shè)計。期待你那真正高端大氣上檔次的產(chǎn)品出現(xiàn)。
當然了,websocket就目前而言,在真正使用的時候還是多少考慮下一些實際的問題,至少天朝帶寬什么的可能并不是特別的理想,網(wǎng)絡(luò)延遲之類的還是比較嚴重。不過,隨著4G的出現(xiàn)及今后互聯(lián)網(wǎng)的發(fā)展,興許這以后就真不是什么問題了呢。
聯(lián)系客服