WebSocket,Server-Sent Events:服務器發送事件,使用長連接進行通信

轉自利用WebSocket和EventSource實現服務端推送

概述

傳統的網頁都是瀏覽器向服務器「查詢」數據,可是不少場合,最有效的方式是服務器向瀏覽器「發送」數據。好比,webpack的HRM,每當收到新的電子郵件,服務器就向瀏覽器發送一個「通知」,這要比瀏覽器按時向服務器查詢(polling)更有效率。html

服務器發送事件(Server-Sent Events,簡稱SSE)就是爲了解決這個問題,而提出的一種新API,部署在EventSource對象上。目前,除了IE,其餘主流瀏覽器都支持。前端

簡單說,所謂SSE,就是瀏覽器向服務器發送一個HTTP請求,而後服務器不斷單向地向瀏覽器推送「信息」(message)。這種信息在格式上很簡單,就是「信息」加上前綴「data: 」,而後以「\n\n」結尾。node

$ curl http://example.com/dates
data: 1394572346452

data: 1394572347457

data: 1394572348463
複製代碼

SSE與WebSocket有類似功能,都是用來創建瀏覽器與服務器之間的通訊渠道。二者的區別在於:webpack

  • WebSocket是全雙工通道,能夠雙向通訊,功能更強;SSE是單向通道,只能服務器向瀏覽器端發送。git

  • WebSocket是一個新的協議,須要服務器端支持;SSE則是部署在HTTP協議之上的,現有的服務器軟件都支持。github

  • SSE是一個輕量級協議,相對簡單;WebSocket是一種較重的協議,相對複雜。web

  • SSE默認支持斷線重連,WebSocket則須要額外部署。ajax

  • SSE支持自定義發送的數據類型。express

從上面的比較能夠看出,二者各有特色,適合不一樣的場合。npm

客戶端代碼

概述

首先,使用下面的代碼,檢測瀏覽器是否支持SSE。

if (!!window.EventSource) {
  // ...
}
複製代碼

而後,部署SSE大概以下。

var source = new EventSource('/dates');

source.onmessage = function(e){
  console.log(e.data);
};

// 或者

source.addEventListener('message', function(e){})
複製代碼

創建鏈接

首先,瀏覽器向服務器發起鏈接,生成一個EventSource的實例對象。

var source = new EventSource(url);
複製代碼

參數url就是服務器網址,必須與當前網頁的網址在同一個網域(domain),並且協議和端口都必須相同。

下面是一個創建鏈接的實例。

if (!!window.EventSource) {
  var source = new EventSource('http://127.0.0.1/sses/');
}
複製代碼

新生成的EventSource實例對象,有一個readyState屬性,代表鏈接所處的狀態。

source.readyState
複製代碼

它能夠取如下值:

0,至關於常量EventSource.CONNECTING,表示鏈接還未創建,或者鏈接斷線。

1,至關於常量EventSource.OPEN,表示鏈接已經創建,能夠接受數據。

2,至關於常量EventSource.CLOSED,表示鏈接已斷,且不會重連。

open事件

鏈接一旦創建,就會觸發open事件,能夠定義相應的回調函數。

source.onopen = function(event) {
  // handle open event
};

// 或者

source.addEventListener("open", function(event) {
  // handle open event
}, false);
複製代碼

message事件

收到數據就會觸發message事件。

source.onmessage = function(event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
};

// 或者

source.addEventListener("message", function(event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
}, false);
複製代碼

參數對象event有以下屬性:

  • data:服務器端傳回的數據(文本格式)。

  • origin: 服務器端URL的域名部分,即協議、域名和端口。

  • lastEventId:數據的編號,由服務器端發送。若是沒有編號,這個屬性爲空。

error事件

若是發生通訊錯誤(好比鏈接中斷),就會觸發error事件。

source.onerror = function(event) {
  // handle error event
};

// 或者

source.addEventListener("error", function(event) {
  // handle error event
}, false);
複製代碼

自定義事件

服務器能夠與瀏覽器約定自定義事件。這種狀況下,發送回來的數據不會觸發message事件。

source.addEventListener("foo", function(event) {
  var data = event.data;
  var origin = event.origin;
  var lastEventId = event.lastEventId;
  // handle message
}, false);
複製代碼

上面代碼表示,瀏覽器對foo事件進行監聽。

close方法

close方法用於關閉鏈接。

source.close();
複製代碼

數據格式

概述

服務器端發送的數據的HTTP頭信息以下:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
複製代碼

後面的行都是以下格式:

field: value\n
複製代碼

field能夠取四個值:「data」, 「event」, 「id」, or 「retry」,也就是說有四類頭信息。每次HTTP通訊能夠包含這四類頭信息中的一類或多類。\n表明換行符。

以冒號開頭的行,表示註釋。一般,服務器每隔一段時間就會向瀏覽器發送一個註釋,保持鏈接不中斷。

: This is a comment
複製代碼

下面是一些例子。

: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n
複製代碼

data:數據欄

數據內容用data表示,能夠佔用一行或多行。若是數據只有一行,則像下面這樣,以「\n\n」結尾。

data:  message\n\n
複製代碼

若是數據有多行,則最後一行用「\n\n」結尾,前面行都用「\n」結尾。

data: begin message\n
data: continue message\n\n
複製代碼

總之,最後一行的data,結尾要用兩個換行符號,表示數據結束。

以發送JSON格式的數據爲例。

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
複製代碼

id:數據標識符

數據標識符用id表示,至關於每一條數據的編號。

id: msg1\n
data: message\n\n
複製代碼

瀏覽器用lastEventId屬性讀取這個值。一旦鏈接斷線,瀏覽器會發送一個HTTP頭,裏面包含一個特殊的「Last-Event-ID」頭信息,將這個值發送回來,用來幫助服務器端重建鏈接。所以,這個頭信息能夠被視爲一種同步機制。

event欄:自定義信息類型

event頭信息表示自定義的數據類型,或者說數據的名字。

event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n
複製代碼

上面的代碼創造了三條信息。第一條是foo,觸發瀏覽器端的foo事件;第二條未取名,表示默認類型,觸發瀏覽器端的message事件;第三條是bar,觸發瀏覽器端的bar事件。

retry:最大間隔時間

瀏覽器默認的是,若是服務器端三秒內沒有發送任何信息,則開始重連。服務器端能夠用retry頭信息,指定通訊的最大間隔時間。

retry: 10000\n
複製代碼

服務器代碼

服務器端發送事件,要求服務器與瀏覽器保持鏈接。對於不一樣的服務器軟件來講,所消耗的資源是不同的。Apache服務器,每一個鏈接就是一個線程,若是要維持大量鏈接,勢必要消耗大量資源。Node.js則是全部鏈接都使用同一個線程,所以消耗的資源會小得多,可是這要求每一個鏈接不能包含很耗時的操做,好比磁盤的IO讀寫。

下面是Node.js的服務器發送事件的代碼實例

var http = require("http");

http.createServer(function (req, res) {

    var fileName = "." + req.url;

    if (fileName === "./stream") {
        res.writeHead(200, {"Content-Type":"text/event-stream", 
                            "Cache-Control":"no-cache", 
                            "Connection":"keep-alive"});
        res.write("retry: 10000\n");
        res.write("event: connecttime\n");
        res.write("data: " + (new Date()) + "\n\n");
        res.write("data: " + (new Date()) + "\n\n");

        interval = setInterval(function() {
            res.write("data: " + (new Date()) + "\n\n");
        }, 1000);

        req.connection.addListener("close", function () {
            clearInterval(interval);
        }, false);
  }
}).listen(80, "127.0.0.1");
複製代碼

可能有不少的同窗有用 setInterval 控制 ajax 不斷向服務端請求最新數據的經歷(輪詢)看下面的代碼:

setInterval(function() {
    $.get('/get/data-list', function(data, status) {
        console.log(data)
    })
}, 5000)
複製代碼複製代碼

這樣每隔5秒前端會向後臺請求一次數據,實現上看起來很簡單可是有個很重要的問題,就是咱們沒辦法控制網速的穩定,不能保證在下次發請求的時候上一次的請求結果已經順利返回,這樣勢必會有隱患,有聰明的同窗立刻會想到用 setTimeout 配合遞歸看下面的代碼:

function poll() {
    setTimeout(function() {
        $.get('/get/data-list', function(data, status) {
            console.log(data)
            poll()
        })
    }, 5000)
}
複製代碼複製代碼

當結果返回以後再延時觸發下一次的請求,這樣雖然沒辦法保證兩次請求之間的間隔時間徹底一致可是至少能夠保證數據返回的節奏是穩定的,看似已經實現了需求可是這麼搞咱們先不去管他的性能就代碼結構也算不上優雅,爲了解決這個問題能夠讓服務端長時間和客戶端保持鏈接進行數據互通h5新增了 WebSocket 和 EventSource 用來實現長輪詢,下面咱們來分析一下這二者的特色以及使用場景。

WebSocket

是什麼: WebSocket是一種通信手段,基於TCP協議,默認端口也是80和443,協議標識符是ws(加密爲wss),它實現了瀏覽器與服務器的全雙工通訊,擴展了瀏覽器與服務端的通訊功能,使服務端也能主動向客戶端發送數據,不受跨域的限制。

有什麼用: WebSocket用來解決http不能持久鏈接的問題,由於能夠雙向通訊因此能夠用來實現聊天室,以及其餘由服務端主動推送的功能例如 實時天氣、股票報價、餘票顯示、消息通知等。

EventSource

是什麼: EventSource的官方名稱應該是 Server-sent events(縮寫SSE)服務端派發事件,EventSource 基於http協議只是簡單的單項通訊,實現了服務端推的過程客戶端沒法經過EventSource向服務端發送數據。喜聞樂見的是ie並無良好的兼容固然也有解決的辦法好比 npm install event-source-polyfill。雖然不能實現雙向通訊可是在功能設計上他也有一些優勢好比能夠自動重鏈接,event IDs,以及發送隨機事件的能力(WebSocket要藉助第三方庫好比socket.io能夠實現重連。)

有什麼用: 由於受單項通訊的限制EventSource只能用來實現像股票報價、新聞推送、實時天氣這些只須要服務器發送消息給客戶端場景中。EventSource的使用更加便捷這也是他的優勢。

WebSocket & EventSource 的區別

  1. WebSocket基於TCP協議,EventSource基於http協議。
  2. EventSource是單向通訊,而websocket是雙向通訊。
  3. EventSource只能發送文本,而websocket支持發送二進制數據。
  4. 在實現上EventSource比websocket更簡單。
  5. EventSource有自動重鏈接(不借助第三方)以及發送隨機事件的能力。
  6. websocket的資源佔用過大EventSource更輕量。
  7. websocket能夠跨域,EventSource基於http跨域須要服務端設置請求頭。

EventSource的實現案例

客戶端代碼

// 實例化 EventSource 參數是服務端監聽的路由
var source = new EventSource('/EventSource-test')
source.onopen = function (event) { // 與服務器鏈接成功回調
  console.log('成功與服務器鏈接')
}
// 監遵從服務器發送來的全部沒有指定事件類型的消息(沒有event字段的消息)
source.onmessage = function (event) { // 監聽未命名事件
  console.log('未命名事件', event.data)
}
source.onerror = function (error) { // 監聽錯誤
  console.log('錯誤')
}
// 監聽指定類型的事件(能夠監聽多個)
source.addEventListener("myEve", function (event) {
  console.log("myEve", event.data)
})
複製代碼複製代碼

服務端代碼(node.js)

const fs = require('fs') const express = require('express') // npm install express const app = express()

// 啓動一個簡易的本地server返回index.html app.get('/', (req, res) => { fs.stat('./index.html', (err, stats) => { if (!err && stats.isFile()) { res.writeHead(200) fs.createReadStream('./index.html').pipe(res) } else { res.writeHead(404) res.end('404 Not Found') } }) })

// 監聽EventSource-test路由服務端返回事件流 app.get('/EventSource-test', (ewq, res) => { // 根據 EventSource 規範設置報頭 res.writeHead(200, { "Content-Type": "text/event-stream", // 規定把報頭設置爲 text/event-stream "Cache-Control": "no-cache" // 設置不對頁面進行緩存 }) // 用write返回事件流,事件流僅僅是一個簡單的文本數據流,每條消息以一個空行(\n)做爲分割。 res.write(':註釋' + '\n\n') // 註釋行 res.write('data:' + '消息內容1' + '\n\n') // 未命名事件

res.write( // 命名事件 'event: myEve' + '\n' + 'data:' + '消息內容2' + '\n' + 'retry:' + '2000' + '\n' + 'id:' + '12345' + '\n\n' )

setInterval(() => { // 定時事件 res.write('data:' + '定時消息' + '\n\n') }, 2000) })

複製代碼// 監聽 6788 app.listen(6788, () => { console.log(server runing on port 6788 ...複製代碼) }) 複製代碼複製代碼server runing on port 6788 ...複製代碼

客戶端訪問 http://127.0.0.1:6788/ 會看到以下的輸出:

來總結一下相關的api,客戶端的api很簡單都在註釋裏了,服務端有一些要注意的地方:

事件流格式?

事件流僅僅是一個簡單的文本數據流,文本應該使用UTF-8格式的編碼。每條消息後面都由一個空行做爲分隔符。以冒號開頭的行爲註釋行,會被忽略。

註釋有何用?

註釋行能夠用來防止鏈接超時,服務器能夠按期發送一條消息註釋行,以保持鏈接不斷。

EventSource規範中規定了那些字段?

event: 事件類型,若是指定了該字段,則在客戶端接收到該條消息時,會在當前的EventSource對象上觸發一個事件,事件類型就是該字段的字段值,你可使用addEventListener()方法在當前EventSource對象上監放任意類型的命名事件,若是該條消息沒有event字段,則會觸發onmessage屬性上的事件處理函數。 data: 消息的數據字段,若是該條消息包含多個data字段,則客戶端會用換行符把它們鏈接成一個字符串來做爲字段值。 id: 事件ID,會成爲當前EventSource對象的內部屬性"最後一個事件ID"的屬性值。 retry: 一個整數值,指定了從新鏈接的時間(單位爲毫秒),若是該字段值不是整數,則會被忽略。

重連是幹什麼的?

上文提過retry字段是用來指定重連時間的,那爲何要重連呢,咱們拿node來講,你們知道node的特色是單線程異步io,單線程就意味着若是server端報錯那麼服務就會停掉,固然在node開發的過程當中會處理這些異常,可是一旦服務停掉了這時就須要用pm2之類的工具去作重啓操做,這時候server雖然正常了,可是客戶端的EventSource連接仍是斷開的這時候就用到了重連。

爲何案例中消息要用\n結尾?

\n是換行的轉義字符,EventSource規範規定每條消息後面都由一個空行做爲分隔符,結尾加一個\n表示一個字段結束,加兩個\n表示一條消息結束。(兩個\n表示換行以後又加了一個空行)

注: 若是一行文本中不包含冒號,則整行文本會被解析成爲字段名,其字段值爲空。



WebSocket的實現案例

WebSocket的客戶端原生api

var ws = new WebSocket('ws://localhost:8080') WebSocket 對象做爲一個構造函數,用於新建 WebSocket 實例。

ws.onopen = function(){} 用於指定鏈接成功後的回調函數。

ws.onclose = function(){} 用於指定鏈接關閉後的回調函數

ws.onmessage = function(){} 用於指定收到服務器數據後的回調函數

ws.send('data') 實例對象的send()方法用於向服務器發送數據

socket.onerror = function(){} 用於指定報錯時的回調函數

服務端的WebSocket如何實現

npm上有不少包對websocket作了實現好比 socket.io、WebSocket-Node、ws、還有不少,本文只對 socket.io以及ws 作簡單的分析,細節還請查看官方文檔。

socket.io和ws有什麼不一樣

Socket.io: Socket.io是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它會自動根據瀏覽器從WebSocket、AJAX長輪詢、Iframe流等等各類方式中選擇最佳的方式來實現網絡實時應用(不支持WebSocket的狀況會降級到AJAX輪詢),很是方便和人性化,兼容性很是好,支持的瀏覽器最低達IE5.5。屏蔽了細節差別和兼容性問題,實現了跨瀏覽器/跨設備進行雙向數據通訊。

ws: 不像 socket.io 模塊, ws 是一個單純的websocket模塊,不提供向上兼容,不須要在客戶端掛額外的js文件。在客戶端不須要使用二次封裝的api使用瀏覽器的原生Websocket API便可通訊。

基於socket.io實現WebSocket雙向通訊

客戶端代碼

<button id="closeSocket">斷開鏈接</button> <button id="openSocket">恢復鏈接</button> <script src="/socket.io/socket.io.js"></script> <script> // 創建鏈接 默認指向 window.location let socket = io('http://127.0.0.1:6788')

openSocket.onclick = () => { socket.open() // 手動打開socket 也能夠從新鏈接 } closeSocket.onclick = () => { socket.close() // 手動關閉客戶端對服務器的連接 }

socket.on('connect', () => { // 鏈接成功 // socket.id是惟一標識,在客戶端鏈接到服務器後被設置。 console.log(socket.id) })

socket.on('connect_error', (error) => { console.log('鏈接錯誤') }) socket.on('disconnect', (timeout) => { console.log('斷開鏈接') }) socket.on('reconnect', (timeout) => { console.log('成功重連') }) socket.on('reconnecting', (timeout) => { console.log('開始重連') }) socket.on('reconnect_error', (timeout) => { console.log('重連錯誤') })

// 監聽服務端返回事件 socket.on('serverEve', (data) => { console.log('serverEve', data) })

let num = 0 setInterval(() => { // 向服務端發送事件 socket.emit('feEve', ++num) }, 1000)

複製代碼複製代碼複製代碼

服務端代碼(node.js)

const app = require('express')()
const server = require('http').Server(app)
const io = require('socket.io')(server, {})

// 啓動一個簡易的本地server返回index.html
app.get('/', (req, res) => {
  res.sendfile(__dirname + '/index.html')
})

// 監聽 6788
server.listen(6788, () => {
  console.log(`server runing on port 6788 ...`)
})

// 服務器監聽全部客戶端 並返回該新鏈接對象
// 每一個客戶端socket鏈接時都會觸發 connection 事件
let num = 0
io.on('connection', (socket) => {

  socket.on('disconnect', (reason) => {
    console.log('斷開鏈接')
  })
  socket.on('error', (error) => {
    console.log('發生錯誤')
  })
  socket.on('disconnecting', (reason) => {
    console.log('客戶端斷開鏈接但還沒有離開')
  })

  console.log(socket.id) // 獲取當前鏈接進入的客戶端的id
  io.clients((error, ids) => {
    console.log(ids)  // 獲取已鏈接的所有客戶機的ID
  })

  // 監聽客戶端發送的事件
  socket.on('feEve', (data) => {
    console.log('feEve', data)
  })
  // 給客戶端發送事件
  setInterval(() => {
    socket.emit('serverEve', ++num)
  }, 1000)
})

/*
  io.close()  // 關閉全部鏈接
*/
複製代碼複製代碼

const io = require('socket.io')(server, {}) 第二個參數是配置項,能夠傳入以下參數:

  • path: '/socket.io' 捕獲路徑的名稱
  • serveClient: false 是否提供客戶端文件
  • pingInterval: 10000 發送消息的時間間隔
  • pingTimeout: 5000 在該時間下沒有數據傳輸鏈接斷開
  • origins: '*' 容許跨域
  • ...

上面基於socket.io的實現中 express 作爲socket通訊的依賴服務基礎 socket.io 做爲socket通訊模塊,實現了雙向數據傳輸。最後,須要注意的是,在服務器端 emit 區分如下三種狀況:

  • socket.emit() :向創建該鏈接的客戶端發送
  • socket.broadcast.emit() :向除去創建該鏈接的客戶端的全部客戶端發送
  • io.sockets.emit() :向全部客戶端發送 等同於上面兩個的和
  • io.to(id).emit() : 向指定id的客戶端發送事件

基於ws實現WebSocket雙向通訊

客戶端代碼

let num = 0
let ws = new WebSocket('ws://127.0.0.1:6788')
ws.onopen = (evt) => {
  console.log('鏈接成功')
  setInterval(() => {
    ws.send(++ num)  // 向服務器發送數據
  }, 1000)
}
ws.onmessage = (evt) => {
  console.log('收到服務端數據', evt.data)
}
ws.onclose = (evt) => {
  console.log('關閉')
}
ws.onerror = (evt) => {
  console.log('錯誤')
}
closeSocket.onclick = () => {
  ws.close()  // 斷開鏈接
}
複製代碼複製代碼

服務端代碼(node.js)

const fs = require('fs') const express = require('express') const app = express()

// 啓動一個簡易的本地server返回index.html const httpServer = app.get('/', (req, res) => { res.writeHead(200) fs.createReadStream('./index.html').pipe(res) }).listen(6788, () => { console.log(server runing on port 6788 ...複製代碼) })

// ws const WebSocketServer = require('ws').Server const wssOptions = {
server: httpServer, // port: 6789, // path: '/test' } const wss = new WebSocketServer(wssOptions, () => { console.log(server runing on port ws 6789 ...複製代碼) })

let num = 1 wss.on('connection', (wsocket) => { console.log('鏈接成功')

wsocket.on('message', (message) => { console.log('收到消息', message) }) wsocket.on('close', (message) => { console.log('斷開了') }) wsocket.on('error', (message) => { console.log('發生錯誤') }) wsocket.on('open', (message) => { console.log('創建鏈接') })

複製代碼server runing on port 6788 ...複製代碼server runing on port ws 6789 ...複製代碼setInterval(() => { wsocket.send( ++num ) }, 1000) }) 複製代碼複製代碼
        上面代碼中在 new WebSocketServer 的時候傳入了 server: httpServer 目的是統一端口,雖然 WebSocketServer 可使用別的端口,可是統一端口仍是更優的選擇,其實express並無直接佔用6788端口而是express調用了內置http模塊建立了http.Server監聽了6788。express只是把響應函數註冊到該http.Server裏面。相似的,WebSocketServer也能夠把本身的響應函數註冊到 http.Server中,這樣同一個端口,根據協議,能夠分別由express和ws處理。咱們拿到express建立的http.Server的引用,再配置到 wssOptions.server 裏讓WebSocketServer根據咱們傳入的http服務來啓動,就實現了統一端口的目的。

        要始終注意,瀏覽器建立WebSocket時發送的仍然是標準的HTTP請求。不管是WebSocket請求,仍是普通HTTP請求,都會被http.Server處理。具體的處理方式則是由express和WebSocketServer注入的回調函數實現的。WebSocketServer會首先判斷請求是否是WS請求,若是是,它將處理該請求,若是不是,該請求仍由express處理。因此,WS請求會直接由WebSocketServer處理,它根本不會通過express。
*案例倉庫:https://github.com/cp0725/YouChat/tree/master/webSocket-eventSource-test* *部分概念參考自 https://www.w3cschool.cn/socket/*
相關文章
相關標籤/搜索