Node.js精进(11)——Socket.IO

虚幻大学 xuhss 176℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

  Socket.IO 是一个建立在 WebSocket 协议之上的库,可以在客户端和服务器之间实现低延迟、双向和基于事件的通信。

  851238f7bdecbaaf77db57a4dd5a7af8 - Node.js精进(11)——Socket.IO

  并且提供额外的保证,例如回退到 HTTP 长轮询、自动重连、数据包缓冲、多路复用等。

  WebSocket 是一种基于 TCP 协议在服务器和浏览器之间提供全双工和低延迟通道的通信协议。

  注意,Socket.IO 不是 WebSocket 的实现。尽管 Socket.IO 确实在可能的情况下使用 WebSocket 进行传输,但它为每个数据包添加了额外的元数据。

  这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通的 WebSocket 服务器。

  如果需要一个普通的 WebSocket 服务器,可以使用 wsµWebSockets.js

  在 Socket.IO 的底层依赖 Engine.IO 引擎,它是跨浏览器/跨设备双向通信层的实现,可处理各种传输、升级机制和断线检测等。

  刚刚所说的自动重连、数据包缓冲、多路复用等附加功能都是 Engine.IO 引擎提供的能力。

  本系列所有的示例源码都已上传至Github,点击此处获取。

一、广播

  现在来建立一个提供表单和消息列表的简单 HTML 网页,用 Socket.IO 广播消息(如下图所示),并且可以在页面中呈现消息内容。

  ba6ee8e5815ffd0c503b838f98d6d6ce - Node.js精进(11)——Socket.IO

1) HTTP 服务器

  首先安装 socket.io 包:npm install socket.io。

  然后创建一个 HTTP 服务器,用于接收 HTML 和 JavaScript 文件的请求,内部实现了个简单的路由。

  其中 fs.readFileSync() 同步读取到的。

  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 样式,在表单中有一个按钮和文本框,如下图所示。

DOCTYPE html>
<html>
 <head>
 <title>Socket.IO broadcasttitle>
 <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>Sendbutton>
 form>
 <script src="../socket.io.js">script>
 body>
html>

  876dbfff2faae19cebb497843ef2e962 - Node.js精进(11)——Socket.IO

  在页面的内嵌脚本中,先初始化 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 = "";
 }
});

  20e7b8eb85f367cdd8d86a4d04d3f6c1 - Node.js精进(11)——Socket.IO

  最后注册 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);
});

  5522b58078e276c8245f8c3ba72bfe5d - Node.js精进(11)——Socket.IO

  客户端中的 socket 实例有 3 个保留事件,connect、connect_error 和 disconnect。

  其中 connect_error 会在底层连接失败或中间件拒绝连接时触发。

  下面是一张客户端 socket 连接的生命周期图,在建立连接时会分两种情况,在断开连接时还会自动重连。

  eb7f22e0bd0012fa01debba4c1d9f337 - Node.js精进(11)——Socket.IO

4)请求头和消息

  下图是一个请求头,状态码是 101 表示可以使用新协议,Connection: Upgrade 指示这是一个升级请求,Upgrade 指定 websocket 协议。

  一旦这次升级完成后,连接就变成了双向管道。sid 参数表示一个会话 ID。

  8f37428bbc955045796843b8190e3daf - Node.js精进(11)——Socket.IO

  下图一系列的消息,每条消息的开头都是 1 到 2 个数字,它们都有各自的含义。

  第四条是发送的消息,第五条是接收的消息。

  72de264f96e67e88061168dc2b6adfad - Node.js精进(11)——Socket.IO

  第一个数字是 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)

  命名空间是一种通信通道,允许通过单个共享连接拆分应用程序的逻辑,即多路复用,适合一台服务器提供多条不同长连接业务的场景。

  如下图所示,分配了两个命名空间,通过一条管道连接了客户端和服务器。

  398c10666ee1aabcf280639d9086310a - Node.js精进(11)——Socket.IO

  在服务端,注册 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);
 });
});

  23b7e639c13213716d81f0d7f4392fc2 - Node.js精进(11)——Socket.IO

  socket.to() 的效果其实就是这条消息不会让自己收到,与 io.to() 的区别是后者可以让自己也收到。

  不过在调试的时候,调用 io.to() 后,不知为何,客户端都收不到消息。

  在做即时通信的项目时,采用 socket.to() 更合适,自己发送的消息完全可以通过脚本添加到聊天界面中。

3)适配器(adapter)

  适配器是一个服务端组件,负责将事件广播到所有或部分客户端。

  当扩展到多个 Socket.IO 服务器时,需要集群部署时,就得将默认的内存适配器替换为另一种,例如 Redis、MongoDB 等。

  这样做的目的,就是为了将事件正确路由到所有客户端。

  在下图中,客户端触发事件后,经过适配器路由到集群的 Socket.IO 服务器中。

  d40541302f8fb9fdcc3fda14df737023 - Node.js精进(11)——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 实现一对一即时聊天

socket.io官方文档中文版

基于socket.io构建即时通讯应用

socket.io namespaces and rooms (译)

Socket.io源码分析

转载请注明:xuhss » Node.js精进(11)——Socket.IO

喜欢 (0)

您必须 登录 才能发表评论!