socketio
yaofly2012 opened this issue · comments
Socket.IO
Socket.IO的能力
实现Socket.IO服务器和Socket.IO客户端之间双向通讯。基于websocket技术实现,并以HTTP长轮询(HTTP long-polling, 简称polling)作为兜底方案。
Socket.IO不是WebScoket的实现
Socket.IO不是WebScoket的实现!
Socket.IO不是WebScoket的实现!
Socket.IO不是WebScoket的实现!
重要的事情说三遍~~~
socket.io本质是一个实现双向通讯的库,只是在实现上依赖了websocket技术。弄清楚这一点很重要,明白这个就会更好的理解Socket.IO一些行为。
代码结构
Socket.IO客户端和服务端分别是两个库,并且各自又分为两层:
socket.io-client
和socket.io
提供顶层的API,供外部直接使用。- Engine.IO则负责底层的连接管理,如:传输方式,升级机制,离线检测等。让上层API不用关注数据是如何传输的。
传输方式(transports)
既然websocket是实现双向通讯的最佳方案,那为啥Socket.IO默认采用HTTP长轮询?
Socket.IO-client
代码片段:
_this.transports = opts.transports || ["polling", "websocket"];
理想很丰富,现实很骨感。websocket服务总是不可用?
Engine.IO 首先关注可靠性和用户体验,其次是潜在的用户体验改进和提高服务器性能。
Polling工作机制
客户端定时发起轮询,一个轮询包含一个GET请和一个POST请求:
- GET请求是为了获取服务端数据(模拟服务端向客户端“推送”数据)
- POST请求是把客户端数据传给服务端。
websocket工作机制
客户端请求query string
客户群请求时会拼接用户自定义的query string,还有一些内置的query string:
- EIO: EIO是Engine.IO缩写,表示Engine.IO的版本
- transport: 当前采用的传输类型
- t:时间戳hash
- sid: SessionId。在握手时由服务端生成,客户端后续请求都必须带上。
升级机制(Upgrade)
为什么要升级?
Socket.IO优先采用HTTP长轮询实现双向通讯,但毕竟webscoket技术双向通讯的最佳方案。条件合适时Socket.IO便升级采用websocket技术传输。
什么情况下会触发升级?
- 客户和服务端都支持webscoket协议(由客户端发起握手请求诊断)
- 客户端和服务端都确保传出缓冲区为空(即没有待通过HTTP方式传输的数据)
升级过程?
这个过程涉及两个“握手”:
- 第一个是Socket.IO里的概念,即Engne.IO建立连接的第一个HTTP请求。
- 第二个是webscoket里的概念,即请求升级协议的HTTP请求。
离线检测(心跳检测)
由服务端发起。
当采用Polling传输时怎么进行心跳检测的?只针对websocket方式么?毕竟Polling方式本身就是一种心跳检测。
服务端
概念:
- Server instance
- Socket instance:
一个Service instace管理多个socket instance? - engine
- namespace
- room
- handshake
- packet
- middleware
- sticky-session
namespace和room用了组织sockets?
Server instance
表示Socket服务。
import { Server } from "socket.io";
const io = new Server({ /* options */ });
Server和Namespcae什么关系啊,他们具有许多相同方法和事件。
Namespaces(命名空间)
Socket instance
负责和客户端进行交互,即通信
- 监听事件,
on
,once
- 向客户端发消息
emit
- 加入,离开room?
中间件
什么时候执行
客户端向服务端发送消息时?【不是】
中间件执行的时候连接还未建立
一个连接只执行一次
连接一致活着不用多次执行。这个跟Express等HTTP服务中间件不同。
客户端
每个WebSocket客户端是一个socket实例(Socket服务端对应着多个socket实例),并且每个socket实例都归属一个命名空间,默认是/
。
WebSocket客户端一般有三个主要操作:
- 主动向Socket服务请求连接;
- 想Socket服务器发消息;
- 监听事件。
概念:
- Socket instance
- Manager, Manager instance
- long-polling
- ping
生命周期
三大角色
- Socket实例:
客户端JS直接操作的API,负责和服务交互。和服务的建立连接则依赖Manager实例。 - Manager实例
管理Engine.IO客户端实例,主要负责重新连接的逻辑。一个Manager实例被多个Socket实例复用(多路复用)。 - Engine.IO
Socket实例
var socket = io();
- 默认Socket服务地址是当前域名+
/socket.io/
。 - 默认会自动连接服务器。
- 每次刷新页面socket实例
id
都会发生变化,即之前的会被disconnect,而重新建立连接。
注意:
- 虽然执行
io()
返回的是socket实例,但是这个函数其实会创建一个新的Mananger实例。
事件(events)
Socket
继承EventEmitter
,可以采用相关API进行发送,监听事件。
如何绑定事件
- 虽然运行在浏览器里,但是绑定事件方式跟DOM不一样哦。
DOM继承自EventTarget
),Socket
继承EventEmitter
,即同Nodejs环境绑定方式。 - 不要在
connect
回调函数里绑定其他事件。
因为Socket可能会断开连接,并自动重新建立连接。这会导致重复帮忙事件。
事件类型
- 内置事件,即Socket内置占用的事件,如
connect
,disconnect
等; - 自定义事件,即非内置事件。
connect
disconnect
- 当已存在连接断开后会触发
disconnect
事件。注意未建立连接前的断开不会触发改事件。 - 当主动(即socket客户端或者socket服务端调用
disconnect
方法)断开连接时socket instance不会自动出发重连。
参考
WebSocket
是什么
双工通讯协议,ws://
/wss://
,一般也叫有状态协议。
协议升级
websocket协议升级的请求和响应首部由协议升级机制相关首部和websocket协议特有的首部构成。
请求首部:
GET ws://xxxx HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: VUZh0hbu6SdzHvT5vQAFXA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
响应首部:
HTTP/1.1 101 Switching Protocols
Date: Thu, 24 Nov 2022 12:16:23 GMT
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 1VI4mAt55cMbKxpP4xYnqA05/Mo=
PK HTTP
都是应用层协议。参考
Namespaces(命名空间)
A Namespace is a communication channel that allows you to split the logic of your application over a single shared connection (also called "multiplexing").
Represents a pool of sockets connected under a given scope identified by a pathname.
字面翻译就是:
- 命名空间是一个通讯通道,可以在同一个连接上拆分应用逻辑。
- 表示一个特定范围下的已连接的Socket池。
一、有啥使用场景
命名空间是一个通讯通道,每个客户端的连接都属于且只属于一个命名空间,即客户端本质连接的是Socket服务命名空间。
这也要求:
- 命名空间必须在客户端请求连接前创建好,否则客户端就会抛
Invalid namespace
异常; - 客户端必须指定请求的命名空间(当然了默认是主命名空间)。如果指定错了,那相当于服务的没有创建命名空间(即也会抛
Invalid namespace
异常)。
主要用于代码逻辑拆分(是得代码更新模块化),一般基于两个方面拆分:
- 权限:不同权限的客户端的访问不同的命名空间;
- 业务功能:将独立的业务功能放在不同的命名空间下面,比如视频聊天和文字聊天分别放在
/videoChat
和/textChat
命名空间下面。
总结下就是:
- 命名空间用于组织代码,各个命名空间之间互相独立;
- 多个命名空间共享同一个连接(即多路复用),命名空间就是多路复用中“多路”;
- 命名空间管理着已连接的Socket。
二、主命名空间和自定义命名空间
2.1 主命名空间(Main Namespace)
Socket服务默认的命名空间,并且在创建Server
对象时就内部创建了,不用显示的创建。代码如下
this.sockets = this.of("/");
主命名空间和Server实例关系
从关系图上看可以看出一个Server实例对应多个命名空间,但是一个Server实例却有且只有一个主命名空间,并且Server实例提供一里一些直接操作主命名空间的方法和属性,比如:
io.sockets === io.of("/") // true
io.on("connection", (socket) => {}); // 等价 io.of("/").on("connection", (socket) => {});
io.use((socket, next) => { next() }); // 等价 io.of("/").use((socket, next) => { next() });
io.emit("hello"); // 等价 io.of("/").emit("hello");
详细的可以看Server
源码
2.2 自定义命名空间
io.of(nsp);
只能通过Server实例的of
方法创建自定义命名空间。
动态命名空间
2.3 主命名空间 PK 自定义命名空间总结
- 自定义命名空间必须显示的创建,且要在在客户端发起连接前创建,否则客户端无法连接服务(客户端会抛异常Invalid Namespace);
三、特性
3.1 互相独立
- 事件处理
- rooms
- 中间件
3.2 多路复用
- 同源下的不同名称命名空间会使用同一个websocket连接,即多路复用。
- 非同源命名空间或者创建相同名称的命名空间,则使用不同的websocket连接。
// 这三个命名空间会共用同一个连接
const socket = io("https://example.com"); // or io("https://example.com/"), the main namespace
const orderSocket = io("https://example.com/orders"); // the "orders" namespace
const userSocket = io("https://example.com/users"); // the "users" namespace
// 不同源会采用多个连接
const socket1 = io("https://first.example.com");
const socket2 = io("https://second.example.com"); // no multiplexing, two distinct WebSocket connections
// 同源同命的命名空间(本质是两个不同的Server服务),会采用多个连接
const socket1 = io();
const socket2 = io();
四、和Rooms
👇
Rooms
参考
Rooms
Within each Namespace, you can also define arbitrary channels (called room) that the Socket can join and leave. That provides a convenient way to broadcast to a group of
Sockets
- 属于具体某个命名空间,只是为了方便广播消息。
- 是个抽象的概念,为什么这样说呢?
Namespace实例里并没获取room的属性或者方法,相反却又关于sockets的属性。room只是为了方便广播消息给socket分组下。 - socket可以加入或者离开的一个任意渠道;
- socket可以
join
多个房间。
一、有啥使用场景
room用确定广播消息的范围,即决定哪些socket可以收到广播消息。这是服务端的概念,客户端只是一个单一的socket,并不知道自己属于哪个room。
二、PK 命名空间 ?
一定程度上讲两者都是用于“分组”socket实例。但具体两者的关注点不同:
- 命名空间用于通讯渠道(可用于权限控制等),分割代码逻辑;
- 房间更关注广播消息的范围;
- 房间是命名空间在广播消息维度的细分,也就是说Room属于一个特定的命名空间。
相似点
- 命名空间也可以划定广播的范围。
但这就没有room灵活了,room可以动态创建,而命名空间虽然也可以动态,但相对较弱,并且还需要提前创建。
选择Room还是命名空间 ?
- 主要从大的方面看看socket是否具有相同的权限,是否在处理同一个业务逻辑;
- 两者的能力是不同的,根据实际业务情况选择。
总结下: 两者本来就是不同的概念,职责不同。不要刻意去对比。
参考
多进程,多服务器问题
多进程/多服务器会带来两个问题:
- 长轮询transport时要粘性会话(Sticky Session)
长轮询机制下每个Socket会话客户端会发送多个HTTP请求(握手,拉取数据,提交数据,协议升级),但处理这些请求必须保证是同一个进程,否则就会报400错误; - 广播消息
触发广播消息在具体某个进程里,但客户端连接的socket服务可能是其他进程(或者其他服务器的进程),如何保证连接在非当前进程的客户端也能收到消息呢。
一、粘性会话解决方案
1.1 不用长轮询兜底
听着有点粗暴,但是如果不考虑老浏览器,只兼容现代浏览器(比如内部的管理系统)这倒是很不错的解决方案。
const socket = io("xxx", {
// 注意:这种情况下不会用长轮询作为兜底方案
transports: [ "websocket" ]
});
1.2 开启粘性会话
Socket IO官网提供了两种方式开启粘性会话。
- 根据cookie路由客户(推荐)
- 根据客户的原始地址路由客户
二、跨进程广播消息(同一个服务器里的不同进程)
2.1 Cluster多进程
采用官方提供的@socket.io/cluster-adapter
即可解决,具体参考文档Cluster适配器即可。
代码片段:
const cluster = require('node:cluster');
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
if (cluster.isPrimary) {
// xxx
setupPrimary();
} else {
const { Server } = require('socket.io');
const io = new Server();
// use the cluster adapter
io.adapter(createAdapter());
// XXXX
}
注意: 如果不用解决粘性会话问题,就不需要引入@socket.io/sticky
。
工作原理
- 触发广播的进程向Mater进程发个消息,
- 然后Master进程再向其他进程发消息,
- 最后其他进程再向客户端推送消息。
2.2 PM2多进程
现实中基本使用PM2管理nodejs进程。
采用@socket.io/pm2
参考文档结合PM2使用,本质上同Cluster方式,可以看看commit
但这个方式可行性弱些,一般服务器会预装官方PM2。
使用官方PM2
可以利用PM2 API实现进程间通讯。
代码片段:
pm2.launchBus(function(err, pm2_bus) {
// 每个进程都会收到消息(包含发送消息的进程)
pm2_bus.on('process:msg', function(packet) {
const { data } = packet;
const { msgType, msg } = data;
io.emit(msgType, msg);
})
});
io.on('connection', socket => {
// 广播
process.send({
type : 'process:msg',
data : {
msgType: 'joinus',
msg: `Hi all, someone from ${process.pid} join us`
}
});
});
三、跨服务器广播消息
3.1 发布/订阅机制(如 Redis)
在 worker里声明Redis订阅既处理夸服务器,也可以解决夸进程广播问题。
3.2 消息队列(如 RabbitMQ)
留意下多进程问题。如果同一个服务器消费消息只有一个进程会消费到消息,那还得结合夸进程消息广播。
参考
自定义adapter
socket.io-adapter
问题汇总
发布后socket接口很慢,本地倒没问题?!
原因没有处理粘性会话,Socket IO内部处理了粘性会话,但是性能很差。参考[Why is sticky-session required](https://socket.io/docs/v4/using-multiple-nodes/#why-is-sticky-session-required)有些浏览器不准许建立ws
连接
Modern browsers do not allow connecting to an unsecure server from a secure origin。
目前发现:Chrome 版本86.0.4240.75 不行, 但版本106.0.5249.119却可以。
解决方案:
- 采用降级方案,即HTTP轮询;
- 启动
wss
。
websocket连接时报400错误
原因:协议升级时头部参数不对,
解决方案:排查发现时Nginx反向代理配置需要增加协议升级相关配置。
参考协议升级机制,Socket.io 连接异常