WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協(xié)議。
在 WebSocket API 中,瀏覽器和服務器只需要做一個握手的動作,然后,瀏覽器和服務器之間就形成了一條快速通道。兩者之間就直接可以數(shù)據(jù)互相傳送汁蝶。
傳統(tǒng)的HTTP協(xié)議是一個請求-響應協(xié)議,瀏覽器不主動請求论悴,服務器是沒法主動發(fā)數(shù)據(jù)給瀏覽器的掖棉。
傳統(tǒng)服務器推送方式
Ajax 輪詢
瀏覽器通過JavaScript啟動一個定時器,然后以固定的間隔給服務器發(fā)請求膀估,詢問服務器有沒有新消息幔亥。
缺點
- 實時性不夠
- 頻繁的請求會給服務器帶來極大的壓力。
服務器反推
本質(zhì)上也是輪詢察纯,但是在沒有消息的情況下帕棉,服務器先拖一段時間,等到有消息了再回復饼记。暫時地解決了實時性問題香伴。
缺點
- 以多線程模式運行的服務器會讓大部分線程大部分時間都處于掛起狀態(tài),極大地浪費服務器資源具则。
- 一個HTTP連接在長時間沒有數(shù)據(jù)傳輸?shù)那闆r下即纲,鏈路上的任何一個網(wǎng)關都可能關閉這個連接。 長期占用連接博肋,喪失了無狀態(tài)高并發(fā)的特點低斋。
WebSocket協(xié)議
WebSocket并不是全新的協(xié)議蜂厅,而是利用了HTTP協(xié)議來建立TCP連接。
請求
WebSocket連接必須由瀏覽器發(fā)起拔稳,因為請求協(xié)議是一個標準的HTTP請求葛峻。
格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
WebSocket請求和普通的HTTP請求有幾點不同:
- GET請求的地址不是類似/path/锹雏,而是以ws://開頭的地址巴比;
- 請求頭Upgrade: websocket和Connection: Upgrade表示這個連接將要被轉(zhuǎn)換為WebSocket連接;
- Sec-WebSocket-Key是用于標識這個連接礁遵,并非用于加密數(shù)據(jù)轻绞;
- Sec-WebSocket-Version指定了WebSocket的協(xié)議版本。
響應
服務器如果接受該請求佣耐,就會返回如下響應:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
響應代碼101表示本次連接的HTTP協(xié)議即將被更改政勃,更改后的協(xié)議就是Upgrade: websocket指定的WebSocket協(xié)議
WebSocket、HTTP 與 TCP 區(qū)別
HTTP兼砖、WebSocket 等應用層協(xié)議奸远,都是基于 TCP 協(xié)議來傳輸數(shù)據(jù)的。 所以連接和斷開讽挟,都要遵循 TCP 協(xié)議中的三次握手和四次握手 懒叛,只是在連接之后發(fā)送的內(nèi)容不同,或者是斷開的時間不同耽梅。
對于 WebSocket
來說薛窥,它必須依賴 HTTP 協(xié)議進行一次握手
,握手成功后眼姐,數(shù)據(jù)就直接從 TCP 通道傳輸诅迷,與 HTTP 無關了。
Socket 與 WebScoket
Socket是應用層與TCP/IP協(xié)議族通信的中間軟件抽象層众旗,它是一組接口罢杉。在設計模式中,Socket其實就是一個門面模式贡歧,它把復雜的TCP/IP協(xié)議族隱藏在Socket接口后面滩租,對用戶來說,一組簡單的接口就是全部艘款,讓Socket去組織數(shù)據(jù)持际,以符合指定的協(xié)議。
主機 A 的應用程序要能和主機 B 的應用程序通信哗咆,必須通過 Socket 建立連接蜘欲,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來建立 TCP 連接。建立 TCP 連接需要底層 IP 協(xié)議來尋址網(wǎng)絡中的主機晌柬。我們知道網(wǎng)絡層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來找到目標主機姥份,但是一臺主機上可能運行著多個應用程序郭脂,如何才能與指定的應用程序通信就要通過 TCP 或 UPD 的地址也就是端口號來指定。這樣就可以通過一個 Socket 實例唯一代表一個主機上的一個應用程序的通信鏈路了澈歉。
WebSocket 則不同展鸡,它是一個完整的 應用層協(xié)議,包含一套標準的 API埃难。
從使用上來說莹弊,WebSocket 更易用,而 Socket 更靈活涡尘。
HTML5 與 WebSocket
WebSocket API 是 HTML5 標準的一部分忍弛, 但這并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于瀏覽器的應用程序中使用考抄。
注意事項
長連接應用必須加心跳细疚,否則連接可能由于長時間未通訊被路由節(jié)點強行斷開。
消息堆積
心跳作用主要有兩個
1川梅、客戶端定時給服務端發(fā)送點數(shù)據(jù)疯兼,防止連接由于長時間沒有通訊而被某些節(jié)點的防火墻關閉導致連接斷開的情況。
2贫途、服務端可以通過心跳來判斷客戶端是否在線吧彪,如果客戶端在規(guī)定時間內(nèi)沒有發(fā)來任何數(shù)據(jù),就認為客戶端下線潮饱。這樣可以檢測到客戶端由于極端情況(斷電来氧、斷網(wǎng)等)下線的事件。
心跳間隔建議值:
建議客戶端發(fā)送心跳間隔小于60秒香拉,比如55秒啦扬。
HTML5 WebSocket
WebSocket 屬性
Socket.readyState
只讀屬性 readyState 表示連接狀態(tài),可以是以下值:
0 - 表示連接尚未建立凫碌。
1 - 表示連接已建立扑毡,可以進行通信。
2 - 表示連接正在進行關閉盛险。
3 - 表示連接已經(jīng)關閉或者連接不能打開瞄摊。
Socket.bufferedAmount
只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,但是還沒有發(fā)出的 UTF-8 文本字節(jié)數(shù)苦掘。
WebSocket 事件
假定我們使用了以上代碼創(chuàng)建了 Socket 對象:
Socket.onopen 連接建立時觸發(fā)
Socket.onmessage 客戶端接收服務端數(shù)據(jù)時觸發(fā)
Socket.onerror 通信發(fā)生錯誤時觸發(fā)
Socket.onclose 連接關閉時觸發(fā)
WebSocket 方法
假定我們使用了以上代碼創(chuàng)建了 Socket 對象:
Socket.send() 使用連接發(fā)送數(shù)據(jù)
Socket.close() 關閉連接
參考:
WebSocket
注意是事項
注意:長連接應用必須加心跳换帜,否則連接可能由于長時間未通訊被路由節(jié)點強行斷開。
心跳作用主要有兩個:
1鹤啡、客戶端定時給服務端發(fā)送點數(shù)據(jù)惯驼,防止連接由于長時間沒有通訊而被某些節(jié)點的防火墻關閉導致連接斷開的情況。
2、服務端可以通過心跳來判斷客戶端是否在線祟牲,如果客戶端在規(guī)定時間內(nèi)沒有發(fā)來任何數(shù)據(jù)隙畜,就認為客戶端下線。這樣可以檢測到客戶端由于極端情況(斷電说贝、斷網(wǎng)等)下線的事件议惰。
心跳間隔建議值:
建議客戶端發(fā)送心跳間隔小于60秒,比如55秒乡恕。
代碼演示
服務端代碼:
這里采用php方式來進行演示言询,其他語言也是類似,這里不在敘述几颜。
<?php
use Workerman\Worker;
require_once __DIR__ . '/../../Workerman/Autoloader.php';
use \Workerman\Lib\Timer;
// 創(chuàng)建一個Worker監(jiān)聽2000端口倍试,使用websocket協(xié)議通訊
$ws_worker = new Worker("websocket://0.0.0.0:2000");
// 進程數(shù)設置為1讯屈,采用單進程
$ws_worker->count = 1;
// 保存uid到connection的映射(uid是用戶id或者客戶端唯一標識)
$ws_worker->uidConnections = [];
// 設置心跳時間 0 代表服務器主動钡翱蓿活
define('HEARTBEAT_TIME', 10);
// 進程啟動后設置一個每秒運行一次的定時器
$ws_worker->onWorkerStart = function ($worker) {
// 這里使用一個定時器,間隔時間為1s
Timer::add(12, function () use ($worker) {
$time_now = time();
foreach ($worker->connections as $connection) {
// 有可能該connection還沒收到過消息涮母,則lastMessageTime設置為當前時間
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通訊時間間隔大于心跳間隔谆趾,則認為客戶端已經(jīng)下線,關閉連接
if (HEARTBEAT_TIME > 0 && $time_now - $connection->lastMessageTime > HEARTBEAT_TIME) {
$connection->close("Network connection timeout!");
}
// 如果心跳間隔設置為0的話叛本,可以服務端主動發(fā)送ping
if (HEARTBEAT_TIME == 0) {
$connection->send('ping');
}
}
});
};
$ws_worker->onMessage = function ($connection, $data) {
global $ws_worker;
// 假設消息格式為
// uid:message 時是對 uid 發(fā)送 message
// uid 為 all 時是全局廣播
list($recv_uid, $message) = explode(':', $data);
$ws_worker->uidConnections[$recv_uid] = $connection;
// 給connection臨時設置一個lastMessageTime屬性沪蓬,用來記錄上次收到消息的時間
$connection->lastMessageTime = time();
// 全局廣播
if ($recv_uid == 'all') {
broadcast($message);
} // 給特定uid發(fā)送
else {
// 可以向執(zhí)行的uid發(fā)送消息
sendMessageByUid($recv_uid, $message);
}
};
/**
* 直接將消息發(fā)送給用戶推送數(shù)據(jù)
* @param $message
*/
function broadcast($message)
{
global $ws_worker;
foreach ($ws_worker->uidConnections as $connection) {
$connection->send($message);
}
}
/**
* 針對uid推送數(shù)據(jù)
* @param $uid
* @param $message
*/
function sendMessageByUid($uid, $message)
{
global $ws_worker;
// 這里可以自定義自己的邏輯業(yè)務
if (isset($ws_worker->uidConnections[$uid]) && $uid) {
$ws_worker->uidConnections[$uid]->send($message);
}
}
// 運行worker
Worker::runAll();
客戶端代碼:
這個采用html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>WebSocket示例</title>
<script type="text/javascript">
function WebSocketTest() {
if ("WebSocket" in window) {
// 打開一個 web socket
var ws = new WebSocket("ws://localhost:2000");
ws.onopen = function () {
ws.send("100:haha");
console.log("數(shù)據(jù)發(fā)送中...");
};
ws.onmessage = function (e) {
var received_msg = e.data;
console.log("數(shù)據(jù)已接收:" + received_msg)
};
ws.onclose = function () {
// 關閉 websocket
console.log("連接已關閉...")
};
}
else {
// 瀏覽器不支持 WebSocket
alert("您的瀏覽器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">打開連接</a>
</div>
</body>
</html>
效果