這個[Socket.IO](https://socket.io/)是一個建立在 WebSocket 協議之上的庫,可以在客戶端和服務器之間實現低延遲、雙向和基于事件的通信。
:-: 
  并且提供額外的保證,例如回退到 HTTP 長輪詢、自動重連、數據包緩沖、多路復用等。
  這個[WebSocket](https://zh.wikipedia.org/wiki/WebSocket)是一種基于 TCP 協議在服務器和瀏覽器之間提供全雙工和低延遲通道的通信協議。
  注意,Socket.IO 不是 WebSocket 的實現。盡管 Socket.IO 確實在可能的情況下使用 WebSocket 進行傳輸,但它為每個數據包添加了額外的元數據。
  這就是為什么 WebSocket 客戶端將無法成功連接到 Socket.IO 服務器,而 Socket.IO 客戶端也將無法連接到普通的 WebSocket 服務器。
  如果需要一個普通的 WebSocket 服務器,可以使用[ws](https://github.com/websockets/ws)或[μWebSockets.js](https://github.com/uNetworking/uWebSockets.js)。
  在 Socket.IO 的底層依賴[Engine.IO](https://github.com/socketio/engine.io)引擎,它是跨瀏覽器/跨設備雙向通信層的實現,可處理各種傳輸、[升級機制](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Protocol_upgrade_mechanism)和斷線檢測等。
  剛剛所說的自動重連、數據包緩沖、多路復用等附加功能都是 Engine.IO 引擎提供的能力。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、廣播
  現在來建立一個提供表單和消息列表的簡單 HTML 網頁,用 Socket.IO 廣播消息(如下圖所示),并且可以在頁面中呈現消息內容。
:-: 
**1) HTTP 服務器**
  首先安裝 socket.io 包:npm install socket.io。
  然后創建一個[HTTP 服務器](https://www.cnblogs.com/strick/p/16243384.html),用于接收 HTML 和 JavaScript 文件的請求,內部實現了個簡單的路由。
  其中[URL](https://nodejs.org/dist/latest-v18.x/docs/api/url.html)實例用于解析請求地址,最終響應的內容是通過[fs.readFileSync()](https://www.cnblogs.com/strick/p/16252310.html)同步讀取到的。
  index.html 文件的內容會在后文給出,socket.io.js 是從 node\_modules/socket.io/client-dist/socket.io.js 目錄中復制過來的。
~~~
const http = require('http');
const fs = require('fs');
// HTTP服務器
const server = http.createServer((req, res) => {
// 實例化 URL 類
const url = new URL(req.url, 'http://localhost:1234');
const { pathname } = url;
// 路由
if(pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(fs.readFileSync('./index.html'));
}else if(pathname === '/socket.io.js') {
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(fs.readFileSync('../socket.io.js'));
}
});
// 監控端口
server.listen(1234);
~~~
**2)Socket.IO 服務器**
  接著是創建 Socket.IO 服務器,其中 socket.id 是每個新連接都會被分配到的一個隨機的 20 個字符的標識符,此標識符與客戶端的值同步。
  connection 是建立連接時的事件,disconnect 是斷開連接時的事件,chat message 是注冊的接收消息的自定義事件。
~~~
const { Server } = require("socket.io");
const io = new Server(server);
io.on('connection', (socket) => {
console.log('id', socket.id);
// socket.broadcast.emit('hi'); // 廣播給其他人,除了自己
console.log('a user connected');
// 注冊斷開連接事件
socket.on('disconnect', () => {
console.log('user disconnected');
});
// 注冊接收消息事件
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
// 觸發事件
io.emit('chat message', msg);
});
});
~~~
**3)廣播頁面**
  在廣播頁面中,先給出 HTML 結構和 CSS 樣式,在表單中有一個按鈕和文本框,如下圖所示。
~~~html
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO broadcast</title>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
<script src="../socket.io.js"></script>
</body>
</html>
~~~
:-: 
  在頁面的內嵌腳本中,先初始化 socket,io() 中的協議既可以是 http 也可以是 ws。
  其中 WS 是 WebSocket 協議的縮寫,WSS(Web Socket Secure)是 WebSocket 的加密版本。WS 一般默認是 80 端口,而 WSS 默認是 443 端口。
~~~
var socket = io("ws://localhost:1234");
~~~
  然后是注冊表單提交事件,在文本框中輸入內容后,觸發 chat message 事件發送消息到服務器中,服務器情況如下圖所示。
~~~
var messages = document.getElementById("messages");
var form = document.getElementById("form");
var input = document.getElementById("input");
// 注冊表單提交事件
form.addEventListener("submit", function (e) {
e.preventDefault();
if (input.value) {
socket.emit("chat message", input.value);
input.value = "";
}
});
~~~
:-: 
  最后注冊 chat message 事件,和服務器中的事件同名,在接收到從服務器傳回的消息時,就在頁面中增加一欄消息(如下圖所示),類似于聊天記錄。
~~~
socket.on("chat message", function (msg) {
var item = document.createElement("li");
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
});
~~~
:-: 
  客戶端中的 socket 實例有 3 個保留事件,connect、connect\_error 和 disconnect。
  其中 connect\_error 會在底層連接失敗或中間件拒絕連接時觸發。
  下面是一張客戶端 socket 連接的生命周期圖,在建立連接時會分兩種情況,在斷開連接時還會自動重連。
:-: 
**4)請求頭和消息**
  下圖是一個請求頭,狀態碼是 101 表示可以使用新協議,Connection: Upgrade 指示這是一個升級請求,Upgrade 指定 websocket 協議。
  一旦這次升級完成后,連接就變成了雙向管道。sid 參數表示一個會話 ID。
:-: 
  下圖一系列的消息,每條消息的開頭都是 1 到 2 個數字,它們都有各自的含義。
  第四條是發送的消息,第五條是接收的消息。
:-: 
  第一個數字是 Engine.IO 的通信類型。
| key | value |
| --- | --- |
| 0 | open |
| 1 | close |
| 2 | ping |
| 3 | pong |
| 4 | message |
| 5 | upgrade |
| 6 | noop |
  第二個數字是 Socket.IO 的操作類型。
| key | value |
| --- | --- |
| 0 | CONNECT |
| 1 | DISCONNECT |
| 2 | EVENT |
| 3 | ACK |
| 4 | ERROR |
| 5 | BINARY\_EVENT |
| 6 | BINARY\_ACK |
## 二、附加功能
  附加功能包括命名空間、專屬通道和適配器。
**1)命名空間(namespace)**
  命名空間是一種通信通道,允許通過單個共享連接拆分應用程序的邏輯,即多路復用,適合一臺服務器提供多條不同長連接業務的場景。
  如下圖所示,分配了兩個命名空間,通過一條管道連接了客戶端和服務器。
:-: 
  在服務端,注冊 connection 事件之前需要先調用 of() 方法,參數要和客戶端請求地址中的路徑一致。
  注意,與之前不同的是,觸發事件的對象是 socket 而不是 io,也就是調用 socket.emit() 才能發送消息。
~~~
const io = new Server(server);
io.of("/orders").on('connection', (socket) => {
socket.on('chat message', (msg) => {
console.log('orders message: ' + msg);
socket.emit('chat message', msg);
});
});
io.of("/users").on('connection', (socket) => {
socket.on('chat message', (msg) => {
console.log('users message: ' + msg);
socket.emit('chat message', msg);
});
});
~~~
**2)專屬通道(room)**
  room 可以建立專屬于幾條 socket 的通道,用于向一部分客戶端廣播事件,如下圖所示。
  類似于微信群的概念,發送的消息,只能群里的人收到。
  注意,room 是服務端的概念,客戶端是不知道 room 的存在。
  客戶端延續命名空間中的代碼不需要改造,在服務端調用 join() 方法加入一個 room,leave() 方法可以離開一個 room。
  然后在接收消息時調用 to() 方法給指定 room 中的 socket 發送消息,但不包括自己,效果如下圖所示。
~~~
io.of("/orders").on('connection', (socket) => {
socket.join("one room");
// 注冊接收消息事件
socket.on('chat message', (msg) => {
socket.to("one room").emit('chat message', msg);
});
});
~~~
:-: 
  socket.to() 的效果其實就是這條消息不會讓自己收到,與 io.to() 的區別是后者可以讓自己也收到。
  不過在調試的時候,調用 io.to() 后,不知為何,客戶端都收不到消息。
  在做即時通信的項目時,采用 socket.to() 更合適,自己發送的消息完全可以通過腳本添加到聊天界面中。
**3)適配器(adapter)**
  適配器是一個服務端組件,負責將事件廣播到所有或部分客戶端。
  當擴展到多個 Socket.IO 服務器時,需要集群部署時,就得將默認的內存適配器替換為另一種,例如 Redis、MongoDB 等。
  這樣做的目的,就是為了將事件正確路由到所有客戶端。
  在下圖中,客戶端觸發事件后,經過適配器路由到集群的 Socket.IO 服務器中。

  以 redis 為例,首先安裝 @socket.io/redis-adapter 和 ioredis 庫,前者在 v7 版本之前叫 socket.io-redis。
  然后是改造服務端,客戶端不用做調整,引入兩個庫。本機已安裝 redis 環境,若未安裝不知道會不會報錯。
~~~
const { Server } = require("socket.io");
const { createAdapter } = require("@socket.io/redis-adapter");
const { Cluster } = require("ioredis");
~~~
  接著連接 redis 庫,調用 adapter() 方法選擇適配器。
~~~
const io = new Server(server);
const pubClient = new Cluster([
{
host: "localhost",
port: 6380,
}
]);
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
~~~
參考資料:
[Node.js + Socket.io 實現一對一即時聊天](https://www.nodejs.red/#/nodejs/npm/private-chat-socketio)
[socket.io官方文檔中文版](https://zhuanlan.zhihu.com/p/29148869)
[基于socket.io構建即時通訊應用](https://zhuanlan.zhihu.com/p/95575230)
[socket.io namespaces and rooms (譯)?](https://segmentfault.com/a/1190000021255876)
[Socket.io源碼分析](https://zhuanlan.zhihu.com/p/27624534)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020