HTML5規范在傳統的web交互基礎上為我們帶來了眾多的新特性,隨着web技術被廣泛用於web APP的開發,這些新特性得以推廣和使用,而websocket作為一種新的web通信技術具有巨大意義。
什么是socket?什么是websocket?兩者有什么區別?websocket是僅僅將socket的概念移植到瀏覽器中的實現嗎?
我們知道,在網絡中的兩個應用程序(進程)需要全雙工相互通信(全雙工即雙方可同時向對方發送消息),需要用到的就是socket,它能夠提供端對端通信,對於程序員來講,他只需要在某個應用程序的一端(暫且稱之為客戶端)創建一個socket實例並且提供它所要連接一端(暫且稱之為服務端)的IP地址和端口,而另外一端(服務端)創建另一個socket並綁定本地端口進行監聽,然后客戶端進行連接服務端,服務端接受連接之后雙方建立了一個端對端的TCP連接,在該連接上就可以雙向通訊了,而且一旦建立這個連接之后,通信雙方就沒有客戶端服務端之分了,提供的就是端對端通信了。我們可以采取這種方式構建一個桌面版的im程序,讓不同主機上的用戶發送消息。從本質上來說,socket並不是一個新的協議,它只是為了便於程序員進行網絡編程而對tcp/ip協議族通信機制的一種封裝。
websocket是html5規范中的一個部分,它借鑒了socket這種思想,為web應用程序客戶端和服務端之間(注意是客戶端服務端)提供了一種全雙工通信機制。同時,它又是一種新的應用層協議,websocket協議是為了提供web應用程序和服務端全雙工通信而專門制定的一種應用層協議,通常它表示為:ws://echo.websocket.org/?encoding=text HTTP/1.1,可以看到除了前面的協議名和http不同之外,它的表示地址就是傳統的url地址。
可以看到,websocket並不是簡單地將socket這一概念在瀏覽器環境中的移植,本文最后也會通過一個小的demo來進一步講述socket和websocket在使用上的區別。
websocket的通信原理和機制
既然是基於瀏覽器端的web技術,那么它的通信肯定少不了http,websocket本身雖然也是一種新的應用層協議,但是它也不能夠脫離http而單獨存在。具體來講,我們在客戶端構建一個websocket實例,並且為它綁定一個需要連接到的服務器地址,當客戶端連接服務端的時候,會向服務端發送一個類似下面的http報文
可以看到,這是一個http get請求報文,注意該報文中有一個upgrade首部,它的作用是告訴服務端需要將通信協議切換到websocket,如果服務端支持websocket協議,那么它就會將自己的通信協議切換到websocket,同時發給客戶端類似於以下的一個響應報文頭
返回的狀態碼為101,表示同意客戶端協議轉換請求,並將它轉換為websocket協議。以上過程都是利用http通信完成的,稱之為websocket協議握手(websocket Protocol handshake),進過這握手之后,客戶端和服務端就建立了websocket連接,以后的通信走的都是websocket協議了。所以總結為websocket握手需要借助於http協議,建立連接后通信過程使用websocket協議。同時需要了解的是,該websocket連接還是基於我們剛才發起http連接的那個TCP連接。一旦建立連接之后,我們就可以進行數據傳輸了,websocket提供兩種數據傳輸:文本數據和二進制數據。
基於以上分析,我們可以看到,websocket能夠提供低延遲,高性能的客戶端與服務端的雙向數據通信。它顛覆了之前web開發的請求處理響應模式,並且提供了一種真正意義上的客戶端請求,服務器推送數據的模式,特別適合實時數據交互應用開發。
在websocket之前,我們在web上要得到實時數據交互都采用了哪些方式?
1)定期輪詢的方式:
客戶端按照某個時間間隔不斷地向服務端發送請求,請求服務端的最新數據然后更新客戶端顯示。這種方式實際上浪費了大量流量並且對服務端造成了很大壓力。
2)comet技術
comet並不是一種新的通信技術,它是在客戶端請求服務端這個模式上的一種hack技術,通常來講,它主要分為以下兩種做法
(1)基於長輪詢的服務端推送技術
具體來講,就是客戶端首先給服務端發送一個請求,服務端收到該請求之后如果數據沒有更新則並不立即返回,服務端阻塞請求的返回,直到數據發生了更新或者發生了連接超時,服務端返回數據之后客戶端再次發送同樣的請求,如下所示:
2)基於流式數據傳輸的長連接
通常的做法是在頁面中嵌入一個隱藏的iframe,然后讓這個iframe的src屬性指向我們請求的一個服務端地址,並且為了數據更新,我們將頁面上數據更新操作封裝為一個js函數,將函數名當做參數傳遞到這個地址當中,
服務端收到請求后解析地址取出參數(客戶端js函數調用名),每當有數據更新的時候,返回對客戶端函數的調用,並且將要跟新的數據以js函數的參數填入到返回內容當中,例如返回“<script type="text/javascript">update("data")</script>
”這樣一個字符串,意味着以data為參數調用客戶端update函數進行客戶端view更新。基本模型如下所示:
可以看到comet技術是針對客戶端請求服務器響應模型而模擬出的一個服務端推送數據實時更新技術。而且由於瀏覽器兼容性不能夠廣泛應用。
當然並不是說這些技術沒有用,就算websocket已經作為規范被提出並實現,但是對於老式瀏覽器,我們依然需要將它降級為以上方式來實現實時交互和服務端數據推送。
到此為止,我們明白了websocket的原理,下面通過一個簡單的聊天應用來再次加深下對websocket的理解。
該應用需求很簡單,就是在web選項卡中打開兩個網頁,模擬兩個web客戶端實現聊天功能。
首先是客戶端如下:
client.html
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <style> *{ margin: 0; padding: 0; } .message{ width: 60%; margin: 0 10px; display: inline-block; text-align: center; height: 40px; line-height: 40px; font-size: 20px; border-radius: 5px; border: 1px solid #B3D33F; } .form{ width:100%; position: fixed; bottom: 300px; left: 0; } .connect{ height: 40px; vertical-align: top; /* padding: 0; */ width: 80px; font-size: 20px; border-radius: 5px; border: none; background: #B3D33F; color: #fff; } </style> </head> <body> <ul id="content"></ul> <form class="form"> <input type="text" placeholder="請輸入發送的消息" class="message" id="message"/> <input type="button" value="發送" id="send" class="connect"/> <input type="button" value="連接" id="connect" class="connect"/> </form> <script></script> </body> </html>
客戶端js代碼
var oUl=document.getElementById('content'); var oConnect=document.getElementById('connect'); var oSend=document.getElementById('send'); var oInput=document.getElementById('message'); var ws=null; oConnect.onclick=function(){ ws=new WebSocket('ws://localhost:5000'); ws.onopen=function(){ oUl.innerHTML+="<li>客戶端已連接</li>"; } ws.onmessage=function(evt){ oUl.innerHTML+="<li>"+evt.data+"</li>"; } ws.onclose=function(){ oUl.innerHTML+="<li>客戶端已斷開連接</li>"; }; ws.onerror=function(evt){ oUl.innerHTML+="<li>"+evt.data+"</li>"; }; }; oSend.onclick=function(){ if(ws){ ws.send(oInput.value); } }
這里使用的是w3c規范中關於HTML5 websocket API的原生API,這些api很簡單,就是利用new WebSocket創建一個指定連接服務端地址的ws實例,然后為該實例注冊onopen(連接服務端),onmessage(接受服務端數據),onclose(關閉連接)以及ws.send(建立連接后)發送請求。上面說了那么多,事實上可以看到html5 websocket API本身是很簡單的一個對象和它的幾個方法而已。
服務端采用nodejs,這里需要基於一個nodejs-websocket的nodejs服務端的庫,它是一個輕量級的nodejs websocket server端的實現,實際上也是使用nodejs提供的net模塊寫成的。
server.js
var app=require('http').createServer(handler); var ws=require('nodejs-websocket'); var fs=require('fs'); app.listen(80); function handler(req,res){ fs.readFile(__dirname+'/client.html',function(err,data){ if(err){ res.writeHead(500); return res.end('error '); } res.writeHead(200); res.end(data); }); } var server=ws.createServer(function(conn){ console.log('new conneciton'); conn.on("text",function(str){ broadcast(server,str); }); conn.on("close",function(code,reason){ console.log('connection closed'); }) }).listen(5000); function broadcast(server, msg) { server.connections.forEach(function (conn) { conn.sendText(msg); }) }
首先利用http模塊監聽用戶的http請求並顯示client.html界面,然后創建一個websocket服務端等待用戶連接,在接收到用戶發送來的數據之后將它廣播到所有連接到的客戶端。
下面我們打開兩個瀏覽器選項卡模擬兩個客戶端進行連接,
客戶端一連接:
請求響應報文如下:
可以看到這次握手和我們之前講的如出一轍,
客戶端二的連接過程和1是一樣的,這里為了查看我們使用ff瀏覽器,兩個客戶端的連接情況如下:
發送消息情況如下:
可以看到,雙方發送的消息被服務端廣播給了每個和自己連接的客戶端。
從以上我們可以看到,要想做一個點對點的im應用,websocket采取的方式是讓所有客戶端連接服務端,服務器將不同客戶端發送給自己的消息進行轉發或者廣播,而對於原始的socket,只要兩端建立連接之后,就可以發送端對端的數據,不需要經過第三方的轉發,這也是websocket不同於socket的一個重要特點。
最后,本文為了說明html5規范中的websocket在客戶端采用了原生的API,實際開發中,有比較著名的兩個庫socket.io和sockjs,它們都對原始的API做了進一步封裝,提供了更多功能,都分為客戶端和服務端的實現,實際應用中,可以選擇使用。