这个[Socket.IO](https://socket.io/)是一个建立在 WebSocket 协议之上的库,可以在客户端和服务器之间实现低延迟、双向和基于事件的通信。
:-: ![](https://img.kancloud.cn/fe/96/fe9652857d1e096233080d3180029900_840x131.png =600x)
  并且提供额外的保证,例如回退到 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 广播消息(如下图所示),并且可以在页面中呈现消息内容。
:-: ![](https://img.kancloud.cn/80/71/8071c4a28f950597dcba222bc9eea54e_688x331.png =500x)
**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>
~~~
:-: ![](https://img.kancloud.cn/1c/42/1c426a250bf38c4e40f902b9edf579af_1392x90.png =600x)
  在页面的内嵌脚本中,先初始化 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 = "";
}
});
~~~
:-: ![](https://img.kancloud.cn/b9/b3/b9b3bfde7ba98f79e3537c424b424566_908x114.png =600x)
  最后注册 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);
});
~~~
:-: ![](https://img.kancloud.cn/38/b4/38b4004ed0af801da35e3651308d24f7_1132x522.png =600x)
  客户端中的 socket 实例有 3 个保留事件,connect、connect\_error 和 disconnect。
  其中 connect\_error 会在底层连接失败或中间件拒绝连接时触发。
  下面是一张客户端 socket 连接的生命周期图,在建立连接时会分两种情况,在断开连接时还会自动重连。
:-: ![](https://img.kancloud.cn/04/2c/042c51310d2d8efeca3cefd2a91dc588_1242x1266.png =800x)
**4)请求头和消息**
  下图是一个请求头,状态码是 101 表示可以使用新协议,Connection: Upgrade 指示这是一个升级请求,Upgrade 指定 websocket 协议。
  一旦这次升级完成后,连接就变成了双向管道。sid 参数表示一个会话 ID。
:-: ![](https://img.kancloud.cn/66/d9/66d9250c0b92e8497cd8bd6e639c96b5_1400x370.png =800x)
  下图一系列的消息,每条消息的开头都是 1 到 2 个数字,它们都有各自的含义。
  第四条是发送的消息,第五条是接收的消息。
:-: ![](https://img.kancloud.cn/da/24/da2401b6375da45d97fe060d64712c4c_1596x364.png =600x)
  第一个数字是 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)**
  命名空间是一种通信通道,允许通过单个共享连接拆分应用程序的逻辑,即多路复用,适合一台服务器提供多条不同长连接业务的场景。
  如下图所示,分配了两个命名空间,通过一条管道连接了客户端和服务器。
:-: ![](https://img.kancloud.cn/02/8b/028b503236ec0d1b0b92acf0e6622b01_1321x326.png =800x)
  在服务端,注册 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);
});
});
~~~
:-: ![](https://img.kancloud.cn/b4/82/b482a57846a142d289377d1e371e6939_688x331.png =500x)
  socket.to() 的效果其实就是这条消息不会让自己收到,与 io.to() 的区别是后者可以让自己也收到。
  不过在调试的时候,调用 io.to() 后,不知为何,客户端都收不到消息。
  在做即时通信的项目时,采用 socket.to() 更合适,自己发送的消息完全可以通过脚本添加到聊天界面中。
**3)适配器(adapter)**
  适配器是一个服务端组件,负责将事件广播到所有或部分客户端。
  当扩展到多个 Socket.IO 服务器时,需要集群部署时,就得将默认的内存适配器替换为另一种,例如 Redis、MongoDB 等。
  这样做的目的,就是为了将事件正确路由到所有客户端。
  在下图中,客户端触发事件后,经过适配器路由到集群的 Socket.IO 服务器中。
![](https://img.kancloud.cn/02/99/029916463caa029ac8efd378e4349ef5_1134x702.png =800x)
  以 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),欢迎浏览。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[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年
- 日志
- 2020