基于socket.io快速實現(xiàn)一個實時通訊應(yīng)用

隨著web技術(shù)的發(fā)展充甚,使用場景和需求也越來越復(fù)雜以政,客戶端不再滿足于簡單的請求得到狀態(tài)的需求。實時通訊越來越多應(yīng)用于各個領(lǐng)域伴找。

HTTP是最常用的客戶端與服務(wù)端的通信技術(shù)盈蛮,但是HTTP通信只能由客戶端發(fā)起,無法及時獲取服務(wù)端的數(shù)據(jù)改變技矮。只能依靠定期輪詢來獲取最新的狀態(tài)抖誉。時效性無法保證,同時更多的請求也會增加服務(wù)器的負(fù)擔(dān)衰倦。

WebSocket技術(shù)應(yīng)運而生袒炉。

WebSocket概念

不同于HTTP半雙工協(xié)議,WebSocket是基于TCP 連接的全雙工協(xié)議樊零,支持客戶端服務(wù)端雙向通信我磁。

WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)驻襟。在 WebSocket API 中夺艰,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接沉衣,并進(jìn)行雙向數(shù)據(jù)傳輸郁副。

在WebSocket API中,瀏覽器和服務(wù)器只需要做一個握手的動作豌习,然后存谎,瀏覽器和服務(wù)器之間就形成了一條快速通道拔疚。兩者之間就直接可以數(shù)據(jù)互相傳送。

實現(xiàn)

原生實現(xiàn)

WebSocket對象一共支持四個消息 onopen, onmessage, onclose和onerror既荚。

建立連接

通過javascript可以快速的建立一個WebSocket連接:

var Socket = new WebSocket(url, [protocol] );

以上代碼中的第一個參數(shù)url, 指定連接的URL草雕。第二個參數(shù)?protocol是可選的,指定了可接受的子協(xié)議固以。

同http協(xié)議使用http://開頭一樣墩虹,WebSocket協(xié)議的URL使用ws://開頭,另外安全的WebSocket協(xié)議使用wss://開頭憨琳。

1.當(dāng)Browser和WebSocketServer連接成功后诫钓,會觸發(fā)onopen消息。

Socket.onopen = function(evt) {};

如果連接失敗篙螟,發(fā)送菌湃、接收數(shù)據(jù)失敗或者處理數(shù)據(jù)出現(xiàn)錯誤,browser會觸發(fā)onerror消息遍略。

Socket.onerror = function(evt) { };

當(dāng)Browser接收到WebSocketServer端發(fā)送的關(guān)閉連接請求時惧所,就會觸發(fā)onclose消息。

Socket.onclose = function(evt) { };

收發(fā)消息

當(dāng)Browser接收到WebSocketServer發(fā)送過來的數(shù)據(jù)時绪杏,就會觸發(fā)onmessage消息下愈,參數(shù)evt中包含server傳輸過來的數(shù)據(jù)。

Socket.onmessage = function(evt) { };

send用于向服務(wù)端發(fā)送消息蕾久。

Socket.send();

socket

WebSocket是跟隨HTML5一同提出的势似,所以在兼容性上存在問題,這時一個非常好用的庫就登場了——Socket.io僧著。

socket.io封裝了websocket履因,同時包含了其它的連接方式,你在任何瀏覽器里都可以使用socket.io來建立異步的連接盹愚。socket.io包含了服務(wù)端和客戶端的庫栅迄,如果在瀏覽器中使用了socket.io的js,服務(wù)端也必須同樣適用皆怕。

socket.io是基于 Websocket 的Client-Server 實時通信庫毅舆。

socket.io底層是基于engine.io這個庫。engine.io為 socket.io 提供跨瀏覽器/跨設(shè)備的雙向通信的底層庫端逼。engine.io使用了 Websocket 和 XHR 方式封裝了一套 socket 協(xié)議朗兵。在低版本的瀏覽器中,不支持Websocket顶滩,為了兼容使用長輪詢(polling)替代。

Socket.io允許你觸發(fā)或響應(yīng)自定義的事件寸爆,除了connect礁鲁,message盐欺,disconnect這些事件的名字不能使用之外,你可以觸發(fā)任何自定義的事件名稱仅醇。

建立連接

const socket = io("ws://0.0.0.0:port"); // port為自己定義的端口號

? ? let io = require("socket.io")(http);

? ? io.on("connection", function(socket) {})

消息收發(fā)

一冗美、發(fā)送數(shù)據(jù)

socket.emit(自定義發(fā)送的字段, data);

二、接收數(shù)據(jù)

socket.on(自定義發(fā)送的字段, function(data) {

? ? ? ? console.log(data);

? ? })

斷開連接

一析二、全部斷開連接

let io = require("socket.io")(http);

? ? io.close();

二粉洼、某個客戶端斷開與服務(wù)端的鏈接

// 客戶端

? ? socket.emit("close", {});

// 服務(wù)端

? ? socket.on("close", data => {

? ? ? ? socket.disconnect(true);

? ? });

room和namespace

有時候websocket有如下的使用場景:1.服務(wù)端發(fā)送的消息有分類,不同的客戶端需要接收的分類不同叶摄;2.服務(wù)端并不需要對所有的客戶端都發(fā)送消息属韧,只需要針對某個特定群體發(fā)送消息;

針對這種使用場景蛤吓,socket中非常實用的namespace和room就上場了宵喂。

先來一張圖看看namespace與room之間的關(guān)系:

namespace

服務(wù)端

io.of("/post").on("connection", function(socket) {

? ? ? ? socket.emit("new message", { mess: `這是post的命名空間` });

? ? });


? ? io.of("/get").on("connection", function(socket) {

? ? ? ? socket.emit("new message", { mess: `這是get的命名空間` });

? ? });

客戶端

// index.js

? ? const socket = io("ws://0.0.0.0:****/post");

? ? socket.on("new message", function(data) {

? ? ? ? console.log('index',data);

? ? }


? ? //message.js

? ? const socket = io("ws://0.0.0.0:****/get");

? ? socket.on("new message", function(data) {

? ? ? ? console.log('message',data);

? ? }

room

客戶端

//可用于客戶端進(jìn)入房間;

? ? socket.join('room one');

? ? //用于離開房間;

? ? socket.leave('room one');

服務(wù)端

io.sockets.on('connection',function(socket){

? ? ? ? //提交者會被排除在外(即不會收到消息)

? ? ? ? socket.broadcast.to('room one').emit('new messages', data);

? ? ? ? // 向所有用戶發(fā)送消息

? ? ? ? io.sockets.to(data).emit("recive message", "hello,房間中的用戶");? ? ?

? ? }

用socket.io實現(xiàn)一個實時接收信息的例子

終于來到應(yīng)用的階段啦,服務(wù)端用node.js模擬了服務(wù)端接口会傲。以下的例子都在本地服務(wù)器中實現(xiàn)锅棕。

服務(wù)端

先來看看服務(wù)端,先來開啟一個服務(wù)淌山,安裝express和socket.io

安裝依賴

npm install --Dev express

? ? npm install --Dev socket.io

構(gòu)建node服務(wù)器

let app = require("express")();

? ? let http = require("http").createServer(handler);

? ? let io = require("socket.io")(http);

? ? let fs = require("fs");


? ? http.listen(port); //port:輸入需要的端口號


? ? function handler(req, res) {

? ? ? fs.readFile(__dirname + "/index.html", function(err, data) {

? ? ? ? if (err) {

? ? ? ? ? res.writeHead(500);

? ? ? ? ? return res.end("Error loading index.html");

? ? ? ? }


? ? ? ? res.writeHead(200);

? ? ? ? res.end(data);

? ? ? });

? ? }


? ? io.on("connection", function(socket) {

? ? ? ? console.log('連接成功');

? ? ? ? //連接成功之后發(fā)送消息

? ? ? ? socket.emit("new message", { mess: `初始消息` });


? ? });

客戶端

核心代碼——index.html(向服務(wù)端發(fā)送數(shù)據(jù))

<div>發(fā)送信息</div>

? ? <input placeholder="請輸入要發(fā)送的信息" />

? ? <button onclick="postMessage()">發(fā)送</button>

// 接收到服務(wù)端傳來的name匹配的消息

? ? socket.on("new message", function(data) {

? ? ? console.log(data);

? ? });


? ? function postMessage() {

? ? ? socket.emit("recive message", {

? ? ? ? message: content,

? ? ? ? time: new Date()

? ? ? });

? ? ? messList.push({

? ? ? ? message: content,

? ? ? ? time: new Date()

? ? ? });

? ? }

核心代碼——message.html(從服務(wù)端接收數(shù)據(jù))

socket.on("new message", function(data) {

? ? ? console.log(data);

? ? });

效果

實時通訊效果


客戶端全部斷開連接



namespace應(yīng)用

加入房間


框架中的應(yīng)用

npm install socket.io-client

const socket = require('socket.io-client')('http://localhost:port');

? ? componentDidMount() {

? ? ? ? socket.on('login', (data) => {

? ? ? ? ? ? console.log(data)

? ? ? ? });

? ? ? ? socket.on('add user', (data) => {

? ? ? ? ? ? console.log(data)

? ? ? ? });

? ? ? ? socket.on('new message', (data) => {

? ? ? ? ? ? console.log(data)

? ? ? ? });

? ? }

分析webSocket協(xié)議

Headers

請求包

Accept-Encoding: gzip, deflate

? ? Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

? ? Cache-Control: no-cache

? ? Connection: Upgrade

? ? Cookie: MEIQIA_VISIT_ID=1IcBRlE1mZhdVi1dEFNtGNAfjyG; token=0b81ffd758ea4a33e7724d9c67efbb26; io=ouI5Vqe7_WnIHlKnAAAG

? ? Host: 0.0.0.0:2699

? ? Origin: http://127.0.0.1:5500

? ? Pragma: no-cache

? ? Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

? ? Sec-WebSocket-Key: PJS0iPLxrL0ueNPoAFUSiA==

? ? Sec-WebSocket-Version: 13

? ? Upgrade: websocket

? ? User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1

請求包說明:

必須是有效的http request 格式裸燎;

HTTP request method 必須是GET,協(xié)議應(yīng)不小于1.1 如: Get / HTTP/1.1泼疑;

必須包括Upgrade頭域顺少,并且其值為“websocket”,用于告訴服務(wù)器此連接需要升級到websocket;

必須包括”Connection” 頭域王浴,并且其值為“Upgrade”;

必須包括”Sec-WebSocket-Key”頭域脆炎,其值采用base64編碼的隨機16字節(jié)長的字符序列;

如果請求來自瀏覽器客戶端,還必須包括Origin頭域 氓辣。 該頭域用于防止未授權(quán)的跨域腳本攻擊秒裕,服務(wù)器可以從Origin決定是否接受該WebSocket連接;

必須包括“Sec-webSocket-Version”頭域,是當(dāng)前使用協(xié)議的版本號钞啸,當(dāng)前值必須是13;

可能包括“Sec-WebSocket-Protocol”几蜻,表示client(應(yīng)用程序)支持的協(xié)議列表,server選擇一個或者沒有可接受的協(xié)議響應(yīng)之;

可能包括“Sec-WebSocket-Extensions”体斩, 協(xié)議擴展梭稚, 某類協(xié)議可能支持多個擴展,通過它可以實現(xiàn)協(xié)議增強;

可能包括任意其他域絮吵,如cookie.

應(yīng)答包

應(yīng)答包說明:

Connection: Upgrade

? ? Sec-WebSocket-Accept: I4jyFwm0r1J8lrnD3yN+EvxTABQ=

? ? Sec-WebSocket-Extensions: permessage-deflate

? ? Upgrade: websocket

必須包括Upgrade頭域弧烤,并且其值為“websocket”;

必須包括Connection頭域,并且其值為“Upgrade”;

必須包括Sec-WebSocket-Accept頭域蹬敲,其值是將請求包“Sec-WebSocket-Key”的值暇昂,與”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″這個字符串進(jìn)行拼接莺戒,然后對拼接后的字符串進(jìn)行sha-1運算,再進(jìn)行base64編碼急波,就是“Sec-WebSocket-Accept”的值从铲;

應(yīng)答包中冒號后面有一個空格;

最后需要兩個空行作為應(yīng)答包結(jié)束澄暮。

請求數(shù)據(jù)

EIO: 3

? ? transport: websocket

? ? sid: 8Uehk2UumXoHVJRzAAAA

EIO:3 表示使用的是engine.io協(xié)議版本3

transport 表示傳輸采用的類型

sid: session id (String)

Frames

WebSocket協(xié)議使用幀(Frame)收發(fā)數(shù)據(jù),在控制臺->Frames中可以查看發(fā)送的幀數(shù)據(jù)名段。

其中幀數(shù)據(jù)前的數(shù)字代表什么意思呢?

這是 Engine.io協(xié)議泣懊,其中的數(shù)字是數(shù)據(jù)包編碼:

<Packet type id> [<data>]

0 open——在打開新傳輸時從服務(wù)器發(fā)送(重新檢查)

1 close——請求關(guān)閉此傳輸伸辟,但不關(guān)閉連接本身。

2 ping——由客戶端發(fā)送嗅定。服務(wù)器應(yīng)該用包含相同數(shù)據(jù)的乓包應(yīng)答

客戶端發(fā)送:2probe探測幀

3 pong——由服務(wù)器發(fā)送以響應(yīng)ping數(shù)據(jù)包自娩。

服務(wù)器發(fā)送:3probe,響應(yīng)客戶端

4 message——實際消息,客戶端和服務(wù)器應(yīng)該使用數(shù)據(jù)調(diào)用它們的回調(diào)渠退。

5 upgrade——在engine.io切換傳輸之前忙迁,它測試,如果服務(wù)器和客戶端可以通過這個傳輸進(jìn)行通信碎乃。如果此測試成功姊扔,客戶端發(fā)送升級數(shù)據(jù)包,請求服務(wù)器刷新其在舊傳輸上的緩存并切換到新傳輸梅誓。

6 noop——noop數(shù)據(jù)包恰梢。主要用于在接收到傳入WebSocket連接時強制輪詢周期。

實例

以上的截圖是上述例子中數(shù)據(jù)傳輸?shù)膶嵗j治鲆幌麓蟾胚^程就是:

connect握手成功

客戶端會發(fā)送2 probe探測幀

服務(wù)端發(fā)送響應(yīng)幀3probe

客戶端會發(fā)送內(nèi)容為5的Upgrade幀

服務(wù)端回應(yīng)內(nèi)容為6的noop幀

探測幀檢查通過后嵌言,客戶端停止輪詢請求,將傳輸通道轉(zhuǎn)到websocket連接及穗,轉(zhuǎn)到websocket后摧茴,接下來就開始定期(默認(rèn)是25秒)的 ping/pong

客戶端、服務(wù)端收發(fā)數(shù)據(jù)埂陆,4表示的是engine.io的message消息苛白,后面跟隨收發(fā)的消息內(nèi)容

為了知道Client和Server鏈接是否正常,項目中使用的ClientSocket和ServerSocket都有一個心跳的線程焚虱,這個線程主要是為了檢測Client和Server是否正常鏈接购裙,Client和Server是否正常鏈接主要是用ping pong流程來保證的。

該心跳定期發(fā)送的間隔是socket.io默認(rèn)設(shè)定的25m鹃栽,在上圖中也可觀察發(fā)現(xiàn)躏率。該間隔可通過配置修改。


如果你覺得我的文章寫的還不錯,可以關(guān)注我或者加我的專欄討論禾锤。

http://www.reibang.com/c/76c4c7b817f8

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末私股,一起剝皮案震驚了整個濱河市摹察,隨后出現(xiàn)的幾起案子恩掷,更是在濱河造成了極大的恐慌,老刑警劉巖供嚎,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黄娘,死亡現(xiàn)場離奇詭異,居然都是意外死亡克滴,警方通過查閱死者的電腦和手機逼争,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劝赔,“玉大人誓焦,你說我怎么就攤上這事∽琶保” “怎么了杂伟?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仍翰。 經(jīng)常有香客問我赫粥,道長,這世上最難降的妖魔是什么予借? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任越平,我火速辦了婚禮,結(jié)果婚禮上灵迫,老公的妹妹穿的比我還像新娘秦叛。我一直安慰自己,他們只是感情好瀑粥,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布挣跋。 她就那樣靜靜地躺著,像睡著了一般利凑。 火紅的嫁衣襯著肌膚如雪浆劲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天哀澈,我揣著相機與錄音牌借,去河邊找鬼。 笑死割按,一個胖子當(dāng)著我的面吹牛膨报,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼现柠,長吁一口氣:“原來是場噩夢啊……” “哼院领!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起够吩,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤比然,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后周循,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體强法,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年湾笛,在試婚紗的時候發(fā)現(xiàn)自己被綠了饮怯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡嚎研,死狀恐怖蓖墅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情临扮,我是刑警寧澤论矾,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站公条,受9級特大地震影響拇囊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜靶橱,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一寥袭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧关霸,春花似錦传黄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佳遣,卻和暖如春识埋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背零渐。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工窒舟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诵盼。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓惠豺,卻偏偏與公主長得像银还,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子洁墙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容