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