首先簡單說一下常用的http協議的特點:http是客戶端/服務器模式中請求-響應所用的協議,在這種模式中,客戶端(一般是web瀏覽器)向服務器提交HTTP請求,服務器響應請求的資源。
HTTP是半雙工協議,也就是說,在同一時刻數據只能是單向流動,客戶端向服務器發送請求(單向的),服務器響應請求(單向的)。
那么如果想要實時通訊,能使服務器實時地將更新的信息傳送到客戶端,而無需客戶端發出請求,目前有以下幾種方式:
1. polling 輪循
輪循:客戶端和服務器之間會一直進行連接,每隔一段時間就詢問一次(setInterval)
特點:連接數會很多,一個接收,一個發送,而且每次發送請求都會消耗流量,也會消耗CPU的利用率 。
(由於http數據包的頭部數據往往很大,通常有400多個字節,但是真正被服務器需要的數據卻很少,有時只有10字節左右,這樣的數據包在網路上周期性的傳輸,難免對網絡帶寬是一種浪費)
代碼實現:
index.html:
<!--輪循 polling -->
<div id="clock"></div>
<script>
let clock = document.getElementById('clock')
setInterval(() => {
let xhr = new XMLHttpRequest;
xhr.open('get','/clock',true)
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
clock.innerText = xhr.responseText
}
}
xhr.send()
}, 1000);
</script>
app.js:node模擬服務器,運行app.js, http://localhost:3000/訪問index.html
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',function(req,res){
res.send(new Date().toLocaleString())
})
app.listen(3000)
可以看到的效果就是,每隔1秒發送一次請求,服務器返回更新后的信息。

2. long-polling 長輪循
長輪循:是對輪循的改良版,客戶端發送請求給服務器之后,需要滿足一些條件才返回新的數據,反之若沒有新數據就一直等待。
當有新消息時才會返回給客戶端,在某種程度上減少了網絡帶寬和CPU利用率的問題。
鏈接會一直保持,直到有數據更新或鏈接超時,此時服務器不能在發送數據。
代碼實現:
index.html:
<body>
<div id="clock"></div>
</body>
<script>
let clock = document.getElementById('clock')
function send(){
let xhr = new XMLHttpRequest;
xhr.open('get','/clock',true)
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
clock.innerText = xhr.responseText
send() // 服務器響應之后,在發送第二次請求
}
}
xhr.send()
}
send()
</script>
app.js:node模擬服務器,運行app.js, http://localhost:3000/訪問index.html
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',function(req,res){
let $timer = setInterval(() => {
let date = new Date()
let seconds = date.getSeconds()
if(seconds % 5 == 0){ // 需要滿足一些條件下,才會進行數據的返回
res.send(date.toLocaleString())
clearInterval($timer) // 清除定時器
}
}, 1000);
})
app.listen(3000)
可以看到的效果就是,服務器每隔5秒更新數據。然后在返回給客戶端,結束一次請求響應,開始第二次的請求響應,而實際應用場景中,服務器的響應時候,會更長一些。

3. iframe 流
在html 頁面嵌入一個隱藏的iframe,將這個iframe的src 屬性設為對一個長鏈接的請求,服務器端就能源源不斷的向客戶端推送數據
代碼實現:
index.html:
<style>
div{
height: 100px;
width:230px;
border: 1px solid slateblue;
line-height: 100px;
text-align: center;
}
</style>
<body>
<!-- iframe 流 -->
<div id="clock"></div>
<iframe src="/clock" frameborder="0"></iframe>
</body>
<script>
let clock = document.getElementById('clock')
function setTime(st) {
clock.innerText = st
}
</script>
app.js:node模擬服務器,運行app.js, http://localhost:3000/訪問index.html
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',function(req,res){
setInterval(() => {
res.write(` // 此處 注意不能用上述案例中的send方法,send方法會默認執行end結束,
<script>
parent.setTime('${new Date().toLocaleString()}')
</script>
`)
}, 1000);
})
app.listen(3000)
可以看到的效果就是,服務器每隔1秒更新數據。並主動推送給客戶端,但是同樣存在問題,就是頁簽上的icon一直處於loading狀態,表示響應一直未結束。用戶體驗並不是很好。

4. EventSource 流
嚴格地說,HTTP 協議無法做到服務器主動推送信息。但是,有一種變通方法,就是服務器向客戶端聲明,接下來要發送的是流信息(streaming)。也就是說,發送的不是一次性的數據包,而是一個數據流,會連續不斷地發送過來。此時客戶端不會關閉連接,會一直等着服務器發過來的新的數據流,視頻播放就是這樣的例子。本質上,這種通信就是以流信息的方式,完成一次用時很長的下載。
H5規范中提供了服務端事件EventSource,瀏覽器創建一個EventSource連接后,便可收到服務端發送的數據,這些數據需要遵守一定的格式,直到服務端或者客戶端關閉該流,所以eventSource也叫做SSE(server-send-event)。
SSE 就是利用這種機制,使用流信息向瀏覽器推送信息。目前除了 IE/Edge,其他瀏覽器都支持,實現方式對客戶端開發人員而言非常簡單,只需在瀏覽器中監聽對應的事件即可。
另外對於服務器,SSE使用的也是HTTP傳輸協議,這意味着我們不需要一個特殊的協議或者額外的實現就可以使用。
其實說白了SSE 是單向通道,只能服務器向瀏覽器發送,因為流信息本質上就是下載。如果瀏覽器向服務器發送信息,就變成了另一次 HTTP 請求。應用場景:在股票行情、新聞推送的這種只需要服務器發送消息給客戶端場景中,顯然使用SSE更加合適。
EventSource的實現同樣分為兩部分:
瀏覽器端:
-
在瀏覽器端創建一個
EventSource實例,向服務器發起連接 -
open:連接一旦建立,就會觸發open事件,可以在onopen屬性定義回調函數 -
message:客戶端收到服務器發來的數據,就會觸發message事件,可以在onmessage屬性定義回調函數。 -
error:如果發生通信錯誤(如連接中斷,服務器返回數據失敗),就會觸發error事件,可以在onerror屬性定義回調函數。 -
close:用於關閉 SSE 連接。source.close(); -
自定義事件:EventSource規范允許服務器端執行自定義事件,客戶端監聽該事件即可,需要使用addEventListener
index.html:
<style>
div{
border: 1px solid #ce4;
width: 300px;
height: 40px;
padding: 20px;
margin-bottom: 20px;
}
p{
color: #888;
font-size: 14px;
}
</style>
<body>
<p>默認事件message:</p>
<div id="clock"></div>
<p>自定義事件 yya:</p>
<div id="yya"></div>
</body>
<script>
let sse = new EventSource('/clock')
let clock = document.getElementById('clock')
let yya = document.getElementById('yya')
// 監聽連接剛打開時被調用
sse.onopen = function () {
console.log('open');
}
// 監聽服務器發過來的信息
sse.onmessage = function (event) {
clock.innerText = event.data
}
// 監聽鏈接請求失敗 關閉流
sse.onerror = function (event) {
console.log('error');
sse.close();
}
// 監聽自定義事件, 不能通過on的方式的去綁定
sse.addEventListener('yya', function (event) {
yya.innerText = event.data
}, false);
</script>
服務器端:
-
事件流的對應MIME格式為
text/event-stream。 -
服務器向瀏覽器發送的 SSE 數據,必須是 UTF-8 編碼的文本
-
服務端返回數據需要特殊的格式,分為
四種消息類型,且消息的每個字段使用"\n"來做分割,-
Event: 事件類型,支持自定義事件 -
Data: 發送的數據內容,如果數據很長,可以分成多行,用\n結尾,最后一行用\n\n結尾。 -
ID: 每一條事件流的ID,相當於每一條數據的編號, -
Retry:指定瀏覽器重新發起連接的時間間隔。在自動重連過程中,之前收到的最后一個ID會被發送到服務端。
-
app.js:node模擬服務器,運行app.js, http://localhost:3000/訪問index.html
let express = require('express')
let app = express()
app.use(express.static(__dirname))
let counter = 0
app.get('/clock',function(req,res){
res.header('Content-Type','text/event-stream')
let $timer = setInterval(() => {
// 第一種寫法
res.write(`id:${counter++}\nevent:message\ndata:${new Date().toLocaleString()}\n\n`)
// 另一種寫法
res.write(`event:yya\n`) // 觸發 自定義事件
res.write(`data:${counter}\n\n`)
}, 1000 );
res.on('close',function(){
counter = 0
clearInterval($timer)
})
})
app.listen(3000)
實現效果:可以看到已經很好的解決了在iframe流中遺留的問題,也就是頁簽一直loading的現象。

5. websocket
WebSocket 是H5下一種新的協議,它誕生於2008年,2011年成為國際標准,現在所有瀏覽器都已支持。它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬,並達到實時通訊的目的,最大特點就是:服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話。
優勢及特點:
-
在客戶端和服務器之間保有一個持有的連接,兩邊可以隨時給對方發送數據,有很強的實時性;
-
屬於應用層協議,基於TCP傳輸協議,並且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器;
-
可以發送文本,也可以支持二進制數據的傳輸;
-
數據格式比較輕量,性能開銷小,通信高效;
-
沒有同源限制,客戶端可以與任意服務器通信;
-
協議標識符是ws(如果加密,則為wss),服務器網址就是 URL;
WebSocket所涉及的內容遠不止於此,這里只是拋磚引玉,帶大家入門,知道有這么個東西,可以干一件什么樣的事兒,要想完全掌握還需要下一番功夫嘞,那么就簡單的用代碼實現一下雙工通訊,並使用序號標識出執行順序:
index.html:
<script>
let socket = new WebSocket('ws://localhost:8888')
socket.onopen = function () {
console.log('1. 客戶端連接上了服務器',new Date().getTime());
socket.send('3. 你好')
}
socket.onmessage = function (e) {
console.log('6',e.data);
}
</script>
app.js:node模擬服務器,運行app.js, http://localhost:3000/訪問index.html
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.listen(3000)
let WebSocket = require('ws')
let wss = new WebSocket.Server({port:8888})
wss.on('connection',function(ws){
console.log('2.服務器監聽到了客戶端的連接',new Date().getTime());
ws.on('message',function(data){
console.log('4.客戶端發來的消息',data);
ws.send('5.服務端說:你也好')
})
})

以上就是我今天要分享的全部內容!
