HTML5 的出現,標誌着後 Flash 時代各類現代瀏覽器的集體爆發,也是謹防 Adobe 一家獨大的各家廠商們,歷經多年各自爲戰,想換個活法兒並終於達成必定共識後,所積kao累bei的技術的一次集中釋放 -- 正所謂 「H5 是個筐,什麼均可以往裏裝」。javascript
其中引人矚目並被普遍支持的一項,就是這次要談論的 WebSocket 了。本文將嘗試說明它被用來解決什麼問題,以及與久經沙場的「傳統」 Socket 又有什麼異同等基礎問題。php
望文而生義,面對 WebSocket 這個名稱,web
無需作太多解釋,傻傻分不清楚的 socket
看着也是至關的面熟;甭管有沒有聯繫,先來了解一下也無妨:html
Socket 每每指的是 TCP/IP 網絡環境中的兩個鏈接端,以及爲方便此類開發所設計的一組編程 API前端
如圖,英文單詞 "socket" 的字面原義是 「孔」 或 「插座」。html5
做爲一個技術用語時,socket
一般取後一種意思,像一個多孔插座。用於描述一個通訊鏈路兩端的 IP 地址和端口等,能夠用來實現不一樣設備之間的通訊。Socket
、TCP Socket
都是通用的叫法,中文通常習慣性的譯做**「套接字」、「TCP套接字」** 等。java
...至於爲嘛把「插座兒」翻譯成「套接字」,好奇的程序猿並不在少數,科考文章在文章底部參考連接中能夠找到。node
能夠將服務端主機想象成一個佈滿各類插座的房間,每一個插座有一個編號,有的插座提供 220 伏交流電,有的提供固定電話信號,有的則提供有線電視節目。客戶端軟件將插頭接入不一樣編號的插座,就能夠獲得不一樣的服務git
OSI 模型做爲一種概念模型,由國際標準化組織(ISO)提出,一個試圖使各類計算機在世界範圍內互連爲網絡的標準框架。咱們熟悉的 HTTP、FTP 等協議都工做在最頂端的應用層(Application Layer)。github
而 **TCP/IP 協議族(Protocol Suite)**將軟件通訊過程抽象化爲四個抽象層,常被視爲是簡化的七層OSI模型。當多個層次的協議共同工做時,相似數據結構中的堆棧,所以又被稱爲 TCP/IP 協議棧(Protocol Stack)。web
單說 TCP 的話,指的是面向鏈接的一種傳輸控制協議。TCP 鏈接以後,客戶端和服務器能夠互相發送和接收消息,在客戶端或者服務器沒有主動斷開以前,鏈接一直存在,故稱爲長鏈接。
Socket 其實並非一個標準的協議,而是應用層與 TCP/IP 協議族通訊的中間軟件抽象層,它是一組接口,工做位置基本在 OSI 模型會話層(第5層),是爲了方便你們直接使用更底層協議(通常是 TCP 或 UDP )而存在的一個抽象層。
在設計模式中,Socket其實就是一個門面(facade)模式,它把複雜的 TCP/IP 協議族隱藏在 Socket API 後面,對用戶來講,一組簡單的接口就是所有,讓 Socket 去組織數據,以符合指定的協議
最先的一套 Socket API 是採用 C 語言實現的,也就成爲了 Socket 的事實標準。
傳統的後端編程語言基本都有 Socket API 的封裝;而在 HTML5 出現以前,要想用純前端技術實現 TCP Socket 的客戶端,也基本只有 Java Applet (java.net.Socket
或 java.net.DatagramSocket
或 java.net.MulticastSocket
) 、Flash (flash.net.Socket
或 flash.net.XMLSocket
) 或 Silverlight(System.Net.Sockets
) 等能夠選擇。
下面以 PHP 的 服務器/客戶端 實現爲例,演示一個最基礎的例子:
<?php
//server.php
set_time_limit(0);
$ip = '127.0.0.1';
$port = 1999;
// 建立一個Socket
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
// 綁定Socket地址和端口
$ret = socket_bind($sock,$ip,$port);
// 開始監聽連接
$ret = socket_listen($sock,4);
$count = 0; //最多接受幾回請求後就退出
do {
// 另外一個Socket來處理通訊
if (($msgsock = socket_accept($sock)) >= 0) {
// 發到客戶端
$msg ="server: HELLO CLIENTS!\n";
if (socket_write($msgsock, $msg, strlen($msg))) {
echo "發送成功!\n";
}
// 得到客戶端的輸入
$buf = socket_read($msgsock,8192);
$talkback = "接受成功!內容爲:$buf\n";
echo $talkback;
if(++$count >= 5){
break;
};
}
// 關閉sockets
socket_close($msgsock);
} while (true);
socket_close($sock);
echo "TCP 鏈接關閉OK\n";
?>
複製代碼
<?php
//client.php
error_reporting(E_ALL);
set_time_limit(0);
$port = 1999;
$ip = "127.0.0.1";
// 建立Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 綁定Socket地址和端口
$result = socket_connect($socket, $ip, $port);
if ($result >= 0) echo "TCP 鏈接OK\n";
$in = "client: HELLO SERVER!\r\n";
if(socket_write($socket, $in, strlen($in))) {
echo "發送成功!\n";
}
$out = '';
while($out = socket_read($socket, 8192)) {
echo "接受成功!內容爲:",$out;
}
socket_close($socket);
echo "TCP 鏈接關閉OK\n";
?>
複製代碼
WebSockets
爲 C/S 兩端提供了實時交互通訊的能力,容許服務器主動發送信息給客戶端,是一種區別於 HTTP 的全新雙向數據流協議
簡單的說,傳統的 TCP Socket 是一套相對標準化的 API,而出現時間不久的 WebSocket 是一種網絡協議 -- 兩碼事。
WebSocket 底層是基於 TCP 協議的,因此早期草案中叫作 TCPConnection,最後之因此更名,實際上是借用了傳統 Socket 溝通 TCP 網絡兩端的意思而已。
Socket 之於 WebSocket
就像
Java 和 JavaScript
雷鋒 和 雷峯塔
張三 和 張三丰
周杰 和 周杰倫
北大 和 北大青鳥
印度 和 印度尼西亞
同樣
基本上沒啥關係...
-- 魯迅《我沒說過的話》
複製代碼
在基於 請求/響應 模式的 HTTP/HTTPS 下,若是是對實時性要求較高的場景,客戶端就須要不停的詢問服務端有無可用的數據,這在各方面都是笨拙而不划算的。
而在 WebSocket 的全雙工(容許數據在兩個方向上同時傳輸)方式下,客戶端只要靜靜地聽招呼便可,有可用數據時服務端會自動通知它。
實際上,每當談到實時雙向通訊的問題時,咱們天然會想起歷年來一些基於 HTTP 技術的嘗試;也正是基於這些以前工做中的實踐和困擾,WebSocket 才應運而生。讓咱們大概回顧一下相關的方案及其缺陷:
輪詢 (Polling)
藉助於 setInterval() 等方式,客戶端不斷的發送請求並獲得響應。這種作法比較簡單,能夠在必定程度上解決問題。不過對於輪詢的時間間隔須要進行仔細考慮。輪詢的間隔過長,會致使用戶不能及時接收到更新的數據;輪詢的間隔太短,會致使查詢請求過多,增長服務器端的負擔。
長輪詢 (Long Polling)
這是對輪詢的一種改進。客戶端發出請求後,服務器端用 while(true) 等方式阻塞住請求,直到有可用數據才發送響應數據,而客戶端收到響應後再發送下一個請求。
這種方式又被成爲 「Hanging GET」、「反向 Ajax」 或 「Comet」 等,雖然看上去很像服務器推送,但仍然是基於 HTTP 的一種慢響應;且在數據更新頻繁的狀況下,其效率並不優於通常的輪詢。
HTTP 流 (Streaming)
使用 HTTP 1.1 且響應頭中包含 Transfer-Encoding: chunked
的狀況下,服務器發送給客戶端的數據能夠分紅多個部分,保持打開(while-true, sleep等),並週期性 flush() 分塊傳輸。
客戶端只發送一個HTTP鏈接,在 xhr.readyState==3 狀態下,用 xhr.responseText.substring 獲取每次的數據。
可是數據響應可能會因 代理服務器 或 防火牆 等中間人形成延遲,因此可能還要額外探測這種狀況以切換到長輪詢方式。
SSE (Server-Sent Events)
SSE 規範也是 HTML 5 規範的一個組成部分。服務器端響應的內容類型是text/event-stream
,在瀏覽器端使用 EventSource
對象處理返回的數據。
比之於 WebSocket,SSE 的缺點在於:
大部分傳統的方式既浪費帶寬(HTTP HEAD 是比較大的),又消耗服務器 CPU 佔用(沒有信息也要接受請求);而 WebSocket 則會大幅下降上述的消耗,更適用於如下場景:
兼容性也使人滿意,非要說什麼時候不適用的話,大概就是少數必須兼容老舊瀏覽器,或者對實時要求明顯不高的狀況下了。
WebSocket 鏈接的 URL 使用 ws://
或 wss://
等開頭,其加密、cookie 等策略和對應的 HTTP/HTTPS 基本相同。
HTTP、WebSocket 等應用層協議,都是基於 TCP 協議來傳輸數據的,能夠把這些高級協議理解成對 TCP 的封裝。
在 HTTP 下,客戶端不發請求的話,服務器永遠沒法發送數據給客戶端。
而對於 WebSocket 來講,它必須依賴 HTTP 協議進行一次握手,以兼容瀏覽器的規範;能夠將之視爲 HTTP 的一種補充和升級。
在進行第一次 HTTP 請求以後,後續的就所有采用 TCP 通道進行雙向通信了。因此,HTTP 和 WebSocket 雖都基於 TCP 協議,倒是徹底不一樣的兩種通信方式。
來看個典型的 WebSocket 握手請求:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
複製代碼
服務器響應則是:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
複製代碼
其中第一個關鍵點是 Update
和 Connection
請求響應頭,表示請求和確認切換到 WebSocket 協議;
13
是一個固定的版本號;
而請求頭 Sec-WebSocket-Key
爲瀏覽器隨機生成的 Base64 編碼,將之附加上一個固定爲 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
的「魔法字符串」,再計算SHA-1摘要並進行BASE-64編碼,就是響應中的 Sec-WebSocket-Accept
了。 ╮(╯▽╰)╭
在瀏覽器中能夠直接調用 WebSocket
對象,其定義以下:
enum BinaryType { "blob", "arraybuffer" };
[Constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []), Exposed=(Window,Worker)]
interface WebSocket : EventTarget {
readonly attribute USVString url;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long long bufferedAmount;
// networking
attribute EventHandler onopen;
attribute EventHandler onerror;
attribute EventHandler onclose;
readonly attribute DOMString extensions;
readonly attribute DOMString protocol;
void close([Clamp] optional unsigned short code, optional USVString reason);
// messaging
attribute EventHandler onmessage;
attribute BinaryType binaryType;
void send(USVString data);
void send(Blob data);
void send(ArrayBuffer data);
void send(ArrayBufferView data);
};
複製代碼
使用起來大概是這樣的:
var ws = new WebSocket('ws://www.xxx.com/some.php');
ws.send('xxx'); //每次只能發送字符串
ws.onmessage = function(event) {
var data = event.data;
};
ws.onerror = function() {
ws.close();
};
複製代碼
這裏隨便設想一個用戶場景,好比咱們要作一個在線紙牌遊戲,確定就是一個多人進入同一個房間的形式,而且每一個人的動做能廣播給其餘人。
下面用 WebSocket 作一個最基礎的驗證原型,讓每一個玩家知道其餘人的進入、離開、出牌、悔牌,甚至是耍賴換牌等:
咱們用 nodejs+expressjs 搭建基礎服務器,並用 https://github.com/websockets/ws 封裝的庫實現 WebSocket 協議的服務器端邏輯:
// server.js
var express = require('express')
var ws = require('./ws')
var app = express()
app.get('/', function (req, res) {
res.sendFile(__dirname + '/ws.html');
})
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
})
複製代碼
// ws.js
const { Server, OPEN } = require('ws');
const clients = []; //array of websocket clients
const cardsArr = []; //array of {cardId, count, title, ...}
let _lock = false;
const wss = new Server({port: 40510})
wss.on('connection', function (ws) {
const _cid = clients.push(ws) - 1;
ws.on('message', function (json) {
const {
act,
cid,
data
} = JSON.parse(json);
switch (act) {
case 'client:join':
_onCustomerJoin(ws, _cid);
break;
case 'client:leave':
_onCustomerLeave(cid);
break;
case 'client:add': //增長牌
_onAddCard(cid, data);
break;
case 'client:update': //修改牌
_onUpdateCard(cid, data);
break;
case 'client:remove': //刪除牌
_onRemoveCard(cid, data);
break;
case 'client:win': //下單
_onWin(cid);
break;
default:
console.log('received: %s', act, cid)
break;
}
});
});
function _ensureLock(func) {
return function() {
if (_lock) return;
_lock = true;
const rtn = func.apply(null, arguments);
_lock = false;
return rtn;
};
}
function _findCard(cardId) {
const cidx = cardsArr.map(Card=>Card.cardId).indexOf(cardId);
return cidx;
}
const _broadcast = (excludeId, msg, data=null)=>{
clients.forEach( (client, cidx)=>{
if (cidx === excludeId) return;
if (client && client.readyState === OPEN) {
client.send(JSON.stringify({
act: 'server:broadcast',
msg: msg,
data: data
}));
}
} );
};
const _onCustomerJoin = (ws, cid)=>{
ws.send(JSON.stringify({
act: 'server:regist',
data: {
cid: cid
}
}));
_broadcast(cid, '玩家加入:', {cid: cid});
};
const _onCustomerLeave = (cid)=>{
clients[cid].terminate();
clients.splice(cid, 1);
_broadcast(cid, '玩家退出:', {cid: cid});
};
const _onAddCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d !== -1) {
cardsArr.splice(d, 1);
}
cardsArr.push(data);
_broadcast(-1, '玩家添加了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onUpdateCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d === -1) return;
cardsArr[d] = data;
_broadcast(-1, '玩家更改了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onRemoveCard = _ensureLock( (cid, data)=>{
const d = _findCard(data.cardId);
if (d === -1) return;
cardsArr.splice(d, 1);
_broadcast(-1, '玩家刪除了牌', {
cid: cid,
Card: data,
cardsArr: cardsArr
});
} );
const _onWin = _ensureLock( (cid)=>{
//do sth. here
_broadcast(cid, '玩家胡牌了');
} );
複製代碼
<h1></h1>
<div></div>
<button onclick="_add()">出牌</button>
<button onclick="_update()">換牌</button>
<button onclick="_remove()">悔牌</button>
<button onclick="_win()">胡牌</button>
<button onclick="_leave()">離開</button>
<script> let cid = null; const ws = new WebSocket('ws://localhost:40510'); ws.onopen = function () { console.log('websocket is connected ...'); _send({ act: 'client:join' }); }; ws.onmessage = function (ev) { const { act, msg, data } = JSON.parse(ev.data); switch(act) { case 'server:regist': cid = data.cid; console.log(`regist: cid is ${cid}`); document.querySelector('h1').innerHTML = 'I AM: ' + cid; break; case 'server:broadcast': console.log('從服務器端接收的廣播:', msg, data); if (data && data.cardsArr) { document.querySelector('div').innerHTML = JSON.stringify( data.cardsArr, null, 4 ); } break; default: console.log(ev); break; } } function _send(json) { ws.send(JSON.stringify(json)); } function _add() { _send({ act: 'client:add', cid: cid, data: { cardId: 111, count: 1, title: '紅桃A' } }) } function _update() { _send({ act: 'client:update', cid: cid, data: { cardId: 111, count: 2, title: '黑桃9' } }) } function _remove() { _send({ act: 'client:remove', cid: cid, data: { cardId: 111 } }) } function _win() { _send({ act: 'client:win', cid: cid }) } function _leave() { _send({ act: 'client:leave', cid: cid }) } </script>
複製代碼
玩家 0 加入:
玩家 1 加入:
玩家 1 出牌:
玩家 1 胡牌並退出:
長按二維碼或搜索 fewelife 關注咱們哦