HTML5規(guī)范在傳統(tǒng)的web交互基礎(chǔ)上為我們帶來了眾多的新特性,隨著web技術(shù)被廣泛用于web APP的開發(fā),這些新特性得以推廣和使用,而websocket作為一種新的web通信技術(shù)具有巨大意義。
什么是socket?什么是websocket?兩者有什么區(qū)別?websocket是僅僅將socket的概念移植到瀏覽器中的實(shí)現(xiàn)嗎?
我們知道,在網(wǎng)絡(luò)中的兩個(gè)應(yīng)用程序(進(jìn)程)需要全雙工相互通信(全雙工即雙方可同時(shí)向?qū)Ψ桨l(fā)送消息),需要用到的就是socket,它能夠提供端對(duì)端通信,對(duì)于程序員來講,他只需要在某個(gè)應(yīng)用程序的一端(暫且稱之為客戶端)創(chuàng)建一個(gè)socket實(shí)例并且提供它所要連接一端(暫且稱之為服務(wù)端)的IP地址和端口,而另外一端(服務(wù)端)創(chuàng)建另一個(gè)socket并綁定本地端口進(jìn)行監(jiān)聽,然后客戶端進(jìn)行連接服務(wù)端,服務(wù)端接受連接之后雙方建立了一個(gè)端對(duì)端的TCP連接,在該連接上就可以雙向通訊了,而且一旦建立這個(gè)連接之后,通信雙方就沒有客戶端服務(wù)端之分了,提供的就是端對(duì)端通信了。我們可以采取這種方式構(gòu)建一個(gè)桌面版的im程序,讓不同主機(jī)上的用戶發(fā)送消息。從本質(zhì)上來說,socket并不是一個(gè)新的協(xié)議,它只是為了便于程序員進(jìn)行網(wǎng)絡(luò)編程而對(duì)tcp/ip協(xié)議族通信機(jī)制的一種封裝。
websocket是html5規(guī)范中的一個(gè)部分,它借鑒了socket這種思想,為web應(yīng)用程序客戶端和服務(wù)端之間(注意是客戶端服務(wù)端)提供了一種全雙工通信機(jī)制。同時(shí),它又是一種新的應(yīng)用層協(xié)議,websocket協(xié)議是為了提供web應(yīng)用程序和服務(wù)端全雙工通信而專門制定的一種應(yīng)用層協(xié)議,通常它表示為:ws://echo.websocket.org/?encoding=text HTTP/1.1,可以看到除了前面的協(xié)議名和http不同之外,它的表示地址就是傳統(tǒng)的url地址。
可以看到,websocket并不是簡單地將socket這一概念在瀏覽器環(huán)境中的移植,本文最后也會(huì)通過一個(gè)小的demo來進(jìn)一步講述socket和websocket在使用上的區(qū)別。
websocket的通信原理和機(jī)制
既然是基于瀏覽器端的web技術(shù),那么它的通信肯定少不了http,websocket本身雖然也是一種新的應(yīng)用層協(xié)議,但是它也不能夠脫離http而單獨(dú)存在。具體來講,我們?cè)诳蛻舳藰?gòu)建一個(gè)websocket實(shí)例,并且為它綁定一個(gè)需要連接到的服務(wù)器地址,當(dāng)客戶端連接服務(wù)端的時(shí)候,會(huì)向服務(wù)端發(fā)送一個(gè)類似下面的http報(bào)文
可以看到,這是一個(gè)http get請(qǐng)求報(bào)文,注意該報(bào)文中有一個(gè)upgrade首部,它的作用是告訴服務(wù)端需要將通信協(xié)議切換到websocket,如果服務(wù)端支持websocket協(xié)議,那么它就會(huì)將自己的通信協(xié)議切換到websocket,同時(shí)發(fā)給客戶端類似于以下的一個(gè)響應(yīng)報(bào)文頭
返回的狀態(tài)碼為101,表示同意客戶端協(xié)議轉(zhuǎn)換請(qǐng)求,并將它轉(zhuǎn)換為websocket協(xié)議。以上過程都是利用http通信完成的,稱之為websocket協(xié)議握手(websocket Protocol handshake),進(jìn)過這握手之后,客戶端和服務(wù)端就建立了websocket連接,以后的通信走的都是websocket協(xié)議了。所以總結(jié)為websocket握手需要借助于http協(xié)議,建立連接后通信過程使用websocket協(xié)議。同時(shí)需要了解的是,該websocket連接還是基于我們剛才發(fā)起http連接的那個(gè)TCP連接。一旦建立連接之后,我們就可以進(jìn)行數(shù)據(jù)傳輸了,websocket提供兩種數(shù)據(jù)傳輸:文本數(shù)據(jù)和二進(jìn)制數(shù)據(jù)。
基于以上分析,我們可以看到,websocket能夠提供低延遲,高性能的客戶端與服務(wù)端的雙向數(shù)據(jù)通信。它顛覆了之前web開發(fā)的請(qǐng)求處理響應(yīng)模式,并且提供了一種真正意義上的客戶端請(qǐng)求,服務(wù)器推送數(shù)據(jù)的模式,特別適合實(shí)時(shí)數(shù)據(jù)交互應(yīng)用開發(fā)。
在websocket之前,我們?cè)趙eb上要得到實(shí)時(shí)數(shù)據(jù)交互都采用了哪些方式?
1)定期輪詢的方式:
客戶端按照某個(gè)時(shí)間間隔不斷地向服務(wù)端發(fā)送請(qǐng)求,請(qǐng)求服務(wù)端的最新數(shù)據(jù)然后更新客戶端顯示。這種方式實(shí)際上浪費(fèi)了大量流量并且對(duì)服務(wù)端造成了很大壓力。
2)comet技術(shù)
comet并不是一種新的通信技術(shù),它是在客戶端請(qǐng)求服務(wù)端這個(gè)模式上的一種hack技術(shù),通常來講,它主要分為以下兩種做法
(1)基于長輪詢的服務(wù)端推送技術(shù)
具體來講,就是客戶端首先給服務(wù)端發(fā)送一個(gè)請(qǐng)求,服務(wù)端收到該請(qǐng)求之后如果數(shù)據(jù)沒有更新則并不立即返回,服務(wù)端阻塞請(qǐng)求的返回,直到數(shù)據(jù)發(fā)生了更新或者發(fā)生了連接超時(shí),服務(wù)端返回?cái)?shù)據(jù)之后客戶端再次發(fā)送同樣的請(qǐng)求,如下所示:
2)基于流式數(shù)據(jù)傳輸?shù)拈L連接
通常的做法是在頁面中嵌入一個(gè)隱藏的iframe,然后讓這個(gè)iframe的src屬性指向我們請(qǐng)求的一個(gè)服務(wù)端地址,并且為了數(shù)據(jù)更新,我們將頁面上數(shù)據(jù)更新操作封裝為一個(gè)js函數(shù),將函數(shù)名當(dāng)做參數(shù)傳遞到這個(gè)地址當(dāng)中,
服務(wù)端收到請(qǐng)求后解析地址取出參數(shù)(客戶端js函數(shù)調(diào)用名),每當(dāng)有數(shù)據(jù)更新的時(shí)候,返回對(duì)客戶端函數(shù)的調(diào)用,并且將要跟新的數(shù)據(jù)以js函數(shù)的參數(shù)填入到返回內(nèi)容當(dāng)中,例如返回“<script type="text/javascript">update("data")</script>
”這樣一個(gè)字符串,意味著以data為參數(shù)調(diào)用客戶端update函數(shù)進(jìn)行客戶端view更新?;灸P腿缦滤荆?/p>
可以看到comet技術(shù)是針對(duì)客戶端請(qǐng)求服務(wù)器響應(yīng)模型而模擬出的一個(gè)服務(wù)端推送數(shù)據(jù)實(shí)時(shí)更新技術(shù)。而且由于瀏覽器兼容性不能夠廣泛應(yīng)用。
當(dāng)然并不是說這些技術(shù)沒有用,就算websocket已經(jīng)作為規(guī)范被提出并實(shí)現(xiàn),但是對(duì)于老式瀏覽器,我們依然需要將它降級(jí)為以上方式來實(shí)現(xiàn)實(shí)時(shí)交互和服務(wù)端數(shù)據(jù)推送。
到此為止,我們明白了websocket的原理,下面通過一個(gè)簡單的聊天應(yīng)用來再次加深下對(duì)websocket的理解。
該應(yīng)用需求很簡單,就是在web選項(xiàng)卡中打開兩個(gè)網(wǎng)頁,模擬兩個(gè)web客戶端實(shí)現(xiàn)聊天功能。
首先是客戶端如下:
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="請(qǐng)輸入發(fā)送的消息" class="message" id="message"/><input type="button" value="發(fā)送" 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規(guī)范中關(guān)于HTML5 websocket API的原生API,這些api很簡單,就是利用new WebSocket創(chuàng)建一個(gè)指定連接服務(wù)端地址的ws實(shí)例,然后為該實(shí)例注冊(cè)onopen(連接服務(wù)端),onmessage(接受服務(wù)端數(shù)據(jù)),onclose(關(guān)閉連接)以及ws.send(建立連接后)發(fā)送請(qǐng)求。上面說了那么多,事實(shí)上可以看到html5 websocket API本身是很簡單的一個(gè)對(duì)象和它的幾個(gè)方法而已。
服務(wù)端采用nodejs,這里需要基于一個(gè)nodejs-websocket的nodejs服務(wù)端的庫,它是一個(gè)輕量級(jí)的nodejs websocket server端的實(shí)現(xiàn),實(shí)際上也是使用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模塊監(jiān)聽用戶的http請(qǐng)求并顯示client.html界面,然后創(chuàng)建一個(gè)websocket服務(wù)端等待用戶連接,在接收到用戶發(fā)送來的數(shù)據(jù)之后將它廣播到所有連接到的客戶端。
下面我們打開兩個(gè)瀏覽器選項(xiàng)卡模擬兩個(gè)客戶端進(jìn)行連接,
客戶端一連接:
請(qǐng)求響應(yīng)報(bào)文如下:
可以看到這次握手和我們之前講的如出一轍,
客戶端二的連接過程和1是一樣的,這里為了查看我們使用ff瀏覽器,兩個(gè)客戶端的連接情況如下:
發(fā)送消息情況如下:
可以看到,雙方發(fā)送的消息被服務(wù)端廣播給了每個(gè)和自己連接的客戶端。
從以上我們可以看到,要想做一個(gè)點(diǎn)對(duì)點(diǎn)的im應(yīng)用,websocket采取的方式是讓所有客戶端連接服務(wù)端,服務(wù)器將不同客戶端發(fā)送給自己的消息進(jìn)行轉(zhuǎn)發(fā)或者廣播,而對(duì)于原始的socket,只要兩端建立連接之后,就可以發(fā)送端對(duì)端的數(shù)據(jù),不需要經(jīng)過第三方的轉(zhuǎn)發(fā),這也是websocket不同于socket的一個(gè)重要特點(diǎn)。
最后,本文為了說明html5規(guī)范中的websocket在客戶端采用了原生的API,實(shí)際開發(fā)中,有比較著名的兩個(gè)庫socket.io和sockjs,它們都對(duì)原始的API做了進(jìn)一步封裝,提供了更多功能,都分為客戶端和服務(wù)端的實(shí)現(xiàn),實(shí)際應(yīng)用中,可以選擇使用。
聯(lián)系客服