這篇文章真的是寫的太好了,花了半個小時的時間去閱讀,想分享給大家。
正文如下:
網際網路發展到現在,早已超越了原始的初衷,人類從來沒有像現在這樣依賴過他;也正是這種依賴,促進了網際網路技術的飛速發展。而終端裝置的創新與發展,更加速了網際網路的進化;
HTTP/1.1規範釋出于1999年,同年12月24日,HTML4.01規範釋出;盡管已到2012年,但HTML4.01仍是主流;雖然HTML5的草案已出現了好幾個年頭,但轉正日期,遙遙無期,少則三五年,多則數十年;而HTML5的客戶代理(對于一般使用者而言,就是浏覽器),則已百家争鳴,星星向榮;再加上移動終端的飛速發展,在大多數情況下,我們都可以保證擁有一個HTML5的運作環境,是以,我們來分享一下HTML5中的WebSocket協定;
本文包含以下六個方面:
1.WebSocket的前世今生
2.WebSocket是什麼
3.為什麼使用WebSocket
4.搭建WebSocket伺服器
5.WebSocket API
6.執行個體解析
以上六點分為兩大塊,前3點側重理論,主要讓大家明白WebSocket是什麼,而後3點則結合代碼實戰,加深對WebSocket的認知。
Web 應用的資訊互動過程通常是用戶端通過浏覽器發出一個請求,伺服器端接收和稽核完請求後進行處理并傳回結果給用戶端,然後用戶端浏覽器将資訊呈現出來,這種機制對于資訊變化不是特别頻繁的應用尚能相安無事,但是對于那些實時要求比較高的應用來說就顯得捉襟見肘了。我們需要一種高效節能的雙向通信機制來保證資料的實時傳輸。有web TCP之稱的WebSocket應運而生,給開發人員提供了一把強有力的武器來解決疑難雜症。
(PS:其實,在早期的HTML5規範中,并沒有包含WebSocket的定義,一些早期的HTML5書籍中,完全沒有WebSocket的介紹。直到後來,才加入到目前的草案中。)
其實,從背景介紹中,我們大緻的可以猜出,WebSocket是幹什麼用的。前面我們提到,WebSocket有web TCP之稱,既然是TCP,肯定是用來做通信的,但是它又有不同的地方,WebSocket作為HTML5中新增的一種通信協定,由通信協定和程式設計API組成,它能夠在浏覽器和伺服器之間建立雙向連接配接,以基于事件的方式,賦予浏覽器原生的實時通信能力,來擴充我們的web應用,增加使用者體驗,提升應用的性能。何謂雙向?伺服器端和用戶端可以同時發送并響應請求,而不再像HTTP的請求和響應。
在WebSocket出現之前,我們有一些其它的實時通訊方案,比較常用的有輪詢,長輪詢,流,還有基于Flash的交換資料的方式,接下來,我們一一分析一下,各種通信方式的特點。
① 輪詢
這是最早的一種實作實時web應用的方案;原理比較簡單易懂,就是用戶端以一定的時間間隔向伺服器發送請求,以頻繁請求的方式來保持用戶端和伺服器端的資料同步。但是問題也很明顯:當用戶端以固定頻率向伺服器端發送請求時,伺服器端的資料可能并沒有更新,這樣會帶來很多無謂的請求,浪費帶寬,效率低下。
② 長輪詢
長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網絡傳輸。當伺服器端沒有資料更新的時候,連接配接會保持一段時間周期直到資料或狀态改變或者時間過期,通過這種機制來減少無效的用戶端和伺服器間的互動。當然,如果服務端的資料變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的性能的提高。
③ 流
④ 基于Flash的實時通訊方式
Flash有自己的socket實作,這為實時通信提供了可能。我們可以利用Flash完成資料交換,再利用Flash暴露出相應的接口,友善JavaScript調用,來達到實時傳輸資料的目的。這種方式比前面三種方式都要高效,而且應用場景比較廣泛;因為flash本身的安裝率很高;但是在目前的網際網路環境下,移動終端對flash的支援并不好,以IOS為主的系統中根本沒有flash的存在,而在android陣營中,雖然有flash的支援,但實際的使用效果差強人意,即使是配置較高的移動裝置,也很難讓人滿意。就在前幾天(2012年6月底),Adobe官方宣布,不在支援android4.1以後的系統,這基本上宣告了flash在移動終端上的死亡。
下面是輪詢和長輪詢的資訊流轉圖:

對比完四種不同的實時通信方式,不難發現,除了基于flash的方案外,其它三種方式都是用AJAX方式來模拟實時的效果,每次用戶端和伺服器端互動時,都是一次完整的HTTP請求和應答的過程,而每一次的HTTP請求和應答都帶有完整的HTTP頭資訊,這就增加每次的資料傳輸量,而且這些方案中用戶端和服務端的程式設計實作比較複雜。
接下來,我們再來看一下WebSocket,為什麼要使用它呢?高效節能,簡單易用。
下圖是來自websocket.org的測試結果:
在流量和負載增大的情況下,WebSocket 方案相比傳統的 Ajax 輪詢方案有極大的性能優勢;而在開發方面,也十分簡單,我們隻需要執行個體化WebSocket,建立連接配接,檢視是否連接配接成功,然後就可以發送和相應消息了。我們會在後面的執行個體中去詳細的說明API。
其實,在伺服器的選擇上很廣,基本上,主流語言都有WebSocket的伺服器端實作,而我們作為前端開發工程師,當然要選擇現在比較火熱的NodeJS作為我們的伺服器端環境了。
NodeJS本身并沒有原生的WebSocket支援,但是有第三方的實作(大家要是有興趣的話,完全可以參考WebSocket協定來做自己的實作),我們選擇了“ws”作為我們的伺服器端實作。
由于本文的重點是講解WebSocket,是以,對于NodeJS不做過多的介紹,不太熟悉的朋友可以去參考NodeJS入門指南(http://www.nodebeginner.org/index-zh-cn.html)。
安裝好NodeJS之後,我們需要安裝“ws”,也就是我們的WebSocket實作,安裝方法很簡單,在終端或者指令行中輸入:
<code>1</code>
<code>npm install ws</code>
,等待安裝完成就可以了。
接下來,我們需要啟動我們的WebSocket服務。首先,我們需要建構自己的HTTP伺服器,在NodeJS中建構一個簡單的HTTP伺服器很簡單,so easy。代碼如下:
<code>var app = http.createServer( onRequest ).listen( 8888 );</code>
onRequest()作為回調函數,它的作用是處理請求,然後做出響應,實際上就是根據接收的URL,在伺服器上查找相應的資源,最終傳回給浏覽器。
在建構了HTTP伺服器後,我們需要啟動WebSocket服務,代碼如下:
<code>var WebSocketServer = require('ws').Server;</code>
<code>2</code>
<code>var wss = new WebSocketServer( { server : app } );</code>
從代碼中可以看出,在初始化WebSocket服務時,把我們剛才建構好的HTTP執行個體傳遞進去就好。到這裡,我們的服務端代碼差不多也就編寫完成了。怎麼樣?很簡單吧。
上面我們介紹了WebSocket服務端的知識,接下來,我們需要編寫用戶端代碼了。在前面我們說過,用戶端的API也是一如既往的簡單:
見上圖:ready state中定義的是socket的狀态,分為connection、open、closing和closed四種狀态,從字面上就可以區分出它們所代表的狀态。
上圖描述的是WebSocket的事件,分為onopen、onerror和onclose;
上圖為消息的定義,主要是接收和發送消息。注意:可以發送二進制的資料。
以上個圖的具體的含義就不再一一贅述,較長的描述請參考:
http://www.w3.org/TR/2012/WD-websockets-20120524/
PS:由于WebSocket API(截止到2012年7月)還是草案,API文檔和上文所描述的會有所不同,請以官方文檔為主,這也是我為什麼不較長的描述API中各個屬性的原因。
另外一點需要提醒大家的是:在前端開發中,浏覽器相容是必不可少的,而WebSocket在主浏覽器中的相容還是不錯的,火狐和Chrome不用說,最新版的支援非常不錯,而且支援二進制資料的發送和接收。但是IE9并不支援,對于國内的大多數應用場景,WebSocket無法大規模使用。
截圖來自(http://tongji.baidu.com/data/browser),之是以選擇百度的統計資料,是因為更加符合國内的實際情況。圖中所展示的是2012年4月1日到2012年6月30日之間的統計資料,從圖中不難看出IE6.0、奇虎360、IE7.0和IE8.0加起來一共占據了77%的市場,FireFox屬于其他,chrome隻有5.72%的份額,再一次告訴我們,我們的主戰場依然是IE系。
既然是IE系,那麼對于WebSocket在實際app中的應用就基本不可能了。但我們完全可以在chrome、FireFox、以及移動版的IOS浏覽器中使用它。
搭建好了服務端,熟悉了API,接下來,我們要開始建構我們的應用了。鑒于WebSocket自身的特點,我們的第一個demo選擇了比較常見的聊天程式,我們暫且取名為chat。
說到聊天,大家最先想到的肯定是QQ,沒錯,我們所實作的應用和QQ類似,而且還是基于web的。因為是demo,我們的功能比較簡陋,僅實作了最簡單的會話功能。就是啟動WebSocket伺服器後,用戶端發起連接配接,連接配接成功後,任意用戶端發送消息,都會被伺服器廣播給所有已連接配接的用戶端,包括自己。
既然需要用戶端,我們需要建構一個簡單的html頁面,頁面中樣式和元素,大家可以自由發揮,隻要能夠輸入消息,有發送按鈕,最後有一個展示消息的區域即可。具體的樣子大家可以看附件中的demo。
寫玩HTML頁面之後,我們需要添加用戶端腳本,也就是和WebSocket相關的代碼;前面我們說過,WebSocket的API本身很簡單,是以,我們的用戶端代碼也很直接,如下:
<code>var websocket = new WebSocket(wsServer);</code>
<code>3</code>
<code>websocket.binaryType = "arraybuffer";</code>
<code>4</code>
<code>websocket.onopen = onOpen;</code>
<code>5</code>
<code>websocket.onclose = onClose;</code>
<code>6</code>
<code>websocket.onmessage = onMessage;</code>
<code>7</code>
<code>websocket.onerror = onError;</code>
首先,我們需要指定WebSocket的服務位址,也就是var wsServer = ‘ws://localhost:8888/’;
然後,我們執行個體化WebSocket,new WebSocket(wsServer),
剩下的就是指定相應的回調函數了,分别是onOpen,onClose,onMessage和onError,對于咱們的實驗應用來說,onopen、onclose、onerror甚至可以不管,咱們重點關注一下onmessage。
onmessage()這個回調函數會在用戶端收到消息時觸發,也就是說,隻要伺服器端發送了消息,我們就可以通過onmessage拿到發送的資料,既然拿到了資料,接下去該怎麼玩,就随便我們了。請看下面的僞代碼:
<code>function onMessage(evt) {</code>
<code></code><code>var json = JSON.parse(evt.data);</code>
<code></code><code>commands[json.event](json.data);</code>
<code>}</code>
因為onmessage隻接收字元串和二進制類型的資料,如果需要發送json格式的資料,就需要我們轉換一下格式,把字元串轉換成JSON格式。隻要是支援WebSocket,肯定原生支援window.JSON,是以,我們可以直接使用JSON.parse()和JSON.stringify()來進行轉換。
轉換完成後,我們就得到了我們想要的資料了,接下來所做的工作就是将消息顯示出來。實際上就是
<code>Elements.innerHTML += data + '</br>';</code>
上面展現了用戶端的代碼,伺服器端的代碼相對要簡單一些,因為我們的伺服器端使用的是第三方實作,我們隻需要做一些初始化工作,然後在接收到消息時,将消息廣播出去即可,下面是具體的代碼:
<code>01</code>
<code>02</code>
<code>var WebSocketServer = require('ws').Server,</code>
<code>03</code>
<code></code><code>wss = new WebSocketServer( { server : app } );</code>
<code>04</code>
<code>wss.on('connection', function( ws ) {</code>
<code>05</code>
<code></code><code>console.log('connection successful!');</code>
<code>06</code>
<code></code><code>ws.on('message', function( data, flags ) {</code>
<code>07</code>
<code></code><code>console.log(data);</code>
<code>08</code>
<code></code><code>//do something here</code>
<code>09</code>
<code></code><code>});</code>
<code>10</code>
<code></code><code>ws.on('close', function() {</code>
<code>11</code>
<code></code><code>console.log('stopping client');</code>
<code>12</code>
<code>13</code>
<code>});</code>
我們可以通過wss.clients獲得目前已連接配接的所有用戶端,然後周遊,得到執行個體,調用send()方法發送資料;
<code>var clients = wss.clients, len = clients.length, i = 0;</code>
<code></code><code>for( ; i < len; i = i + 1 ){</code>
<code></code><code>clients[i].send( msg );</code>
<code></code><code>}</code>
說到這裡,一個雙向通信的執行個體基本完成,當然,上面都是僞代碼,完整的demo請檢視附件。
除了常見的聊天程式以外,大家完全可以發揮創意,建構一些“好玩”的應用;
接下來,分享另外一個應用,“你畫我猜”這個應用,很多人都接觸過,大緻上是:某個人在螢幕上畫一些圖形,這些圖檔會實時展示在其它人的螢幕上,然後來猜畫的是什麼。
利用WebSocket和canvas,我們可以很輕松的建構類似的應用。當然,我們這裡隻是demo,并沒有達到産品級的高度,這裡隻是為大家提供思路;
首先,我們再次明确一下,WebSocket賦予了我們在浏覽器端和伺服器進行雙向通信的能力,這樣,我們可以實時的将資料發送給伺服器,然後再廣播給所有的用戶端。這和聊天程式的思路是一緻的。
接下來,伺服器端的代碼不用做任何修改,在html頁面中準備一個canvas,作為我們的畫布。如何在canvas上用滑鼠畫圖形呢?我們需要監聽mousedown、mousemove和mouseup三個滑鼠事件。說到這裡,大家應該知道怎麼做了。沒錯,就是在按下滑鼠的時候,記錄目前的坐标,移動滑鼠的時候,把坐标發送給伺服器,再由伺服器把坐标資料廣播給所有的用戶端,這樣就可以在所有的用戶端上同步繪畫了;最後,mouseup的時候,做一些清理工作就ok了。下面是一些僞代碼:
<code>var WhiteBoard = function( socket, canvasId ){</code>
<code></code><code>var lastPoint = null,</code>
<code></code><code>mouseDown = false,</code>
<code></code><code>canvas = getById(canvasId),</code>
<code></code><code>ctx = canvas.getContext('2d');</code>
<code></code><code>var handleMouseDown = function(event) {</code>
<code></code><code>mouseDown = true;</code>
<code></code><code>lastPoint = resolveMousePosition.bind( canvas, event )();</code>
<code></code><code>};</code>
<code></code><code>var handleMouseUp = function(event) {</code>
<code></code><code>mouseDown = false;</code>
<code>14</code>
<code></code><code>lastPoint = null;</code>
<code>15</code>
<code>16</code>
<code>17</code>
<code></code><code>var handleMouseMove = function(event) {</code>
<code>18</code>
<code></code><code>if (!mouseDown) { return; }</code>
<code>19</code>
<code></code><code>var currentPoint = resolveMousePosition.bind( canvas, event )();</code>
<code>20</code>
<code></code><code>socket.send(JSON.stringify({</code>
<code>21</code>
<code></code><code>event: 'draw',</code>
<code>22</code>
<code></code><code>data: {</code>
<code>23</code>
<code></code><code>points: [</code>
<code>24</code>
<code></code><code>lastPoint.x,</code>
<code>25</code>
<code></code><code>lastPoint.y,</code>
<code>26</code>
<code></code><code>currentPoint.x,</code>
<code>27</code>
<code></code><code>currentPoint.y</code>
<code>28</code>
<code></code><code>]</code>
<code>29</code>
<code>30</code>
<code></code><code>}));</code>
<code>31</code>
<code>32</code>
<code></code><code>lastPoint = currentPoint;</code>
<code>33</code>
<code></code><code>}; </code>
<code>34</code>
<code>35</code>
<code></code><code>var init = function(){</code>
<code>36</code>
<code></code><code>addEvent( canvas, 'mousedown', handleMouseDown );</code>
<code>37</code>
<code></code><code>addEvent( canvas, 'mouseup', handleMouseUp );</code>
<code>38</code>
<code></code><code>addEvent( canvas, 'mousemove', handleMouseMove );</code>
<code>39</code>
<code>40</code>
<code></code><code>var img = new Image();</code>
<code>41</code>
<code></code><code>addEvent( img, 'load', function(e){</code>
<code>42</code>
<code></code><code>canvas.width = img.width;</code>
<code>43</code>
<code></code><code>canvas.height = img.height;</code>
<code>44</code>
<code></code><code>ctx.drawImage( img, 0, 0 );</code>
<code>45</code>
<code></code><code>} );</code>
<code>46</code>
<code></code><code>img.src = '/img/diablo3.png';</code>
<code>47</code>
<code>48</code>
<code>49</code>
<code></code><code>var drawLine = function(data) {</code>
<code>50</code>
<code></code><code>var points = data.points;</code>
<code>51</code>
<code></code><code>ctx.strokeStyle = 'rgb(255, 15, 255)';</code>
<code>52</code>
<code></code><code>ctx.beginPath();</code>
<code>53</code>
<code></code><code>ctx.moveTo( points[0] + 0.5, points[1] + 0.5 );</code>
<code>54</code>
<code></code><code>ctx.lineTo( points[2] + 0.5, points[3] + 0.5 );</code>
<code>55</code>
<code></code><code>ctx.stroke();</code>
<code>56</code>
<code>57</code>
<code>58</code>
<code></code><code>function resolveMousePosition(event) {</code>
<code>59</code>
<code></code><code>var x, y;</code>
<code>60</code>
<code></code><code>if (event.offsetX) {</code>
<code>61</code>
<code></code><code>x = event.offsetX;</code>
<code>62</code>
<code></code><code>y = event.offsetY;</code>
<code>63</code>
<code></code><code>} else { //(注意)實際開發中,這樣擷取滑鼠相對canvas的坐标是不對的</code>
<code>64</code>
<code></code><code>x = event.layerX - this.offsetLeft;</code>
<code>65</code>
<code></code><code>y = event.layerY - this.offsetTop;</code>
<code>66</code>
<code>67</code>
<code></code><code>return { x: x, y: y };</code>
<code>68</code>
<code>69</code>
<code>70</code>
<code></code><code>init();</code>
<code>71</code>
<code>72</code>
<code></code><code>return {</code>
<code>73</code>
<code></code><code>draw : drawLine</code>
<code>74</code>
<code></code><code>//ctx : ctx,</code>
<code>75</code>
<code></code><code>//canvas : canvas</code>
<code>76</code>
<code>77</code>
<code></code><code>}( websocket, 'drawsomething' );</code>
對于canvas不熟悉的同學,請自己去搜尋一下,有許多不錯的教程。其它方面,和聊天應用的思路基本一樣。
最後,我們需要明确一點,WebSocket本身的優點很明顯,但是作為一個正在演變中的web規範,我們必須清楚的認識到WebSocket在建構應用時的一些風險;雖然本身有很多局限性,但是這項技術本身肯定是大勢所趨,WebSocket在移動終端,在chrome web store都有用武之地,我們可以進行大膽的嘗試,讓我們在技術的革新中不被淘汰。
Resources:
http://www.w3.org/TR/websockets/
W3 API的官方文檔,有詳細的接口設計文檔和實作步驟
http://tools.ietf.org/html/rfc6455
WebSocket協定
http://tools.ietf.org/html/rfc6202
Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP
http://msdn.microsoft.com/en-us/library/ie/hh673567(v=vs.85).aspx
msdn中關于WebSocket的介紹
https://developer.mozilla.org/en/WebSockets
http://caniuse.com/#feat=websockets
Compatibility tables for support of HTML5, CSS3, SVG and more in desktop and mobile browsers.
本文轉自 我不會抽煙 51CTO部落格,原文連結:http://blog.51cto.com/zhouhongyu1989/1289822,如需轉載請自行聯系原作者