五分鐘技術(shù)趣談 | Web端即時(shí)通信方案知多少?
Part 01
什么是即時(shí)通信?
即時(shí)通信是一個(gè)實(shí)時(shí)通信系統(tǒng),允許兩人或多人使用網(wǎng)絡(luò)實(shí)時(shí)的傳遞文字消息、文件、語(yǔ)音與視頻交流。即時(shí)通信技術(shù)在Native App中通過(guò)TCP、UDP等協(xié)議可以輕松實(shí)現(xiàn),在Native應(yīng)用較為流行。
受HTTP協(xié)議以及Web客戶(hù)端框架限制,想在Web中實(shí)現(xiàn)真正的即時(shí)通信,可謂是技術(shù)上盡腦汁,極盡所能。從傳統(tǒng)的短輪詢(xún)、長(zhǎng)輪詢(xún)到Comet(長(zhǎng)輪詢(xún)的變體)技術(shù),再到HTML5標(biāo)準(zhǔn)發(fā)布之后的WebSocket、 SSE這類(lèi)技術(shù)的橫空出現(xiàn),使Web端即時(shí)通信的技術(shù)方案越來(lái)越多,實(shí)現(xiàn)也越來(lái)越容易。但是對(duì)于技術(shù)人員面對(duì)不同的場(chǎng)景該如何選擇更實(shí)用的技術(shù)方案呢?
Part 02
Web即時(shí)通信實(shí)現(xiàn)方案有哪些?
2.1 輪詢(xún)的原理與實(shí)現(xiàn)
輪詢(xún)分為短輪詢(xún)與長(zhǎng)輪詢(xún)??
短輪詢(xún):是客戶(hù)端定期向服務(wù)器發(fā)起請(qǐng)求查詢(xún)并獲取數(shù)據(jù),無(wú)論是否有數(shù)據(jù)服務(wù)端都立即響應(yīng)客戶(hù)端的請(qǐng)求,該方案較為簡(jiǎn)單不做過(guò)多介紹。
長(zhǎng)輪詢(xún):是比短輪詢(xún)更有效的一種技術(shù),當(dāng)服務(wù)器收到瀏覽器請(qǐng)求后如果有數(shù)據(jù), 服務(wù)器立刻響應(yīng)請(qǐng)求; 如果沒(méi)有數(shù)據(jù)服務(wù)器就會(huì)hold一段時(shí)間,盡可能長(zhǎng)時(shí)間的保持瀏覽器的連接打開(kāi);這段時(shí)間內(nèi)如果有數(shù)據(jù),服務(wù)器立刻響應(yīng)請(qǐng)求; 如果時(shí)間到了還沒(méi)有數(shù)據(jù), 則響應(yīng)http請(qǐng)求;瀏覽器收到http響應(yīng)后,立即再發(fā)送一個(gè)同樣http請(qǐng)求查詢(xún)是否有數(shù)據(jù);如此重復(fù)下去。下圖是長(zhǎng)輪詢(xún)的交互示意圖:
長(zhǎng)輪詢(xún)?yōu)g覽器代碼實(shí)現(xiàn)如下:
/* Client - subscribing to the test events */
subscribe: (callback) => {
const pollUserEvents = () => {
$.ajax({
method: 'GET',
url: 'http://localhost:8080/testEvents',
success: (data) => {
callback(data) // process the data
},
complete: () => {
pollUserEvents();
},
timeout: 30000
})
}
2.2 WebSocket的原理與實(shí)現(xiàn)
在2008年中期,開(kāi)發(fā)人員Michael Carter和Lan Hickson敏銳的感受到Comet在實(shí)現(xiàn)復(fù)雜交互時(shí)帶來(lái)的的苦惱和局限,他們制定了一項(xiàng)計(jì)劃并引入現(xiàn)代實(shí)時(shí)雙向通信的新標(biāo)準(zhǔn),創(chuàng)造了WebSocket,并進(jìn)入了W3C HTML草案標(biāo)準(zhǔn),2011年RFC 6455-WebSocket協(xié)議被發(fā)布到了IETF網(wǎng)站。
WebSocket是一個(gè)構(gòu)建在設(shè)備TCP/IP堆棧上的瘦傳輸層,一個(gè)獨(dú)立的基于TCP的協(xié)議,為Web應(yīng)用程序開(kāi)發(fā)人員提供盡可能接近原始的TCP通信層,WebSocket與HTTP唯一的關(guān)系是它的握手是由HTTP服務(wù)器解釋為一個(gè)Upgrade請(qǐng)求。
WebSocket是一種事件驅(qū)動(dòng)的協(xié)議,這意味著可以將其用于真正的實(shí)時(shí)通信。WebSocket的實(shí)現(xiàn)了一次連接,雙方多次通信的能力。首先由客戶(hù)端發(fā)出WebSocket連接請(qǐng)求,服務(wù)器端進(jìn)行響應(yīng),實(shí)現(xiàn)類(lèi)似TCP握手的動(dòng)作。這個(gè)連接一旦建立起來(lái),在客戶(hù)端和服務(wù)器之間會(huì)一直保持該連接,兩者之間可以直接的進(jìn)行數(shù)據(jù)的互相傳送,并且在連接被關(guān)閉前可以進(jìn)行多次交互。
WebSocket瀏覽器側(cè)代碼實(shí)現(xiàn):
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost/ws');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
WebSocket服務(wù)端代碼實(shí)現(xiàn):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
2.3 SSE的原理與實(shí)現(xiàn)
SSE是Server-Sent Events的簡(jiǎn)稱(chēng),是一種可以主動(dòng)從服務(wù)端推送消息給瀏覽器的技術(shù)。SSE也是使用HTTP協(xié)議進(jìn)行交互,嚴(yán)格地說(shuō)HTTP協(xié)議無(wú)法做到服務(wù)器主動(dòng)推送信息給瀏覽器,都是瀏覽器主動(dòng)去請(qǐng)求服務(wù)器端獲取最新的數(shù)據(jù),SSE技術(shù)使服務(wù)器與瀏覽器之間維護(hù)一個(gè)HTTP長(zhǎng)連接,當(dāng)有新數(shù)據(jù)時(shí),服務(wù)端可以通過(guò)這個(gè)長(zhǎng)連接將數(shù)據(jù)發(fā)送給瀏覽器。
瀏覽器主動(dòng)請(qǐng)求建立一個(gè)格式為text/event-stream的stream流 ,服務(wù)器給瀏覽器發(fā)送的數(shù)據(jù)不是一次性的數(shù)據(jù)包,而是一個(gè)stream流,并且瀏覽器不會(huì)關(guān)閉連接,會(huì)一直等著服務(wù)器發(fā)送新的數(shù)據(jù)流報(bào)文。
客戶(hù)端代碼實(shí)現(xiàn):
var SSENotification = {
source: null,
subscribe: function() {
if ('EventSource' in window) {
this.source = new EventSource('/sse');
this.source.addEventListener('message', function(res) {
const d = res.data;
window.ChatroomDOM.renderData(JSON.parse(d));
});
}
return this.unsubscribe;
},
unsubscribe: function () {
this.source && this.source.close();
}
}
服務(wù)器端代碼實(shí)現(xiàn):
router.get('/sse', function(req, res) {
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
const response = { code: 200, data: { connectors: connectors, messages: messages } };
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("data: " + JSON.stringify(response) + "\n\n");
var unsubscribe = Event.subscribe(function() {
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
const response = { code: 200, data: { connectors: connectors, messages: messages } };
res.write("data: " + JSON.stringify(response) + "\n\n");
});
req.connection.addListener("close", function () {
unsubscribe();
}, false);
});
Part 03
Web即時(shí)通信方案對(duì)比
三種即時(shí)通信技術(shù)方案各有優(yōu)缺點(diǎn),三種技術(shù)方案特性對(duì)比如下:
從對(duì)比可以得出??
長(zhǎng)輪詢(xún):兼容性好,實(shí)現(xiàn)容易;但是由于需要不停向服務(wù)器請(qǐng)求查詢(xún),很多時(shí)候都是無(wú)效報(bào)文,效率低,客戶(hù)端數(shù)量多時(shí)服務(wù)端的壓力較大;適用于掃碼登錄,流程狀態(tài)變化等場(chǎng)景。
WebSocket:支持雙向通信,可以實(shí)現(xiàn)真正的實(shí)時(shí)通信,支持二進(jìn)制數(shù)據(jù)傳輸,基本可以勝任各種即時(shí)通信場(chǎng)景,而且由于WebSocket的首部信息很小,報(bào)文有效載荷高,在海量并發(fā)和客戶(hù)端與服務(wù)器交互負(fù)載流量大的情況下,極大的節(jié)省了網(wǎng)絡(luò)帶寬資源的消耗,有明顯的性能優(yōu)勢(shì);但是WebSocket協(xié)議相對(duì)于HTTP協(xié)議較復(fù)雜,不支持?jǐn)嗑€重連,有一定維護(hù)成本,因此實(shí)際應(yīng)用中需要基于WebSocket開(kāi)源SDK進(jìn)行開(kāi)發(fā);適用于大型聊天App或大型多人在線游戲的場(chǎng)景。
SSE:屬于輕量級(jí)方案,主流瀏覽器都已經(jīng)支持,使用簡(jiǎn)單,默認(rèn)支持?jǐn)嗑€重連,支持自定義事件類(lèi)型;但是SSE長(zhǎng)連接只支持從服務(wù)器向客戶(hù)端發(fā)數(shù)據(jù),并且不支持二進(jìn)制傳輸(需要編碼后傳輸)。適用于服務(wù)器持續(xù)向客戶(hù)端發(fā)送數(shù)據(jù)的場(chǎng)景,如:實(shí)時(shí)股價(jià)流圖、在線音視頻播放、監(jiān)視器客戶(hù)端查看服務(wù)器實(shí)時(shí)信息等。
Part 04
結(jié)束語(yǔ)
輪詢(xún)、WebSocket、SSE三種技術(shù)方案中該如何選擇?在消息推送時(shí)SSE似乎是我們解決問(wèn)題的最終選擇;但是SSE對(duì)于大型游戲、聊天室時(shí)又有很多力不從心,不如WebSocket全面;而面對(duì)掃碼登錄等簡(jiǎn)單場(chǎng)景時(shí)輪詢(xún)似乎更加簡(jiǎn)單便捷。因此Web端即時(shí)通信方案并不能簡(jiǎn)單的說(shuō)古老的技術(shù)會(huì)被新技術(shù)替代,項(xiàng)目實(shí)施中我們要合理選擇,采用更簡(jiǎn)單、更高效的方案來(lái)完成需求。