WebSocket
(部分來自百科)
WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通信(full-duplex),在瀏覽器中通過http僅能實現單向的通信,comet可以一定程度上模擬雙向通信,但效率較低,並需要服務器有較好的支持; flash中的socket和xmlsocket可以實現真正的雙向通信,通過 flex ajax bridge,可以在javascript中使用這兩項功能. 可以預見,如果websocket一旦在瀏覽器中得到實現,將會替代上面兩項技術,得到廣泛的使用.面對這種狀況,HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬並達到實時通訊,在JavaEE7中也實現了WebSocket協議。
WebSocket 原理:
現很多網站爲了實現即時通訊,所用的技術都是輪詢(polling)。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP request,然後由服務器返回最新的數據給客服端的瀏覽器。這種傳統的HTTP request 的模式帶來很明顯的缺點 – 瀏覽器需要不斷的向服務器發出請求,然而HTTP request 的header是非常長的,裏面包含的有用數據可能只是一個很小的值,這樣會佔用很多的帶寬。
而最比較新的技術去做輪詢的效果是Comet – 用了AJAX。但這種技術雖然可達到全雙工通信,但依然需要發出請求。
在 WebSocket API,瀏覽器和服務器只需要要做一個握手的動作,然後,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數據互相傳送。在此WebSocket 協議中,爲我們實現即時服務帶來了兩大好處:
1. Header
互相溝通的Header是很小的-大概只有 2 Bytes
2. Server Push
在實現websocket連線過程中,需要通過瀏覽器發出websocket連線請求,然後服務器發出迴應,這個過程通常稱爲"握手" (handshaking)。
實現了websocket的瀏覽器: Chrome、Firefox、Internet Explorer、Opera、Safari。
注意:websocket api在瀏覽器端的廣泛實現似乎只是一個時間問題了, 值得注意的是服務器端沒有標準的api, 各個實現都有自己的一套api, 並且jcp也沒有類似的提案, 所以使用websocket開發服務器端有一定的風險.可能會被鎖定在某個平臺上或者將來被迫升級.
一、WebSocket是HTML5出的東西(協議),也就是說HTTP協議沒有變化,或者說沒關係,但HTTP是不支持持久連接的(長連接,循環連接的不算)
首先HTTP有1.1和1.0之說,也就是所謂的keep-alive,把多個HTTP請求合併爲一個,但是Websocket其實是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是HTTP協議上的一種補充可以通過這樣一張圖理解
<img src="https://pic1.zhimg.com/6651f2f811ec133b0e6d7e6d0e194b4c_b.jpg" data-rawwidth="374" data-rawheight="133" class="content_image" width="374">有交集,但是並不是全部。有交集,但是並不是全部。
另外Html5是指的一系列新的API,或者說新規範,新技術。Http協議本身只有1.0和1.1,而且跟Html本身沒有直接關係。。
通俗來說,你可以用HTTP協議傳輸非Html數據,就是這樣。再簡單來說,層級不一樣。
二、Websocket是什麼樣的協議,具體有什麼優點
首先,Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來說。
簡單的舉個例子吧,用目前應用比較廣泛的PHP生命週期來解釋。
1) HTTP的生命週期通過Request來界定,也就是一個Request 一個Response,那麼在HTTP1.0中,這次HTTP請求就結束了。
在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP連接中,可以發送多個Request,接收多個Response。
但是請記住 Request = Response , 在HTTP中永遠是這樣,也就是說一個request只能有一個response。而且這個response也是被動的,不能主動發起。
Websocket呢。。
首先Websocket是基於HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。
在握手階段是一樣的:
首先我們來看個典型的Websocket握手(借用Wikipedia的。。)
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
我會順便講解下作用。
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
然後,Sec_WebSocket-Protocol 是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。簡單理解:今晚我要服務A,別搞錯啦~
最後,Sec-WebSocket-Version 是告訴服務器所使用的Websocket Draft(協議版本),在最初的時候,Websocket協議還在 Draft 階段,各種奇奇怪怪的協議都有,而且還有很多期奇奇怪怪不同的東西,什麼Firefox和Chrome用的不是一個版本之類的,當初Websocket協議太多可是一個大難題。。不過現在還好,已經定下來啦~大家都使用的一個東西~ 脫水:服務員,我要的是13歲的噢→_→
然後服務器會返回下列東西,表示已經接受到請求, 成功建立Websocket啦!
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
Upgrade: websocket
Connection: Upgrade
然後,Sec-WebSocket-Accept 這個則是經過服務器確認,並且加密過後的 Sec-WebSocket-Key。服務器:好啦好啦,知道啦,給你看我的ID CARD來證明行了吧。。
後面的,Sec-WebSocket-Protocol 則是表示最終使用的協議。
至此,HTTP已經完成它所有工作了,接下來就是完全按照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在移動終端上的死亡。
下面是輪詢和長輪詢的信息流轉圖:
對比完四種不同的實時通信方式,不難發現,除了基於flash的方案外,其它三種方式都是用AJAX方式來模擬實時的效果,每次客戶端和服務器端交互時,都是一次完整的HTTP請求和應答的過程,而每一次的HTTP請求和應答都帶有完整的HTTP頭信息,這就增加每次的數據傳輸量,而且這些方案中客戶端和服務端的編程實現比較複雜。
接下來,我們再來看一下WebSocket,爲什麼要使用它呢?高效節能,簡單易用。
下圖是來自websocket.org的測試結果:
在流量和負載增大的情況下,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實現,安裝方法很簡單,在終端或者命令行中輸入:
npm install ws
,等待安裝完成就可以了。
接下來,我們需要啓動我們的WebSocket服務。首先,我們需要構建自己的HTTP服務器,在NodeJS中構建一個簡單的HTTP服務器很簡單,so easy。代碼如下:
var app = http.createServer(onRequest ).listen( 8888 );
onRequest()作爲回調函數,它的作用是處理請求,然後做出響應,實際上就是根據接收的URL,在服務器上查找相應的資源,最終返回給瀏覽器。
在構建了HTTP服務器後,我們需要啓動WebSocket服務,代碼如下:
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer( { server : app } );
從代碼中可以看出,在初始化WebSocket服務時,把我們剛纔構建好的HTTP實例傳遞進去就好。到這裏,服務端代碼差不多也就編寫完成了。怎麼樣?很簡單吧。
五、WebSocket API
上面我們介紹了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本身很簡單,所以,我們的客戶端代碼也很直接,如下:
var wsServer = 'ws://localhost:8888/';
var websocket = new WebSocket(wsServer);
websocket.binaryType = "arraybuffer";
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
websocket.onerror = onError;
首先,我們需要指定WebSocket的服務地址,也就是var wsServer = 'ws://localhost:8888/';
然後,我們實例化WebSocket,new WebSocket(wsServer),
onmessage()這個回調函數會在客戶端收到消息時觸發,也就是說,只要服務器端發送了消息,我們就可以通過onmessage拿到發送的數據,既然拿到了數據,接下去該怎麼玩,就隨便我們了。請看下面的僞代碼:剩下的就是指定相應的回調函數了,分別是onOpen,onClose,onMessage和onError,對於咱們的實驗應用來說,onopen、onclose、onerror甚至可以不管,咱們重點關注一下onmessage。
function onMessage(evt) {
var json = JSON.parse(evt.data);
commands[json.event](json.data);
}
因爲onmessage只接收字符串和二進制類型的數據,如果需要發送json格式的數據,就需要我們轉換一下格式,把字符串轉換成JSON格式。只要是支持WebSocket,肯定原生支持window.JSON,所以,我們可以直接使用JSON.parse()和JSON.stringify()來進行轉換。
轉換完成後,我們就得到了我們想要的數據了,接下來所做的工作就是將消息顯示出來。實際上就是
Elements.innerHTML += data + '</br>';
上面展現了客戶端的代碼,服務器端的代碼相對要簡單一些,因爲我們的服務器端使用的是第三方實現,我們只需要做一些初始化工作,然後在接收到消息時,將消息廣播出去即可,下面是具體的代碼:
var app = http.createServer( onRequest ).listen( 8888 );
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer( { server : app } );
wss.on('connection', function( ws ) {
console.log('connection successful!');
ws.on('message', function( data, flags ) {
console.log(data);
//do something here
});
ws.on('close', function() {
console.log('stopping client');
});
});
我們可以通過wss.clients獲得當前已連接的所有客戶端,然後遍歷,得到實例,調用send()方法發送數據;
var clients = wss.clients, len = clients.length, i = 0;
for( ; i <len; i = i + 1 ){
clients[i].send( msg );
}
說到這裏,一個雙向通信的實例基本完成,當然,上面都是僞代碼,完整的demo請查看附件。
除了常見的聊天程序以外,大家完全可以發揮創意,構建一些“好玩”的應用;
接下來,分享另外一個應用,“你畫我猜”這個應用,很多人都接觸過,大致上是:某個人在屏幕上畫一些圖形,這些圖片會實時展示在其它人的屏幕上,然後來猜畫的是什麼。
利用WebSocket和canvas,我們可以很輕鬆的構建類似的應用。當然,我們這裏只是demo,並沒有達到產品級的高度,這裏只是爲大家提供思路;
首先,我們再次明確一下,WebSocket賦予了我們在瀏覽器端和服務器進行雙向通信的能力,這樣,我們可以實時的將數據發送給服務器,然後再廣播給所有的客戶端。這和聊天程序的思路是一致的。
接下來,服務器端的代碼不用做任何修改,在html頁面中準備一個canvas,作爲我們的畫布。如何在canvas上用鼠標畫圖形呢?我們需要監聽mousedown、mousemove和mouseup三個鼠標事件。說到這裏,大家應該知道怎麼做了。沒錯,就是在按下鼠標的時候,記錄當前的座標,移動鼠標的時候,把座標發送給服務器,再由服務器把座標數據廣播給所有的客戶端,這樣就可以在所有的客戶端上同步繪畫了;最後,mouseup的時候,做一些清理工作就ok了。下面是一些僞代碼:
var WhiteBoard = function( socket, canvasId ){
var lastPoint = null,
mouseDown = false,
canvas = getById(canvasId),
ctx = canvas.getContext('2d');
var handleMouseDown = function(event) {
mouseDown = true;
lastPoint = resolveMousePosition.bind( canvas, event )();
};
var handleMouseUp = function(event) {
mouseDown = false;
lastPoint = null;
};
var handleMouseMove = function(event) {
if (!mouseDown) { return; }
var currentPoint = resolveMousePosition.bind( canvas, event )();
socket.send(JSON.stringify({
event: 'draw',
data: {
points: [
lastPoint.x,
lastPoint.y,
currentPoint.x,
currentPoint.y
]
}
}));
lastPoint = currentPoint;
};
var init = function(){
addEvent( canvas, 'mousedown', handleMouseDown );
addEvent( canvas, 'mouseup', handleMouseUp );
addEvent( canvas, 'mousemove', handleMouseMove );
var img = new Image();
addEvent( img, 'load', function(e){
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage( img, 0, 0 );
} );
img.src = '/img/diablo3.png';
};
var drawLine = function(data) {
var points = data.points;
ctx.strokeStyle = 'rgb(255, 15, 255)';
ctx.beginPath();
ctx.moveTo( points[0] + 0.5, points[1] + 0.5 );
ctx.lineTo( points[2] + 0.5, points[3] + 0.5 );
ctx.stroke();
};
function resolveMousePosition(event) {
var x, y;
if (event.offsetX) {
x = event.offsetX;
y = event.offsetY;
} else { //(注意)實際開發中,這樣獲取鼠標相對canvas的座標是不對的
x = event.layerX - this.offsetLeft;
y = event.layerY - this.offsetTop;
}
return { x: x, y: y };
};
init();
return {
draw : drawLine
//ctx : ctx,
//canvas : canvas
}
}( websocket, 'drawsomething' );
對於canvas不熟悉的同學,請自己去搜索一下,有許多不錯的教程。其它方面,和聊天應用的思路基本一樣。
最後,我們需要明確一點,WebSocket本身的優點很明顯,但是作爲一個正在演變中的web規範,我們必須清楚的認識到WebSocket在構建應用時的一些風險;雖然本身有很多侷限性,但是這項技術本身肯定是大勢所趨,WebSocket在移動終端,在chrome web store都有用武之地,我們可以進行大膽的嘗試,讓我們在技術的革新中不被淘汰。
Resources:
W3 API的官方文檔,有詳細的接口設計文檔和實現步驟http://www.w3.org/TR/websockets/
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.