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在移動終端上的死亡。

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

接下來,我們再來看一下Websocket,爲什麼要使用它呢?高效節能,簡單易用。在流量和負載增大的情況下,Websocket 方案相比傳統的 Ajax 輪詢方案有極大的性能優勢;而在開發方面,也十分簡單,我們只需要實例化Websocket,創建連接,查看是否連接成功,然後就可以發送和相應消息了。

四、Websocket通訊過程

1、客戶端發起連接請求

Websocket客戶端首先發起一個連接請求,發送的數據格式如下:

GET /chat?key=value HTTP/1.1\r\n
Upgrade: Websocket\r\n
Connection: Upgrade\r\n
Host: 10.15.1.218:12345\r\n
Sec-Websocket-Origin: null\r\n
Sec-Websocket-Key: 4tAjitqO9So2Wu8lkrsq3w==\r\n
Sec-Websocket-Version: 13\r\n\r\n

這是類似於HTTP的頭,注意每行數據結尾結束符是“\r\n”, 最後的結束符是“\r\n\r\n”。

Sec-Websocket-Key後面的那一串長度爲24的字符串是客戶端隨機生成16位字符再通過base64編碼生成的,我們暫時叫它cli_key。服務器必須用它經過一定的運算規則生成服務器端的key,暫時叫做ser_key,然後把ser_key發回給客戶端,客戶端驗證正確後,握手成功。

2、生成服務端的密鑰

服務器將cli_key(長度24)截取出來 

4tAjitqO9So2Wu8lkrsq3w==

用它和如下的字符串(該字符串固定,長度36): 

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

連接起來,如下:

4tAjitqO9So2Wu8lkrsq3w==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

然後把這一長串經過SHA-1算法加密,得到長度爲20字節的二進制數據,

再將這些數據經過Base64編碼,最終得到服務端的密鑰,也就是ser_key:

bEVeGLZrb9fS3Rj8WzExJdCsedg=

3、服務端返回密鑰

服務端需要把密鑰返回給客戶端,完成握手,發送的數據格式如下:

HTTP/1.1 101 Switching Protocols\r\n

Upgrade: Websocket\r\n

Connection: Upgrade\r\n

Sec-Websocket-Accept: bEVeGLZrb9fS3Rj8WzExJdCsedg=\r\n\r\n

至此,算是握手成功了!

4、傳輸數據

客戶端與服務器的傳輸數據格式如下:

FIN:1位,用來表明這是一個消息的最後的消息片斷,當然第一個消息片斷也可能是最後的一個消息片斷;

RSV1, RSV2, RSV3: 分別都是1位,如果雙方之間沒有約定自定義協議,那麼這幾位的值都必須爲0,否則必須斷掉Websocket連接;

Opcode:4位操作碼,定義有效負載數據,如果收到了一個未知的操作碼,連接也必須斷掉,以下是定義的操作碼:
*  %x0 表示連續消息片斷
*  %x1 表示文本消息片斷
*  %x2 表未二進制消息片斷
*  %x3-7 爲將來的非控制消息片斷保留的操作碼
*  %x8 表示連接關閉
*  %x9 表示心跳檢查的ping
*  %xA 表示心跳檢查的pong
*  %xB-F 爲將來的控制消息片斷的保留操作碼

Mask:1位,定義傳輸的數據是否有加掩碼,如果設置爲1,掩碼鍵必須放在masking-key區域,客戶端發送給服務端的所有消息,此位的值都是1;

Payload length: 傳輸數據的長度,以字節的形式表示:7位、7+16位、或者7+64位。如果這個值以字節表示是0-125這個範圍,那這個值就表示傳輸數據的長度;如果這個值是126,則隨後的兩個字節表示的是一個16進制無符號數,用來表示傳輸數據的長度;如果這個值是127,則隨後的是8個字節表示的一個64位無符合數,這個數用來表示傳輸數據的長度。多字節長度的數量是以網絡字節的順序表示。負載數據的長度爲擴展數據及應用數據之和,擴展數據的長度可能爲0,因而此時負載數據的長度就爲應用數據的長度。

Masking-key:0或4個字節,客戶端發送給服務端的數據,都是通過內嵌的一個32位值作爲掩碼的;掩碼鍵只有在掩碼位設置爲1的時候存在。第一個字節的數據與掩碼的第一個字節異或,第二個字節的數據與掩碼的第二個字節異或……第五個字節的數據與掩碼的第五個字節異或,依次類推,直到結束。
Payload data:  (x+y)位,負載數據爲擴展數據及應用數據長度之和。
Extension data:x位,如果客戶端與服務端之間沒有特殊約定,那麼擴展數據的長度始終爲0,任何的擴展都必須指定擴展數據的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴展數據,則擴展數據就會包括在負載數據的長度之內。
Application data:y位,任意的應用數據,放在擴展數據之後,應用數據的長度=負載數據的長度-擴展數據的長度。

五、Websocket與TCP,HTTP的關係

Websocket與http協議一樣都是基於TCP的,所以他們都是可靠的協議,Web開發者調用的Websocket的send函數在browser的實現中最終都是通過TCP的系統接口進行傳輸的。Websocket和Http協議一樣都屬於應用層的協議,那麼他們之間有沒有什麼關係呢?答案是肯定的,Websocket在建立握手連接時,數據是通過http協議傳輸的,正如我們上一節所看到的“GET/chat HTTP/1.1”,這裏面用到的只是http協議一些簡單的字段。但是在建立連接之後,真正的數據傳輸階段是不需要http協議參與的。

六、搭建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也是一如既往的簡單。
ready state中定義的是socket的狀態,分爲connection、open、closing和closed四種狀態,從字面上就可以區分出它們所代表的狀態。

Websocket的事件,分爲onopen、onerror和onclose。

八、實例解析

搭建好了服務端,熟悉了API,接下來,我們要開始構建我們的應用了。鑑於Websocket自身的特點,我們的第一個demo選擇了比較常見的聊天程序,我們暫且取名爲chat。

說到聊天,大家最先想到的肯定是QQ,沒錯,我們所實現的應用和QQ類似,而且還是基於web的。因爲是demo,我們的功能比較簡陋,僅實現了最簡單的會話功能。就是啓動Websocket服務器後,客戶端發起連接,連接成功後,任意客戶端發送消息,都會被服務器廣播給所有已連接的客戶端,包括自己。

既然需要客戶端,我們需要構建一個簡單的html頁面,頁面中樣式和元素,大家可以自由發揮,只要能夠輸入消息,有發送按鈕,最後有一個展示消息的區域即可。寫完HTML頁面之後,我們需要添加客戶端腳本,也就是和Websocket相關的代碼;前面我們說過,Websocket的API本身很簡單,所以,我們的客戶端代碼也很直接,如下:

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拿到發送的數據,既然拿到了數據,接下去該怎麼玩,就隨便我們了。請看下面的僞代碼:

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()來進行轉換。
轉換完成後,我們就得到了我們想要的數據了,接下來所做的工作就是將消息顯示出來。

上面展現了客戶端的代碼,服務器端的代碼相對要簡單一些,在此略去。

Resources:
http://www.w3.org/TR/Websockets/

http://msdn.microsoft.com/en-us/library/ie/hh673567(v=vs.85).aspx

https://developer.mozilla.org/en-US/docs/Websockets

http://caniuse.com/#feat=Websockets

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章