初見websocket技術

餘二五發表於2017-11-16

這篇文章真的是寫的太好了,花了半個小時的時間去閱讀,想分享給大家。

正文如下:

網際網路發展到現在,早已超越了原始的初衷,人類從來沒有像現在這樣依賴過他;也正是這種依賴,促進了網際網路技術的飛速發展。而終端裝置的創新與發展,更加速了網際網路的進化;

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的認知。

一、WebSocket的前世今生

Web 應用的資訊互動過程通常是客戶端通過瀏覽器發出一個請求,伺服器端接收和稽核完請求後進行處理並返回結果給客戶端,然後客戶端瀏覽器將資訊呈現出來,這種機制對於資訊變化不是特別頻繁的應用尚能相安無事,但是對於那些實時要求比較高的應用來說就顯得捉襟見肘了。我們需要一種高效節能的雙向通訊機制來保證資料的實時傳輸。有web TCP之稱的WebSocket應運而生,給開發人員提供了一把強有力的武器來解決疑難雜症。

(PS:其實,在早期的HTML5規範中,並沒有包含WebSocket的定義,一些早期的HTML5書籍中,完全沒有WebSocket的介紹。直到後來,才加入到當前的草案中。)

二、WebSocket是什麼?

其實,從背景介紹中,我們大致的可以猜出,WebSocket是幹什麼用的。前面我們提到,WebSocket有web TCP之稱,既然是TCP,肯定是用來做通訊的,但是它又有不同的地方,WebSocket作為HTML5中新增的一種通訊協議,由通訊協議和程式設計API組成,它能夠在瀏覽器和伺服器之間建立雙向連線,以基於事件的方式,賦予瀏覽器原生的實時通訊能力,來擴充套件我們的web應用,增加使用者體驗,提升應用的效能。何謂雙向?伺服器端和客戶端可以同時傳送並響應請求,而不再像HTTP的請求和響應。

三、為什麼使用WebSocket

在WebSocket出現之前,我們有一些其它的實時通訊方案,比較常用的有輪詢,長輪詢,流,還有基於Flash的交換資料的方式,接下來,我們一一分析一下,各種通訊方式的特點。

① 輪詢

這是最早的一種實現實時web應用的方案;原理比較簡單易懂,就是客戶端以一定的時間間隔向伺服器傳送請求,以頻繁請求的方式來保持客戶端和伺服器端的資料同步。但是問題也很明顯:當客戶端以固定頻率向伺服器端傳送請求時,伺服器端的資料可能並沒有更新,這樣會帶來很多無謂的請求,浪費頻寬,效率低下。

② 長輪詢

長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網路傳輸。當伺服器端沒有資料更新的時候,連線會保持一段時間週期直到資料或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和伺服器間的互動。當然,如果服務端的資料變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的效能的提高。

③ 流

長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網路傳輸。當伺服器端沒有資料更新的時候,連線會保持一段時間週期直到資料或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和伺服器間的互動。當然,如果服務端的資料變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的效能的提高。

④ 基於Flash的實時通訊方式

Flash有自己的socket實現,這為實時通訊提供了可能。我們可以利用Flash完成資料交換,再利用Flash暴露出相應的介面,方便JavaScript呼叫,來達到實時傳輸資料的目的。這種方式比前面三種方式都要高效,而且應用場景比較廣泛;因為flash本身的安裝率很高;但是在當前的網際網路環境下,移動終端對flash的支援並不好,以IOS為主的系統中根本沒有flash的存在,而在android陣營中,雖然有flash的支援,但實際的使用效果差強人意,即使是配置較高的移動裝置,也很難讓人滿意。就在前幾天(2012年6月底),Adobe官方宣佈,不在支援android4.1以後的系統,這基本上宣告了flash在移動終端上的死亡。

下面是輪詢和長輪詢的資訊流轉圖:
2012_07_06_10_101.gif
2012_07_06_10_102.gif

對比完四種不同的實時通訊方式,不難發現,除了基於flash的方案外,其它三種方式都是用AJAX方式來模擬實時的效果,每次客戶端和伺服器端互動時,都是一次完整的HTTP請求和應答的過程,而每一次的HTTP請求和應答都帶有完整的HTTP頭資訊,這就增加每次的資料傳輸量,而且這些方案中客戶端和服務端的程式設計實現比較複雜。

接下來,我們再來看一下WebSocket,為什麼要使用它呢?高效節能,簡單易用。

下圖是來自websocket.org的測試結果:
2012_07_06_10_103.gif

在流量和負載增大的情況下,WebSocket 方案相比傳統的 Ajax 輪詢方案有極大的效能優勢;而在開發方面,也十分簡單,我們只需要例項化WebSocket,建立連線,檢視是否連線成功,然後就可以傳送和相應訊息了。我們會在後面的例項中去詳細的說明API。

四、搭建WebSocket伺服器

其實,在伺服器的選擇上很廣,基本上,主流語言都有WebSocket的伺服器端實現,而我們作為前端開發工程師,當然要選擇現在比較火熱的NodeJS作為我們的伺服器端環境了。

NodeJS本身並沒有原生的WebSocket支援,但是有第三方的實現(大家要是有興趣的話,完全可以參考WebSocket協議來做自己的實現),我們選擇了“ws”作為我們的伺服器端實現。

由於本文的重點是講解WebSocket,所以,對於NodeJS不做過多的介紹,不太熟悉的朋友可以去參考NodeJS入門指南(http://www.nodebeginner.org/index-zh-cn.html)。

安裝好NodeJS之後,我們需要安裝“ws”,也就是我們的WebSocket實現,安裝方法很簡單,在終端或者命令列中輸入:

1 npm install ws

,等待安裝完成就可以了。

接下來,我們需要啟動我們的WebSocket服務。首先,我們需要構建自己的HTTP伺服器,在NodeJS中構建一個簡單的HTTP伺服器很簡單,so easy。程式碼如下:

1 var app = http.createServer( onRequest ).listen( 8888 );


onRequest()作為回撥函式,它的作用是處理請求,然後做出響應,實際上就是根據接收的URL,在伺服器上查詢相應的資源,最終返回給瀏覽器。

在構建了HTTP伺服器後,我們需要啟動WebSocket服務,程式碼如下:

1 var WebSocketServer = require(`ws`).Server;
2 var wss = new WebSocketServer( { server : app } );


從程式碼中可以看出,在初始化WebSocket服務時,把我們剛才構建好的HTTP例項傳遞進去就好。到這裡,我們的服務端程式碼差不多也就編寫完成了。怎麼樣?很簡單吧。

五、WebSocket API

上面我們介紹了WebSocket服務端的知識,接下來,我們需要編寫客戶端程式碼了。在前面我們說過,客戶端的API也是一如既往的簡單:
2012_07_06_10_104.gif

見上圖:ready state中定義的是socket的狀態,分為connection、open、closing和closed四種狀態,從字面上就可以區分出它們所代表的狀態。

2012_07_06_10_105.gif

上圖描述的是WebSocket的事件,分為onopen、onerror和onclose;

2012_07_06_10_106.gif

上圖為訊息的定義,主要是接收和傳送訊息。注意:可以傳送二進位制的資料。

以上個圖的具體的含義就不再一一贅述,詳細描述請參考:

http://www.w3.org/TR/2012/WD-websockets-20120524/

PS:由於WebSocket API(截止到2012年7月)還是草案,API文件和上文所描述的會有所不同,請以官方文件為主,這也是我為什麼不詳細描述API中各個屬性的原因。

另外一點需要提醒大家的是:在前端開發中,瀏覽器相容是必不可少的,而WebSocket在主瀏覽器中的相容還是不錯的,火狐和Chrome不用說,最新版的支援非常不錯,而且支援二進位制資料的傳送和接收。但是IE9並不支援,對於國內的大多數應用場景,WebSocket無法大規模使用。

2012_07_06_10_107.gif截圖來自(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本身很簡單,所以,我們的客戶端程式碼也很直接,如下:

1 var wsServer = `ws://localhost:8888/`;
2 var websocket = new WebSocket(wsServer);
3 websocket.binaryType = "arraybuffer";
4 websocket.onopen = onOpen;
5 websocket.onclose = onClose;
6 websocket.onmessage = onMessage;
7 websocket.onerror = onError;


首先,我們需要指定WebSocket的服務地址,也就是var wsServer = ‘ws://localhost:8888/’;

然後,我們例項化WebSocket,new WebSocket(wsServer),

剩下的就是指定相應的回撥函式了,分別是onOpen,onClose,onMessage和onError,對於我們們的實驗應用來說,onopen、onclose、onerror甚至可以不管,我們們重點關注一下onmessage。

onmessage()這個回撥函式會在客戶端收到訊息時觸發,也就是說,只要伺服器端傳送了訊息,我們就可以通過onmessage拿到傳送的資料,既然拿到了資料,接下去該怎麼玩,就隨便我們了。請看下面的虛擬碼:

1 function onMessage(evt) {
2 var json = JSON.parse(evt.data);
3 commands[json.event](json.data);
4 }


因為onmessage只接收字串和二進位制型別的資料,如果需要傳送json格式的資料,就需要我們轉換一下格式,把字串轉換成JSON格式。只要是支援WebSocket,肯定原生支援window.JSON,所以,我們可以直接使用JSON.parse()和JSON.stringify()來進行轉換。

轉換完成後,我們就得到了我們想要的資料了,接下來所做的工作就是將訊息顯示出來。實際上就是

1 Elements.innerHTML += data + `</br>`;


上面展現了客戶端的程式碼,伺服器端的程式碼相對要簡單一些,因為我們的伺服器端使用的是第三方實現,我們只需要做一些初始化工作,然後在接收到訊息時,將訊息廣播出去即可,下面是具體的程式碼:

01 var app = http.createServer( onRequest ).listen( 8888 );
02 var WebSocketServer = require(`ws`).Server,
03 wss = new WebSocketServer( { server : app } );
04 wss.on(`connection`, function( ws ) {
05 console.log(`connection successful!`);
06 ws.on(`message`, function( data, flags ) {
07 console.log(data);
08 //do something here
09 });
10 ws.on(`close`, function() {
11 console.log(`stopping client`);
12 });
13 });


我們可以通過wss.clients獲得當前已連線的所有客戶端,然後遍歷,得到例項,呼叫send()方法傳送資料;

1 var clients = wss.clients, len = clients.length, i = 0;
2 for( ; i < len; i = i + 1 ){
3 clients[i].send( msg );
4 }


說到這裡,一個雙向通訊的例項基本完成,當然,上面都是虛擬碼,完整的demo請檢視附件。

除了常見的聊天程式以外,大家完全可以發揮創意,構建一些“好玩”的應用;

接下來,分享另外一個應用,“你畫我猜”這個應用,很多人都接觸過,大致上是:某個人在螢幕上畫一些圖形,這些圖片會實時展示在其它人的螢幕上,然後來猜畫的是什麼。

利用WebSocket和canvas,我們可以很輕鬆的構建類似的應用。當然,我們這裡只是demo,並沒有達到產品級的高度,這裡只是為大家提供思路;

首先,我們再次明確一下,WebSocket賦予了我們在瀏覽器端和伺服器進行雙向通訊的能力,這樣,我們可以實時的將資料傳送給伺服器,然後再廣播給所有的客戶端。這和聊天程式的思路是一致的。

接下來,伺服器端的程式碼不用做任何修改,在html頁面中準備一個canvas,作為我們的畫布。如何在canvas上用滑鼠畫圖形呢?我們需要監聽mousedown、mousemove和mouseup三個滑鼠事件。說到這裡,大家應該知道怎麼做了。沒錯,就是在按下滑鼠的時候,記錄當前的座標,移動滑鼠的時候,把座標傳送給伺服器,再由伺服器把座標資料廣播給所有的客戶端,這樣就可以在所有的客戶端上同步繪畫了;最後,mouseup的時候,做一些清理工作就ok了。下面是一些虛擬碼:

01 var WhiteBoard = function( socket, canvasId ){
02 var lastPoint = null,
03 mouseDown = false,
04 canvas = getById(canvasId),
05 ctx = canvas.getContext(`2d`);
06
07 var handleMouseDown = function(event) {
08 mouseDown = true;
09 lastPoint = resolveMousePosition.bind( canvas, event )();
10 };
11
12 var handleMouseUp = function(event) {
13 mouseDown = false;
14 lastPoint = null;
15 };
16
17 var handleMouseMove = function(event) {
18 if (!mouseDown) { return; }
19 var currentPoint = resolveMousePosition.bind( canvas, event )();
20 socket.send(JSON.stringify({
21 event: `draw`,
22 data: {
23 points: [
24 lastPoint.x,
25 lastPoint.y,
26 currentPoint.x,
27 currentPoint.y
28 ]
29 }
30 }));
31
32 lastPoint = currentPoint;
33 };        
34
35 var init = function(){
36 addEvent( canvas, `mousedown`, handleMouseDown );
37 addEvent( canvas, `mouseup`, handleMouseUp );
38 addEvent( canvas, `mousemove`, handleMouseMove );
39
40 var img = new Image();
41 addEvent( img, `load`, function(e){
42 canvas.width = img.width;
43 canvas.height = img.height;
44 ctx.drawImage( img, 0, 0 );
45 } );
46 img.src = `/img/diablo3.png`;
47 };
48
49 var drawLine = function(data) {
50 var points = data.points;
51 ctx.strokeStyle = `rgb(255, 15, 255)`;
52 ctx.beginPath();
53 ctx.moveTo( points[0] + 0.5, points[1] + 0.5 );
54 ctx.lineTo( points[2] + 0.5, points[3] + 0.5 );
55 ctx.stroke();
56 };
57
58 function resolveMousePosition(event) {
59 var x, y;
60 if (event.offsetX) {
61 x = event.offsetX;
62 y = event.offsetY;
63 } else {  //(注意)實際開發中,這樣獲取滑鼠相對canvas的座標是不對的
64 x = event.layerX - this.offsetLeft;
65 y = event.layerY - this.offsetTop;
66 }
67 return { x: x, y: y };
68 };
69
70 init();
71
72 return {
73 draw : drawLine
74 //ctx : ctx,
75 //canvas : canvas
76 }
77 }( websocket, `drawsomething` );

對於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,如需轉載請自行聯絡原作者


相關文章