Websocket

長輪詢與短輪詢

短輪詢

其實(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

WebSocketHTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議溪北。WebSocket使客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)夺脾。在WebSocket API中之拨,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以建立持久性的連接咧叭,并進(jìn)行雙向數(shù)據(jù)傳輸蚀乔。

  1. 通常,應(yīng)用層協(xié)議都是完全基于網(wǎng)絡(luò)層協(xié)議TCP/UDP來實(shí)現(xiàn)菲茬,例如HTTP吉挣、SMTP、POP3婉弹,而Websocket是同時(shí)基于HTTPTCP來實(shí)現(xiàn)睬魂。

    • 先用帶有 Upgrade:Websocket 請求頭的特殊HTTP request來實(shí)現(xiàn)與服務(wù)端握手HandShake
    • 握手成功后镀赌,協(xié)議升級(jí)成Websocket氯哮,進(jìn)行長連接通訊;
    • 整個(gè)過程可理解為:小錘摳縫商佛,大錘搞定喉钢。
  2. 為什么不使用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è)資源想暗。
  3. 為什么不直接使用Socket編程,基于TCP直接保持長連接帘不,實(shí)現(xiàn)即時(shí)通訊说莫?

    • Socket編程針對C/S模式的,而瀏覽器是B/S模式寞焙,瀏覽器無法發(fā)起Socket請求储狭。正因如此, W3C最后還是給出了瀏覽器的Socket -- Websocket捣郊。
  4. 實(shí)際上辽狈,HTTP協(xié)議也是建立在TCP協(xié)議之上的,TCP協(xié)議本身就是全雙工通信呛牲,但HTTP協(xié)議的請求-應(yīng)答機(jī)制限制了全雙工通信刮萌。WebSocket其實(shí)也只是簡單規(guī)定了一下:接下來咱們就不使用HTTP協(xié)議了,直接互相發(fā)數(shù)據(jù)吧娘扩。

HTTPwebSocket其實(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:websocketConnection: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)匿沛。

如此之后,我們建立了一個(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ù)不需要掩碼际起;
  • WebSocketSec-WebSocket-Key 的生成算法是拼接服務(wù)端和客戶端生成的字符串拾碌,進(jìn)行SHA1哈希算法,再用base64編碼街望;
  • WebSocket 協(xié)議握手是依靠 HTTP 協(xié)議的校翔,依靠于 HTTP 響應(yīng)101進(jìn)行協(xié)議升級(jí)轉(zhuǎn)換。

SocketIO

SocketIOWebSocket灾前、AJAX和其它的通信方式全部封裝成了統(tǒng)一的通信接口防症。也就是說,我們在使用SocketIO時(shí)哎甲,不用擔(dān)心兼容問題蔫敲,底層會(huì)自動(dòng)選用最佳的通信方式。所以說 WebSocketSocketIO的一個(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í)也提供了JavaClient實(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)端口也是 80443荔燎,并且握手階段采用 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é)議之上挤牛,如TCPWebSocket。 雖然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é)議藕届,SockJSWebSocket 的備選方案挪蹭,也是底層協(xié)議,而 STOMP 是基于 WebSocket(SockJS)的上層協(xié)議休偶。

WebSocket協(xié)議定義了兩種類型的消息梁厉,文本二進(jìn)制,但它們的內(nèi)容是未定義的踏兜。

如果說SocketC/STCP編程词顾,那么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)容格式睡雇。

  1. 創(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ù)的連接等操作都是一樣的它抱。

  2. 連接服務(wù)端

    client.connect(login,passcode,successCallback,errorCallback);
    
    • loginpasscode都是字符串,相當(dāng)于是用戶的登錄名和密碼憑證朴艰。
    • successCallback观蓄、errorCallback 分別是連接成功、失敗的回調(diào)函數(shù)祠墅。

    還可以這樣連接服務(wù)器:

    const loginForm = { login:'admin', passcode:'666', 'token':'2333' }
    client.connect(loginForm , successCallback, errorCallback);
    
  3. 斷開連接:

    client.disconnect(() => {
       console.log("disconnect")
    })
    
  4. Heart-beating(心跳)
    Heart-beating也就是消息傳送的頻率侮穿,incoming是接收頻率,outgoing是發(fā)送頻率毁嗦,其默認(rèn)值都為10000ms

    // 手動(dòng)設(shè)置
    client.heartbeat.outgoing = 5000; 
    client.heartbeat.incoming = 0;
    
  5. 發(fā)送消息
    客戶端向服務(wù)端發(fā)送消息:send(serverAddr, [options], [message])

    • serverAddr 字符串亲茅,發(fā)送消息的目的地;
    • options 可選對象,包含了額外的頭部信息克锣;
    • message 字符串茵肃,發(fā)送的消息。
訂閱消息
  1. 訂閱消息:客戶端接收服務(wù)端發(fā)送的消息袭祟;
    subscribe(serverAddr, callback, [options])

    • serverAddr 字符串验残,接收消息的目的地;
    • callback 回調(diào)函數(shù)巾乳,接收消息胚膊;
    • options 可選對象,包含額外的頭部信息想鹰。
  2. 客戶端可以訂閱廣播

    client.subscribe('/topic/msg',function(messages){
        console.log(messages);
    })
    
  3. 一對一消息的接收

    // 第一種方式
    const uId = 888;
    client.subscribe(`/user/${uId}/msg`, msg => {
        console.log(msg);
    })
    // 第二種方式
    client.subscribe('/msg', msg => {
        console.log(messages);
    }, {userId : uId  })
    

    客戶端采用的寫法要根據(jù)服務(wù)端代碼來做選擇紊婉。

  4. 取消訂閱:unsubscribe()

    constsub =  client.subscribe;
    sub .unsubscribe();
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辑舷,隨后出現(xiàn)的幾起案子喻犁,更是在濱河造成了極大的恐慌,老刑警劉巖何缓,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肢础,死亡現(xiàn)場離奇詭異,居然都是意外死亡碌廓,警方通過查閱死者的電腦和手機(jī)传轰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谷婆,“玉大人慨蛙,你說我怎么就攤上這事〖涂妫” “怎么了期贫?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長异袄。 經(jīng)常有香客問我通砍,道長,這世上最難降的妖魔是什么烤蜕? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任封孙,我火速辦了婚禮,結(jié)果婚禮上讽营,老公的妹妹穿的比我還像新娘虎忌。我一直安慰自己,他們只是感情好斑匪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布呐籽。 她就那樣靜靜地躺著,像睡著了一般蚀瘸。 火紅的嫁衣襯著肌膚如雪狡蝶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天贮勃,我揣著相機(jī)與錄音贪惹,去河邊找鬼。 笑死寂嘉,一個(gè)胖子當(dāng)著我的面吹牛奏瞬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泉孩,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼硼端,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寓搬?” 一聲冷哼從身側(cè)響起珍昨,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎句喷,沒想到半個(gè)月后镣典,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唾琼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年兄春,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锡溯。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赶舆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祭饭,到底是詐尸還是另有隱情涌乳,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布甜癞,位于F島的核電站夕晓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏悠咱。R本人自食惡果不足惜蒸辆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望析既。 院中可真熱鬧躬贡,春花似錦、人聲如沸眼坏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至檐蚜,卻和暖如春魄懂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闯第。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工市栗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咳短。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓填帽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親咙好。 傳聞我的和親對象是個(gè)殘疾皇子篡腌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344