WebSocket 簡介及應用例項

江米小棗tonylua發表於2019-03-04

HTML5 的出現,標誌著後 Flash 時代各種現代瀏覽器的集體爆發,也是謹防 Adobe 一家獨大的各家廠商們,歷經多年各自為戰,想換個活法兒並終於達成一定共識後,所積kao累bei的技術的一次集中釋放 -- 正所謂 “H5 是個筐,什麼都可以往裡裝”。

其中引人矚目並被廣泛支援的一項,就是此次要談論的 WebSocket 了。本文將嘗試說明它被用來解決什麼問題,以及與久經沙場的“傳統” Socket 又有什麼異同等基礎問題。

I. 定義及由來

望文而生義,面對 WebSocket 這個名稱,web 無需做太多解釋,傻傻分不清楚的 socket 看著也是相當的面熟;甭管有沒有聯絡,先來了解一下也無妨:

(1.1) 傳統的 Socket API

Socket 往往指的是 TCP/IP 網路環境中的兩個連線端,以及為方便此類開發所設計的一組程式設計 API

WebSocket 簡介及應用例項

如圖,英文單詞 "socket" 的字面原義是 “孔” 或 “插座”。

作為一個技術用語時,socket 通常取後一種意思,像一個多孔插座。用於描述一個通訊鏈路兩端的 IP 地址和埠等,可以用來實現不同裝置之間的通訊。SocketTCP Socket都是通用的叫法,中文一般習慣性的譯作**“套接字”、“TCP套接字”** 等。

...至於為嘛把“插座兒”翻譯成“套接字”,好奇的程式猿並不在少數,科考文章在文章底部參考連結中可以找到。

WebSocket 簡介及應用例項

可以將服務端主機想象成一個佈滿各種插座的房間,每個插座有一個編號,有的插座提供 220 伏交流電,有的提供固定電話訊號,有的則提供有線電視節目。客戶端軟體將插頭接入不同編號的插座,就可以得到不同的服務

Socket API 所處的樓層

WebSocket 簡介及應用例項

OSI 模型作為一種概念模型,由國際標準化組織(ISO)提出,一個試圖使各種計算機在世界範圍內互連為網路的標準框架。我們熟悉的 HTTP、FTP 等協議都工作在最頂端的應用層(Application Layer)。

而 **TCP/IP 協議族(Protocol Suite)**將軟體通訊過程抽象化為四個抽象層,常被視為是簡化的七層OSI模型。當多個層次的協議共同工作時,類似資料結構中的堆疊,因此又被稱為 TCP/IP 協議棧(Protocol Stack)

單說 TCP 的話,指的是面向連線的一種傳輸控制協議。TCP 連線之後,客戶端和伺服器可以互相傳送和接收訊息,在客戶端或者伺服器沒有主動斷開之前,連線一直存在,故稱為長連線。

Socket 其實並不是一個標準的協議,而是應用層與 TCP/IP 協議族通訊的中間軟體抽象層,它是一組介面,工作位置基本在 OSI 模型會話層(第5層),是為了方便大家直接使用更底層協議(一般是 TCP 或 UDP )而存在的一個抽象層。

在設計模式中,Socket其實就是一個門面(facade)模式,它把複雜的 TCP/IP 協議族隱藏在 Socket API 後面,對使用者來說,一組簡單的介面就是全部,讓 Socket 去組織資料,以符合指定的協議

WebSocket 簡介及應用例項

最早的一套 Socket API 是採用 C 語言實現的,也就成為了 Socket 的事實標準。

WebSocket 簡介及應用例項

常見的 Socket API 實現

一些語言的實現

傳統的後端程式語言基本都有 Socket API 的封裝;而在 HTML5 出現之前,要想用純前端技術實現 TCP Socket 的客戶端,也基本只有 Java Applet (java.net.Socketjava.net.DatagramSocketjava.net.MulticastSocket) 、Flash (flash.net.Socketflash.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";
?>
複製程式碼

WebSocket 簡介及應用例項

WebSocket 簡介及應用例項

(1.2) HTML5 帶來的 WebSocket 協議

WebSockets 為 C/S 兩端提供了實時互動通訊的能力,允許伺服器主動傳送資訊給客戶端,是一種區別於 HTTP 的全新雙向資料流協議

WebSocket 簡介及應用例項

*web + socket ?*

簡單的說,傳統的 TCP Socket 是一套相對標準化的 API,而出現時間不久的 WebSocket 是一種網路協議 -- 兩碼事。

WebSocket 底層是基於 TCP 協議的,所以早期草案中叫做 TCPConnection,最後之所以改名,其實是借用了傳統 Socket 溝通 TCP 網路兩端的意思而已。


	Socket 之於 WebSocket
	就像
	Java 和 JavaScript
	雷鋒 和 雷峰塔
	張三 和 張三丰
	周杰 和 周杰倫
	北大 和 北大青鳥
	印度 和 印度尼西亞
	一樣
	基本上沒啥關係...
	
			-- 魯迅《我沒說過的話》
			
複製程式碼

要解決的問題

WebSocket 簡介及應用例項

*HTTP 的工作方式*

在基於 請求/響應 模式的 HTTP/HTTPS 下,如果是對實時性要求較高的場景,客戶端就需要不停的詢問服務端有無可用的資料,這在各方面都是笨拙而不划算的。

WebSocket 簡介及應用例項

*WebSocket 的工作方式*

而在 WebSocket 的全雙工(允許資料在兩個方向上同時傳輸)方式下,客戶端只要靜靜地聽招呼即可,有可用資料時服務端會自動通知它。

與 WebSocket 類似的技術

實際上,每當談到實時雙向通訊的問題時,我們自然會想起歷年來一些基於 HTTP 技術的嘗試;也正是基於這些之前工作中的實踐和困擾,WebSocket 才應運而生。讓我們大概回顧一下相關的方案及其缺陷:

輪詢 (Polling)

WebSocket 簡介及應用例項

藉助於 setInterval() 等方式,客戶端不斷的傳送請求並得到響應。這種做法比較簡單,可以在一定程度上解決問題。不過對於輪詢的時間間隔需要進行仔細考慮。輪詢的間隔過長,會導致使用者不能及時接收到更新的資料;輪詢的間隔過短,會導致查詢請求過多,增加伺服器端的負擔。

長輪詢 (Long Polling)

WebSocket 簡介及應用例項

這是對輪詢的一種改進。客戶端發出請求後,伺服器端用 while(true) 等方式阻塞住請求,直到有可用資料才傳送響應資料,而客戶端收到響應後再傳送下一個請求。

這種方式又被成為 “Hanging GET”、“反向 Ajax” 或 “Comet” 等,雖然看上去很像伺服器推送,但仍然是基於 HTTP 的一種慢響應;且在資料更新頻繁的情況下,其效率並不優於一般的輪詢。

HTTP 流 (Streaming)

WebSocket 簡介及應用例項

使用 HTTP 1.1 且響應頭中包含 Transfer-Encoding: chunked 的情況下,伺服器傳送給客戶端的資料可以分成多個部分,保持開啟(while-true, sleep等),並週期性 flush() 分塊傳輸。

客戶端只傳送一個HTTP連線,在 xhr.readyState==3 狀態下,用 xhr.responseText.substring 獲取每次的資料。

但是資料響應可能會因 代理伺服器 或 防火牆 等中間人造成延遲,所以可能還要額外探測這種情況以切換到長輪詢方式。

SSE (Server-Sent Events)

WebSocket 簡介及應用例項

SSE 規範也是 HTML 5 規範的一個組成部分。伺服器端響應的內容型別是text/event-stream,在瀏覽器端使用 EventSource 物件處理返回的資料。

比之於 WebSocket,SSE 的缺點在於:

  • 不支援 CORS
  • 單向通道,只能伺服器向瀏覽器端傳送
  • 瀏覽器相容性稍差

WebSocket 的用武之地

大部分傳統的方式既浪費頻寬(HTTP HEAD 是比較大的),又消耗伺服器 CPU 佔用(沒有資訊也要接受請求);而 WebSocket 則會大幅降低上述的消耗,更適用於以下場景:

  • 實時性要求高的應用
  • 聊天室
  • IoT (物聯網 - internet of things)
  • 線上多人遊戲

相容性也令人滿意,非要說何時不適用的話,大概就是少數必須相容老舊瀏覽器,或者對實時要求明顯不高的情況下了。

WebSocket 簡介及應用例項

HTTP 的擴充套件

WebSocket 簡介及應用例項

WebSocket 連線的 URL 使用 ws://wss:// 等開頭,其加密、cookie 等策略和對應的 HTTP/HTTPS 基本相同。

WebSocket 簡介及應用例項

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=
複製程式碼

其中第一個關鍵點是 UpdateConnection 請求響應頭,表示請求和確認切換到 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();
};
複製程式碼

II. 一個多使用者互動的 WebSocket 例項

這裡隨便設想一個使用者場景,比如我們要做一個線上紙牌遊戲,肯定就是一個多人進入同一個房間的形式,並且每個人的動作能廣播給其他人。

下面用 WebSocket 做一個最基礎的驗證原型,讓每個玩家知道其他人的進入、離開、出牌、悔牌,甚至是耍賴換牌等:

(2.1) 伺服器端的實現

我們用 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, '玩家胡牌了');
} );
複製程式碼

(2.2) 客戶端的實現

<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>
複製程式碼

(2.3) 執行效果

玩家 0 加入:

WebSocket 簡介及應用例項

玩家 1 加入:

WebSocket 簡介及應用例項

玩家 1 出牌:

WebSocket 簡介及應用例項

玩家 1 胡牌並退出:

WebSocket 簡介及應用例項

III. 總結

  • 傳統的 TCP Socket 往往指的是 TCP/IP 網路環境中的兩個連線端,以及為方便此類開發所設計的一組程式設計 API
  • WebSockets 為 C/S 兩端提供了實時互動通訊的能力,允許伺服器主動傳送資訊給客戶端
  • WebSockets 是 HTML 5 規範的一個組成部分,是一種區別於 HTTP 的全新雙向資料流協議
  • 全雙工通訊的 WebSockets 有效改善了之前 長輪詢 等方式的弊端
  • WebSockets 適用於實時性要求高的應用、聊天室、多人遊戲等

IV. 參考資料

  • 為什麼要翻譯成套接字:https://www.bbsmax.com/A/kvJ3rDV9zg/
  • https://blog.zengrong.net/post/2199.html
  • http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html
  • https://coderanch.com/t/204527/java/Successful-Applet-Socket-Connection
  • https://www.zhihu.com/question/20215561
  • https://www.jianshu.com/p/42260a2575f8
  • https://www.jianshu.com/p/59b5594ffbb0
  • http://enterprisewebbook.com/ch8_websockets.html
  • https://medium.com/@dominik.t/what-are-web-sockets-what-about-rest-apis-b9c15fd72aac
  • https://medium.com/platform-engineer/web-api-design-35df8167460
  • https://www.websocket.org/quantum.html
  • https://blog.gtwang.org/web-development/websocket-protocol/
  • https://medium.com/kifi-engineering/websockets-vs-regular-sockets-b3b8e7ea0708
  • http://shouce.jb51.net/actionscript3.0/flash/net/Socket.html
  • https://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/net/XMLSocket.html
  • https://github.com/theturtle32/AS3WebSocket
  • https://baike.baidu.com/item/socket/281150
  • https://baike.baidu.com/item/TCP%2FIP協議族
  • https://www.cnblogs.com/xuehaoyue/p/6639029.html
  • https://kotlintc.com/articles/4925
  • https://blog.csdn.net/future_todo/article/details/50097363
  • https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
  • http://lamb-mei.com/462/websocket-%E7%A5%9E%E5%A5%87%E5%AD%97%E4%B8%B2-258eafa5-e914-47da-95ca-c5ab0dc85b11/
  • https://kaazing.com/html5-websocket-security-is-strong/
  • https://www.asni.cn/2152

(end)

----------------------------------------

長按二維碼或搜尋 fewelife 關注我們哦

WebSocket 簡介及應用例項

相關文章