ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
&emsp;&emsp;这个[Socket.IO](https://socket.io/)是一个建立在 WebSocket 协议之上的库,可以在客户端和服务器之间实现低延迟、双向和基于事件的通信。 :-: ![](https://img.kancloud.cn/fe/96/fe9652857d1e096233080d3180029900_840x131.png =600x) &emsp;&emsp;并且提供额外的保证,例如回退到 HTTP 长轮询、自动重连、数据包缓冲、多路复用等。 &emsp;&emsp;这个[WebSocket](https://zh.wikipedia.org/wiki/WebSocket)是一种基于 TCP 协议在服务器和浏览器之间提供全双工和低延迟通道的通信协议。 &emsp;&emsp;注意,Socket.IO 不是 WebSocket 的实现。尽管 Socket.IO 确实在可能的情况下使用 WebSocket 进行传输,但它为每个数据包添加了额外的元数据。 &emsp;&emsp;这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通的 WebSocket 服务器。 &emsp;&emsp;如果需要一个普通的 WebSocket 服务器,可以使用[ws](https://github.com/websockets/ws)或[µWebSockets.js](https://github.com/uNetworking/uWebSockets.js)。 &emsp;&emsp;在 Socket.IO 的底层依赖[Engine.IO](https://github.com/socketio/engine.io)引擎,它是跨浏览器/跨设备双向通信层的实现,可处理各种传输、[升级机制](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Protocol_upgrade_mechanism)和断线检测等。 &emsp;&emsp;刚刚所说的自动重连、数据包缓冲、多路复用等附加功能都是 Engine.IO 引擎提供的能力。 &emsp;&emsp;本系列所有的示例源码都已上传至Github,[点击此处](https://github.com/pwstrick/node)获取。 ## 一、广播 &emsp;&emsp;现在来建立一个提供表单和消息列表的简单 HTML 网页,用 Socket.IO 广播消息(如下图所示),并且可以在页面中呈现消息内容。 :-: ![](https://img.kancloud.cn/80/71/8071c4a28f950597dcba222bc9eea54e_688x331.png =500x) **1) HTTP 服务器** &emsp;&emsp;首先安装 socket.io 包:npm install socket.io。 &emsp;&emsp;然后创建一个[HTTP 服务器](https://www.cnblogs.com/strick/p/16243384.html),用于接收 HTML 和 JavaScript 文件的请求,内部实现了个简单的路由。 &emsp;&emsp;其中[URL](https://nodejs.org/dist/latest-v18.x/docs/api/url.html)实例用于解析请求地址,最终响应的内容是通过[fs.readFileSync()](https://www.cnblogs.com/strick/p/16252310.html)同步读取到的。 &emsp;&emsp;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 服务器** &emsp;&emsp;接着是创建 Socket.IO 服务器,其中 socket.id 是每个新连接都会被分配到的一个随机的 20 个字符的标识符,此标识符与客户端的值同步。 &emsp;&emsp;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)广播页面** &emsp;&emsp;在广播页面中,先给出 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) &emsp;&emsp;在页面的内嵌脚本中,先初始化 socket,io() 中的协议既可以是 http 也可以是 ws。 &emsp;&emsp;其中 WS 是 WebSocket 协议的缩写,WSS(Web Socket Secure)是 WebSocket 的加密版本。WS 一般默认是 80 端口,而 WSS 默认是 443 端口。 ~~~ var socket = io("ws://localhost:1234"); ~~~ &emsp;&emsp;然后是注册表单提交事件,在文本框中输入内容后,触发 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) &emsp;&emsp;最后注册 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) &emsp;&emsp;客户端中的 socket 实例有 3 个保留事件,connect、connect\_error 和 disconnect。 &emsp;&emsp;其中 connect\_error 会在底层连接失败或中间件拒绝连接时触发。 &emsp;&emsp;下面是一张客户端 socket 连接的生命周期图,在建立连接时会分两种情况,在断开连接时还会自动重连。 :-: ![](https://img.kancloud.cn/04/2c/042c51310d2d8efeca3cefd2a91dc588_1242x1266.png =800x) **4)请求头和消息** &emsp;&emsp;下图是一个请求头,状态码是 101 表示可以使用新协议,Connection: Upgrade 指示这是一个升级请求,Upgrade 指定 websocket 协议。 &emsp;&emsp;一旦这次升级完成后,连接就变成了双向管道。sid 参数表示一个会话 ID。 :-: ![](https://img.kancloud.cn/66/d9/66d9250c0b92e8497cd8bd6e639c96b5_1400x370.png =800x) &emsp;&emsp;下图一系列的消息,每条消息的开头都是 1 到 2 个数字,它们都有各自的含义。 &emsp;&emsp;第四条是发送的消息,第五条是接收的消息。 :-: ![](https://img.kancloud.cn/da/24/da2401b6375da45d97fe060d64712c4c_1596x364.png =600x) &emsp;&emsp;第一个数字是 Engine.IO 的通信类型。 | key | value | | --- | --- | | 0 | open | | 1 | close | | 2 | ping | | 3 | pong | | 4 | message | | 5 | upgrade | | 6 | noop | &emsp;&emsp;第二个数字是 Socket.IO 的操作类型。 | key | value | | --- | --- | | 0 | CONNECT | | 1 | DISCONNECT | | 2 | EVENT | | 3 | ACK | | 4 | ERROR | | 5 | BINARY\_EVENT | | 6 | BINARY\_ACK | ## 二、附加功能 &emsp;&emsp;附加功能包括命名空间、专属通道和适配器。 **1)命名空间(namespace)** &emsp;&emsp;命名空间是一种通信通道,允许通过单个共享连接拆分应用程序的逻辑,即多路复用,适合一台服务器提供多条不同长连接业务的场景。 &emsp;&emsp;如下图所示,分配了两个命名空间,通过一条管道连接了客户端和服务器。 :-: ![](https://img.kancloud.cn/02/8b/028b503236ec0d1b0b92acf0e6622b01_1321x326.png =800x) &emsp;&emsp;在服务端,注册 connection 事件之前需要先调用 of() 方法,参数要和客户端请求地址中的路径一致。 &emsp;&emsp;注意,与之前不同的是,触发事件的对象是 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)** &emsp;&emsp;room 可以建立专属于几条 socket 的通道,用于向一部分客户端广播事件,如下图所示。 &emsp;&emsp;类似于微信群的概念,发送的消息,只能群里的人收到。 &emsp;&emsp;注意,room 是服务端的概念,客户端是不知道 room 的存在。 &emsp;&emsp;客户端延续命名空间中的代码不需要改造,在服务端调用 join() 方法加入一个 room,leave() 方法可以离开一个 room。 &emsp;&emsp;然后在接收消息时调用 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) &emsp;&emsp;socket.to() 的效果其实就是这条消息不会让自己收到,与 io.to() 的区别是后者可以让自己也收到。 &emsp;&emsp;不过在调试的时候,调用 io.to() 后,不知为何,客户端都收不到消息。 &emsp;&emsp;在做即时通信的项目时,采用 socket.to() 更合适,自己发送的消息完全可以通过脚本添加到聊天界面中。 **3)适配器(adapter)** &emsp;&emsp;适配器是一个服务端组件,负责将事件广播到所有或部分客户端。 &emsp;&emsp;当扩展到多个 Socket.IO 服务器时,需要集群部署时,就得将默认的内存适配器替换为另一种,例如 Redis、MongoDB 等。 &emsp;&emsp;这样做的目的,就是为了将事件正确路由到所有客户端。 &emsp;&emsp;在下图中,客户端触发事件后,经过适配器路由到集群的 Socket.IO 服务器中。 ![](https://img.kancloud.cn/02/99/029916463caa029ac8efd378e4349ef5_1134x702.png =800x) &emsp;&emsp;以 redis 为例,首先安装 @socket.io/redis-adapter 和 ioredis 库,前者在 v7 版本之前叫 socket.io-redis。 &emsp;&emsp;然后是改造服务端,客户端不用做调整,引入两个库。本机已安装 redis 环境,若未安装不知道会不会报错。 ~~~ const { Server } = require("socket.io"); const { createAdapter } = require("@socket.io/redis-adapter"); const { Cluster } = require("ioredis"); ~~~ &emsp;&emsp;接着连接 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 等。