長輪詢與短輪詢
短輪詢
其實(shí)就是普通的輪詢,在特定的時(shí)間間隔內(nèi)宪萄,由瀏覽器向服務(wù)器發(fā)出HTTP請求谱净,然后服務(wù)器返回最新的數(shù)據(jù)給客戶端。
var xhr = new XMLHttpRequest();
setInterval(function() {
xhr.open('GET','/user');
xhr.onreadystatechange = function() {
// your code ...
};
xhr.send();
}, 1000);
缺點(diǎn): 請求中有大半是無用的噪舀,浪費(fèi)帶寬和服務(wù)器資源魁淳;因?yàn)槭钱惒秸埱螅憫?yīng)的結(jié)果沒有順序傅联。
實(shí)例: 適用于小型應(yīng)用。
長輪詢
客戶端向服務(wù)器發(fā)送HTTP請求疚察,但服務(wù)端不立即返回響應(yīng)蒸走,而是hold住連接,直到有新消息才返回響應(yīng)信息并關(guān)閉連接貌嫡,客戶端處理完響應(yīng)信息后才能向服務(wù)器發(fā)送新的請求比驻。
function ajax(){
var xhr = new XMLHttpRequest();
xhr.open('GET','/user');
xhr.onreadystatechange = function(){
ajax();
};
xhr.send();
}
優(yōu)點(diǎn):在無消息的情況下不會(huì)頻繁的請求,耗費(fèi)資源小
缺點(diǎn):服務(wù)器hold連接會(huì)消耗資源
實(shí)例:WebQQ岛抄、Hi網(wǎng)頁版别惦、Facebook IM
長連接與短連接
HTTP協(xié)議是基于請求/響應(yīng)模式的,因此只要服務(wù)端給了響應(yīng)夫椭,本次HTTP連接(請求)就結(jié)束了。也就是說,HTTP本身根本沒有長連接與短連接這一說瓤漏。
所謂的長連接與短連接此叠,其實(shí)指的是TCP連接,TCP是一個(gè)雙向通道仁讨,可以保持一段時(shí)間不關(guān)閉羽莺,因此TCP連接才有真正的長連接和短連接這一說。
只要客戶端和服務(wù)端的頭部都設(shè)置了connection: keep-alive
洞豁,就是啟用了長連接盐固,為了通道的復(fù)用荒给。HTTP/1.1默認(rèn)啟用長連接。
注意: 長連接是為了通道復(fù)用刁卜,并不意味著永久連接志电,在一段時(shí)間內(nèi)沒有HTTP請求的話,這個(gè)連接就會(huì)被關(guān)閉长酗。
WebSocket
WebSocket
是HTML5
開始提供的一種在單個(gè)TCP
連接上進(jìn)行全雙工通訊的協(xié)議溪北。WebSocket
使客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)夺脾。在WebSocket API
中之拨,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以建立持久性的連接咧叭,并進(jìn)行雙向數(shù)據(jù)傳輸蚀乔。
-
通常,應(yīng)用層協(xié)議都是完全基于網(wǎng)絡(luò)層協(xié)議
TCP/UDP
來實(shí)現(xiàn)菲茬,例如HTTP吉挣、SMTP、POP3
婉弹,而Websocket
是同時(shí)基于HTTP
與TCP
來實(shí)現(xiàn)睬魂。- 先用帶有
Upgrade:Websocket
請求頭的特殊HTTP request
來實(shí)現(xiàn)與服務(wù)端握手HandShake
; - 握手成功后镀赌,協(xié)議升級(jí)成
Websocket
氯哮,進(jìn)行長連接通訊; - 整個(gè)過程可理解為:小錘摳縫商佛,大錘搞定喉钢。
- 先用帶有
-
為什么不使用
HTTP
長連接來實(shí)現(xiàn)即時(shí)通訊?事實(shí)上良姆,在Websocket
之前就是使用HTTP
長連接這種方式肠虽,如Comet
。但是它有如下弊端:-
HTTP 1.1
規(guī)范中規(guī)定玛追,客戶端不應(yīng)該與服務(wù)器端建立超過兩個(gè)的HTTP
連接税课, 新的連接會(huì)被阻塞; - 對于服務(wù)端來說痊剖,每個(gè)長連接都占有一個(gè)用戶線程伯复,在
NIO
或者異步編程之前,服務(wù)端開銷太大邢笙; -
HTTP
不能完成服務(wù)端推送啸如,新出的HTTP/2
只能推送靜態(tài)資源,無法推送即時(shí)消息氮惯。 -
HTTP/2
所謂的server push
其實(shí)是當(dāng)服務(wù)器接收一個(gè)請求時(shí)叮雳,可以響應(yīng)多個(gè)資源想暗。
-
-
為什么不直接使用
Socket
編程,基于TCP
直接保持長連接帘不,實(shí)現(xiàn)即時(shí)通訊说莫?-
Socket
編程針對C/S
模式的,而瀏覽器是B/S
模式寞焙,瀏覽器無法發(fā)起Socket
請求储狭。正因如此,W3C
最后還是給出了瀏覽器的Socket -- Websocket
捣郊。
-
實(shí)際上辽狈,
HTTP
協(xié)議也是建立在TCP
協(xié)議之上的,TCP
協(xié)議本身就是全雙工通信呛牲,但HTTP
協(xié)議的請求-
應(yīng)答機(jī)制限制了全雙工通信刮萌。WebSocket
其實(shí)也只是簡單規(guī)定了一下:接下來咱們就不使用HTTP
協(xié)議了,直接互相發(fā)數(shù)據(jù)吧娘扩。
HTTP
和webSocket
其實(shí)是個(gè)交集着茸,他們都是建立在TCP
鏈接之上的。
使用方式
對于瀏覽器端琐旁,WebSocket API
的使用非常簡單涮阔。
// 首先new一個(gè)websocket對象,發(fā)起建立連接的請求
var ws = new WebSocket("wss://webchat-bj-test5.clink.cn");
// 連接成功后的回調(diào)函數(shù)
ws.onopen = function(evt) {
console.log("Connection open ...");
// 向服務(wù)器發(fā)送數(shù)據(jù)
ws.send("Hello WebSockets!");
};
// 接收服務(wù)器數(shù)據(jù)后的回調(diào)函數(shù)
ws.onmessage = function(evt) {
console.log( "Received: ", evt.data);
// ws.close(); // 主動(dòng)關(guān)閉websocket連接
};
// 服務(wù)器連接關(guān)閉后的回調(diào)函數(shù)
ws.onclose = function(evt) {
console.log("Connection closed.");
};
- 首先灰殴,
WebSocket
連接必須由瀏覽器發(fā)起敬特,因?yàn)檎埱髤f(xié)議是一個(gè)標(biāo)準(zhǔn)的HTTP
請求,格式如下:GET wss://webchat-bj-test5.clink.cn&province= HTTP/1.1 //請求地址 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: O5GLCYKZVQi2jTLENobvtg== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits // ...
-
GET
請求的地址使用ws/wss
協(xié)議验懊,也是webSocket
使用的協(xié)議擅羞; -
Upgrade:websocket
和Connection:Upgrade
表示將這個(gè)連接升級(jí)為websocket
連接尸变; -
Sec-WebSocket-Key
是一個(gè)Base64 encode
的值义图,由瀏覽器隨機(jī)生成,用于標(biāo)識(shí)這個(gè)連接召烂,與服務(wù)器做身份驗(yàn)證碱工; -
Sec-WebSocket-Version
告訴服務(wù)器所使用的Websocke
協(xié)議版本。
-
- 隨后奏夫,如果服務(wù)器接受該請求怕篷,則返回響應(yīng):
HTTP/1.1 101 // 狀態(tài)碼101 Server: nginx/1.13.9 // 服務(wù)器 Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: uZpmP+PDDvSeKsEg9vkAsWcqPzE= Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15 // ...
- 響應(yīng)碼
101
表示本次連接的HTTP
協(xié)議將被更改,更改后的協(xié)議就是Upgrade:websocket
酗昼; -
Sec-WebSocket-Accept
經(jīng)過服務(wù)器確認(rèn)廊谓、并且加密過后的Sec-WebSocket-Key
; -
Sec-WebSocket-Extensions
WebSocket
的擴(kuò)展麻削; -
Sec-WebSocket-Protocol
數(shù)據(jù)交換協(xié)議蒸痹,列出的客戶端請求的子協(xié)議春弥。如果指定了這個(gè)字段,服務(wù)器需要包含相同的字段叠荠,并且按照優(yōu)先順序從子協(xié)議中選擇一個(gè)值作為建立連接的響應(yīng)匿沛。
- 響應(yīng)碼
如此之后,我們建立了一個(gè)websocket
鏈接榛鼎。這個(gè)過程通常稱為握手逃呼。瀏覽器和服務(wù)器隨時(shí)可以主動(dòng)發(fā)送消息給對方。消息有兩種:文本 和 二進(jìn)制數(shù)據(jù)
-
WebSocket
是為了在web
應(yīng)用上進(jìn)行全雙工通信而產(chǎn)生的協(xié)議者娱,相比于輪詢HTTP
請求的方式抡笼,WebSocket
有節(jié)省服務(wù)器資源,效率高等優(yōu)點(diǎn)肺然; -
WebSocket
中的掩碼是為了防止早期版本中存在中間緩存污染攻擊等問題而設(shè)置的蔫缸,客戶端向服務(wù)端發(fā)送數(shù)據(jù)需要掩碼,而服務(wù)端向客戶端發(fā)送數(shù)據(jù)不需要掩碼际起; -
WebSocket
中Sec-WebSocket-Key
的生成算法是拼接服務(wù)端和客戶端生成的字符串拾碌,進(jìn)行SHA1
哈希算法,再用base64
編碼街望; -
WebSocket
協(xié)議握手是依靠HTTP
協(xié)議的校翔,依靠于HTTP
響應(yīng)101
進(jìn)行協(xié)議升級(jí)轉(zhuǎn)換。
SocketIO
SocketIO
將WebSocket灾前、AJAX
和其它的通信方式全部封裝成了統(tǒng)一的通信接口防症。也就是說,我們在使用SocketIO
時(shí)哎甲,不用擔(dān)心兼容問題蔫敲,底層會(huì)自動(dòng)選用最佳的通信方式。所以說WebSocket
是SocketIO
的一個(gè)子集炭玫。
Websocket API
并不是所有瀏覽器都完美支持奈嘿,而當(dāng)瀏覽器不支持Websocket
時(shí),應(yīng)該自動(dòng)切換成Ajax
長輪詢吞加,SSE
等備用解決方案裙犹。所以在實(shí)際開發(fā)中我們通常采用封裝了Websocket
及其備用方案的庫----SockJS(Java) / Socket.IO(Node)
。
- 如果使用
Java
做服務(wù)端衔憨,同時(shí)又恰好使用Spring
作為框架叶圃,那么推薦使用SockJS
,因?yàn)?code>Spring本身就是SockJS
推薦的Java Server
實(shí)現(xiàn)践图,同時(shí)也提供了Java
的Client
實(shí)現(xiàn)掺冠。 - 如果使用·Node.js·做服務(wù)端,那么毫無疑問選擇
Socket.IO
码党,它本省就是從Node.js
開始的德崭,當(dāng)然服務(wù)端也提供了engine.io-server-java
實(shí)現(xiàn)悍及。甚至可以使用netty-socketio
。
注意:不管你使用哪一種接癌,都必須保證客戶端與服務(wù)端同時(shí)支持心赶。
# node端
// 引入koa
var app = require('koa')();
//創(chuàng)建http服務(wù)
var server = require('http').createServer(app.callback());
//給http封裝成io對象
var io = require('socket.io')(server);
// 建立鏈接
io.on('connection', function(socket){
// io.emit代表廣播,socket.emit代表私發(fā)
socket.on('eventB', function(socket){ /* */ });
socket.emit('eventA', /* */);
});
server.listen(3000);
# 前端
<script src="./lib/socket.io.js"></script>
<script>
//創(chuàng)建個(gè)服務(wù)
var socket = new io()
// 用 on 監(jiān)聽
socket.on('eventA', function (res) {
console.log('?戶1接收到信息了了')
})
socket.emit('eventB', data)
</script>
websocket的特點(diǎn)
- 建立在
TCP
協(xié)議之上缺猛,服務(wù)器端的實(shí)現(xiàn)比較容易缨叫; - 與
HTTP
協(xié)議有著良好的兼容性。默認(rèn)端口也是80
和443
荔燎,并且握手階段采用HTTP
協(xié)議耻姥,因此握手時(shí)不容易屏蔽,能通過各種HTTP
代理服務(wù)器有咨; - 數(shù)據(jù)格式比較輕量琐簇,性能開銷小,通信高效座享;
- 可以發(fā)送文本婉商,也可以發(fā)送二進(jìn)制數(shù)據(jù);
- 沒有同源限制渣叛,客戶端可以與任意服務(wù)器通信丈秩。
SSE
所謂
SSE(Sever-Sent Event)
,就是瀏覽器向服務(wù)器發(fā)送一個(gè)HTTP
請求淳衙,保持長連接蘑秽,服務(wù)器不斷單向地向?yàn)g覽器推送消息,這么做是為了節(jié)約網(wǎng)絡(luò)資源箫攀,不用一直發(fā)請求肠牲,建立新連接。
它其實(shí)類似長輪詢靴跛,有一個(gè)瀏覽器內(nèi)置EventSource
對象來操作
//進(jìn)建立鏈接
var source = new EventSource();
//關(guān)閉鏈接
source.close();
缺點(diǎn):無法實(shí)現(xiàn)雙向消息缀雳。
StompJS
在探討StompJS
之前,讓我們先了解一下STOMP -- Simple (or Streaming) Text Orientated Messaging Protocol
汤求,一個(gè)面向消息/流的簡單文本協(xié)議俏险。它提供了一個(gè)可互操作的連接格式严拒,允許STOMP
客戶端與任意STOMP
消息代理(Broker
)進(jìn)行交互扬绪。從而為多語言、多平臺(tái)和Brokers
集群提供簡單且普遍的消息協(xié)作裤唠。
STOMP
可用于任何可靠的雙向流網(wǎng)絡(luò)協(xié)議之上挤牛,如TCP
和WebSocket
。 雖然STOMP
是面向文本的協(xié)議种蘸,但消息有效負(fù)載可以是文本或二進(jìn)制墓赴。
STOMP
是一種基于幀的協(xié)議竞膳,幀的結(jié)構(gòu)是效仿HTTP報(bào)文格式,如下:
COMMAND
header1:value1
header2:value2
Body^@
WebSocket
實(shí)現(xiàn)客戶端看起來比較簡單诫硕,但是需要與后臺(tái)進(jìn)行很好的配合和調(diào)試才能達(dá)到最佳效果坦辟。通過SockJS/SocketIO 、Stomp
來進(jìn)行瀏覽器兼容章办,可以增加消息語義和可用性锉走。簡而言之,WebSocket
是底層協(xié)議藕届,SockJS
是WebSocket
的備選方案挪蹭,也是底層協(xié)議,而 STOMP
是基于 WebSocket(SockJS)
的上層協(xié)議休偶。
WebSocket
協(xié)議定義了兩種類型的消息梁厉,文本和二進(jìn)制,但它們的內(nèi)容是未定義的踏兜。
如果說
Socket
是C/S
的TCP
編程词顾,那么Websocket
就是Web(B/S)
的TCP
編程,所以需要在客戶端與服務(wù)端之間定義一個(gè)機(jī)制去協(xié)商一個(gè)子協(xié)議(更高級(jí)別的消息協(xié)議)碱妆,將它使用在Websocket
之上去定義每次發(fā)送消息的類別计技、格式和內(nèi)容等等。
子協(xié)議的使用是可選的山橄,但無論哪種方式垮媒,客戶端和服務(wù)器都需要就一些定義消息內(nèi)容的協(xié)議達(dá)成一致。
于是航棱,通常選擇在Websocket
協(xié)議上使用STOMP
協(xié)議來定義內(nèi)容格式睡雇。
-
創(chuàng)建
STOMP
客戶端
在web
瀏覽器中,可以通過兩種方式進(jìn)行客戶端的創(chuàng)建- 使用普通的
WebSocket
let url = "ws://localhost:61614/stomp"; let client = Stomp.client(url);
- 使用定制的
WebSocket
let url = "ws://localhost:61614/stomp"; let socket = new SockJS(url); let client = Stomp.over(socket);
雖然客戶端的創(chuàng)建方式不同饮醇,但后續(xù)的連接等操作都是一樣的它抱。
- 使用普通的
-
連接服務(wù)端
client.connect(login,passcode,successCallback,errorCallback);
-
login
和passcode
都是字符串,相當(dāng)于是用戶的登錄名和密碼憑證朴艰。 -
successCallback观蓄、errorCallback
分別是連接成功、失敗的回調(diào)函數(shù)祠墅。
還可以這樣連接服務(wù)器:
const loginForm = { login:'admin', passcode:'666', 'token':'2333' } client.connect(loginForm , successCallback, errorCallback);
-
-
斷開連接:
client.disconnect(() => { console.log("disconnect") })
-
Heart-beating(心跳)
Heart-beating
也就是消息傳送的頻率侮穿,incoming
是接收頻率,outgoing
是發(fā)送頻率毁嗦,其默認(rèn)值都為10000ms
// 手動(dòng)設(shè)置 client.heartbeat.outgoing = 5000; client.heartbeat.incoming = 0;
-
發(fā)送消息
客戶端向服務(wù)端發(fā)送消息:send(serverAddr, [options], [message])
-
serverAddr
字符串亲茅,發(fā)送消息的目的地; -
options
可選對象,包含了額外的頭部信息克锣; -
message
字符串茵肃,發(fā)送的消息。
-
訂閱消息
-
訂閱消息:客戶端接收服務(wù)端發(fā)送的消息袭祟;
subscribe(serverAddr, callback, [options])
-
serverAddr
字符串验残,接收消息的目的地; -
callback
回調(diào)函數(shù)巾乳,接收消息胚膊; -
options
可選對象,包含額外的頭部信息想鹰。
-
-
客戶端可以訂閱廣播
client.subscribe('/topic/msg',function(messages){ console.log(messages); })
-
一對一消息的接收
// 第一種方式 const uId = 888; client.subscribe(`/user/${uId}/msg`, msg => { console.log(msg); }) // 第二種方式 client.subscribe('/msg', msg => { console.log(messages); }, {userId : uId })
客戶端采用的寫法要根據(jù)服務(wù)端代碼來做選擇紊婉。
-
取消訂閱:
unsubscribe()
constsub = client.subscribe; sub .unsubscribe();