fix: unify frontend and backend Socket.io protocol for WebSocket connection

This commit is contained in:
yczpf2019
2026-01-24 18:15:34 +08:00
parent caebc6595e
commit 49d6ec88f0
3 changed files with 288 additions and 260 deletions

110
README.md
View File

@@ -64,73 +64,87 @@ pm2 startup
### 4.2 使用 Nginx 反代 (推荐) ### 4.2 使用 Nginx 反代 (推荐)
#### A. 传统 Nginx 配置 (命令行) #### 步骤 1: 安装 Nginx 和 Certbot
如果你使用原生 Nginx请参考以下配置 ```bash
apt update && apt install -y nginx certbot python3-certbot-nginx
```
```nginx #### 步骤 2: 创建 Nginx 配置文件
```bash
cat > /etc/nginx/sites-available/mixly << 'EOF'
server { server {
listen 80; listen 80;
server_name 你的域名; server_name 你的域名;
return 301 https://$host$request_uri;
}
server { # 主站点反代
listen 443 ssl;
server_name 你的域名;
ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/key.pem;
# 核心配置:处理 WebSocket 和大文件上传
location / { location / {
proxy_pass https://127.0.0.1:7100; proxy_pass https://127.0.0.1:7100;
proxy_http_version 1.1; proxy_ssl_verify off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M; client_max_body_size 100M;
proxy_read_timeout 3600s;
} }
# 处理 Socket.io 特殊路径 # WebSocket 反代 (关键配置)
location /mixly-socket/ { location /mixly-socket/ {
proxy_pass https://127.0.0.1:7100; proxy_pass https://127.0.0.1:7100/socket.io/;
proxy_ssl_verify off;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
} }
} }
EOF
```
> **注意**:将 `你的域名` 替换为实际域名,如 `mixly.example.com`
#### 步骤 3: 启用配置并测试
```bash
# 创建软链接启用配置
ln -sf /etc/nginx/sites-available/mixly /etc/nginx/sites-enabled/
# 删除默认站点 (可选)
rm -f /etc/nginx/sites-enabled/default
# 测试配置语法
nginx -t
# 重启 Nginx
systemctl restart nginx
``` ```
#### B. Nginx Proxy Manager (NPM) 配置 (可视化) #### 步骤 4: 自动申请 SSL 证书 (Let's Encrypt)
如果你使用的是 NPM请按以下步骤配置 ```bash
# 将 your-email@example.com 替换为你的邮箱
certbot --nginx -d 你的域名 --non-interactive --agree-tos -m your-email@example.com
```
1. **Details 选项卡** Certbot 会自动修改 Nginx 配置并启用 HTTPS (443 端口)。
* **Domain Names**: `你的域名`
* **Scheme**: `https` #### 步骤 5: 设置证书自动续期
* **Forward Hostname/IP**: `127.0.0.1` ```bash
* **Forward Port**: `7100` # 测试自动续期
* **Websockets Support**: **必须勾选 (ON)** certbot renew --dry-run
2. **Custom Locations 选项卡**
* 点击 **Add Location**: # 证书默认每 90 天过期certbot 会自动配置定时任务续期
* **Define Location**: `/mixly-socket/` ```
* **Forward Scheme**: `https`
* **Forward Hostname/IP**: `127.0.0.1` #### Nginx 常用管理命令
* **Forward Port**: `7100` ```bash
* 点击内置的齿轮图标或进入 **Advanced**,确保有以下配置(通常勾选 Websockets Support 后 NPM 会自动处理,但建议检查): # 查看状态
```nginx systemctl status nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # 重载配置 (不中断服务)
``` systemctl reload nginx
3. **Advanced 选项卡** (可选)
* 建议添加以下配置以支持大文件上传并忽略自签名证书错误(重要!): # 查看错误日志
```nginx tail -f /var/log/nginx/error.log
client_max_body_size 100M; ```
# 忽略后端自签名证书错误 (必填,否则无法连接 wss)
proxy_ssl_verify off;
```
## 5. 跨平台特性说明 ## 5. 跨平台特性说明
@@ -163,3 +177,7 @@ pm2 reload mixly3
- **反向代理配置**:如果使用 Nginx 反代,请务必处理好的 WebSocket (Upgrade) 头,否则页面无法连接。 - **反向代理配置**:如果使用 Nginx 反代,请务必处理好的 WebSocket (Upgrade) 头,否则页面无法连接。
- **上传报错**:如果提示权限不足,请确认当前用户是否在 `dialout` 组,或尝试 `root` 运行(不推荐)。 - **上传报错**:如果提示权限不足,请确认当前用户是否在 `dialout` 组,或尝试 `root` 运行(不推荐)。
- **Python 命令**:系统必须能识别 `python3` 命令。 - **Python 命令**:系统必须能识别 `python3` 命令。
- **没有“添加设备”按钮**:这是因为 WebSocket 连接失败。
1. 检查 NPM 中是否勾选了 **Websockets Support**
2. 检查 NPM Advanced 配置中是否添加了 `proxy_ssl_verify off;` (关键)。
3. 尝试重启服务端:`pm2 reload mixly3`

View File

@@ -129,6 +129,7 @@
{ {
"path": "/web-socket/socket.js", "path": "/web-socket/socket.js",
"require": [ "require": [
"io",
"Mixly.Env", "Mixly.Env",
"Mixly.Config", "Mixly.Config",
"Mixly.MJson", "Mixly.MJson",
@@ -140,4 +141,4 @@
"Mixly.WebSocket.Socket" "Mixly.WebSocket.Socket"
] ]
} }
] ]

View File

@@ -1,244 +1,253 @@
(() => { (() => {
goog.require('Mixly.Env'); goog.require('Mixly.Env');
goog.require('Mixly.Config'); goog.require('Mixly.Config');
goog.require('Mixly.MJson'); goog.require('Mixly.MJson');
goog.require('Mixly.WebSocket'); goog.require('Mixly.WebSocket');
goog.require('Mixly.LayerExt'); goog.require('Mixly.LayerExt');
goog.require('Mixly.Command'); goog.require('Mixly.Command');
goog.provide('Mixly.WebSocket.Socket'); goog.provide('Mixly.WebSocket.Socket');
const { const {
Env, Env,
Config, Config,
MJson, MJson,
LayerExt, LayerExt,
Command Command
} = Mixly; } = Mixly;
const { SOFTWARE } = Config; const { SOFTWARE } = Config;
const { Socket } = Mixly.WebSocket; const { Socket } = Mixly.WebSocket;
Socket.obj = null; Socket.obj = null;
Socket.url = 'ws://127.0.0.1/socket'; Socket.url = '';
Socket.jsonArr = []; Socket.jsonArr = [];
Socket.connected = false; Socket.connected = false;
Socket.initFunc = null; Socket.initFunc = null;
Socket.debug = SOFTWARE.debug; Socket.debug = SOFTWARE.debug;
let { hostname, protocol, port } = window.location; Socket.disconnectTimes = 0;
if (protocol === 'http:') { Socket.updating = false;
Socket.protocol = 'ws:';
} else {
Socket.protocol = 'wss:';
}
if (port) {
port = ':' + port;
}
Socket.url = Socket.protocol + '//' + hostname + port + '/socket';
Socket.IPAddress = hostname;
Socket.disconnectTimes = 0;
Socket.updating = false;
// 构建 Socket.io 连接 URL (和后端统一使用 Socket.io)
let lockReconnect = false; // 避免重复连接 let { hostname, protocol, port } = window.location;
let timeoutFlag = true; if (protocol === 'http:') {
let timeoutSet = null; Socket.protocol = 'ws:';
let reconectNum = 0; } else {
const timeout = 5000; // 超时重连间隔 Socket.protocol = 'wss:';
function reconnect () {
if (lockReconnect) return;
lockReconnect = true;
// 没连接上会一直重连,设置延迟避免请求过多
setTimeout(function () {
timeoutFlag = true;
Socket.init();
console.info(`正在重连第${reconectNum + 1}`);
reconectNum++;
lockReconnect = false;
}, timeout); // 这里设置重连间隔(ms)
}
//心跳检测
const heartCheck = {
timeout, // 毫秒
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearInterval(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
const self = this;
let count = 0;
let WS = Socket;
this.timeoutObj = setInterval(() => {
if (count < 3) {
if (WS.obj.readyState === 1) {
WS.obj.send('HeartBeat');
console.info(`HeartBeat第${count + 1}`);
}
count++;
} else {
clearInterval(this.timeoutObj);
count = 0;
if (WS.obj.readyState === 0 && WS.obj.readyState === 1) {
WS.obj.close();
}
}
}, self.timeout);
} }
} Socket.url = Socket.protocol + '//' + hostname + (port ? ':' + port : '');
Socket.IPAddress = hostname;
Socket.init = (onopenFunc = (data) => {}, doFunc = () => {}) => { let lockReconnect = false;
if (Socket.connected) { let timeoutFlag = true;
if (Socket.initFunc) { let timeoutSet = null;
Socket.initFunc(); let reconectNum = 0;
Socket.initFunc = null; const timeout = 5000;
}
doFunc();
return;
}
timeoutSet = setTimeout(() => { function reconnect() {
if (timeoutFlag && reconectNum < 3) { if (lockReconnect) return;
console.info(`重连`); lockReconnect = true;
reconectNum++; setTimeout(function () {
timeoutFlag = true;
Socket.init(); Socket.init();
console.info(`正在重连第${reconectNum + 1}`);
reconectNum++;
lockReconnect = false;
}, timeout);
}
// 心跳检测
const heartCheck = {
timeout,
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
clearInterval(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function () {
const self = this;
let count = 0;
this.timeoutObj = setInterval(() => {
if (count < 3) {
if (Socket.connected) {
// Socket.io 自带心跳,这里可以发送自定义心跳
console.info(`HeartBeat第${count + 1}`);
}
count++;
} else {
clearInterval(this.timeoutObj);
count = 0;
if (Socket.obj && !Socket.connected) {
Socket.obj.disconnect();
}
}
}, self.timeout);
} }
}, timeout); }
let WS = Socket; Socket.init = (onopenFunc = (data) => { }, doFunc = () => { }) => {
WS.obj = new WebSocket(WS.url); if (Socket.connected) {
WS.obj.onopen = () => { if (Socket.initFunc) {
console.log('已连接' + WS.url); Socket.initFunc();
WS.connected = true; Socket.initFunc = null;
Socket.initFunc = doFunc; }
reconectNum = 0; doFunc();
timeoutFlag = false; return;
clearTimeout(timeoutSet);
heartCheck.reset().start();
onopenFunc(WS);
Socket.reload();
if (Socket.updating) {
Socket.updating = false;
} }
};
WS.obj.onmessage = (event) => { timeoutSet = setTimeout(() => {
heartCheck.reset().start(); if (timeoutFlag && reconectNum < 3) {
let command = Command.parse(event.data); console.info(`重连`);
command = MJson.decode(command); reconectNum++;
if (Socket.debug) Socket.init();
console.log('receive -> ', event.data); }
Command.run(command); }, timeout);
};
WS.obj.onerror = (event) => { // 使用 Socket.io 客户端连接(和后端统一)
console.log('WebSocket error: ', event); Socket.obj = io(`${Socket.url}/all`, {
reconnect(); path: '/mixly-socket/',
}; reconnection: true,
reconnectionDelayMax: 10000,
transports: ['websocket']
});
WS.obj.onclose = (event) => { Socket.obj.on('connect', () => {
WS.connected = false; console.log('已连接' + Socket.url);
WS.disconnectTimes += 1; Socket.connected = true;
if (WS.disconnectTimes > 255) { Socket.initFunc = doFunc;
WS.disconnectTimes = 1; reconectNum = 0;
}
console.log('已断开' + WS.url);
console.info(`关闭`, event.code);
if (event.code !== 1000) {
timeoutFlag = false; timeoutFlag = false;
clearTimeout(timeoutSet); clearTimeout(timeoutSet);
heartCheck.reset().start();
onopenFunc(Socket);
Socket.reload();
if (Socket.updating) {
Socket.updating = false;
}
});
// Socket.io 消息接收
Socket.obj.onAny((eventName, ...args) => {
heartCheck.reset().start();
// 构造兼容原有 Command 格式的消息
const command = { event: eventName, data: args };
if (Socket.debug) {
console.log('receive -> ', eventName, args);
}
// 尝试使用原有 Command 系统处理
try {
Command.run(MJson.decode(command));
} catch (e) {
// 如果 Command 系统不能处理,忽略
}
});
Socket.obj.on('connect_error', (error) => {
console.log('WebSocket error: ', error);
reconnect(); reconnect();
} else { });
clearInterval(heartCheck.timeoutObj);
clearTimeout(heartCheck.serverTimeoutObj);
}
}
}
Socket.sendCommand = (command) => { Socket.obj.on('disconnect', (reason) => {
let WS = Mixly.WebSocket.Socket; Socket.connected = false;
if (!WS.connected) { Socket.disconnectTimes += 1;
layer.msg('未连接' + WS.url, {time: 1000}); if (Socket.disconnectTimes > 255) {
return; Socket.disconnectTimes = 1;
} }
let commandStr = ''; console.log('已断开' + Socket.url);
console.info(`关闭`, reason);
try {
commandStr = JSON.stringify(MJson.encode(command));
if (Socket.debug)
console.log('send -> ', commandStr);
} catch (e) {
console.log(e);
return;
}
WS.obj.send(commandStr);
}
Socket.clickConnect = () => { if (reason !== 'io client disconnect') {
if (Socket.connected) { timeoutFlag = false;
Socket.disconnect(); clearTimeout(timeoutSet);
} else { reconnect();
Socket.connect((WS) => { } else {
layer.closeAll(); clearInterval(heartCheck.timeoutObj);
layer.msg(WS.url + '连接成功', { time: 1000 }); clearTimeout(heartCheck.serverTimeoutObj);
}
}); });
} }
}
Socket.openLoadingBox = (title, successFunc = () => {}, endFunc = () => {}) => { Socket.sendCommand = (command) => {
layer.open({ if (!Socket.connected) {
type: 1, layer.msg('未连接' + Socket.url, { time: 1000 });
title: title, return;
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_ALL,
closeBtn: 0,
success: function () {
$("#webusb-cancel").css("display","none");
$(".layui-layer-page").css("z-index", "198910151");
successFunc();
},
end: function () {
$("#mixly-loader-div").css("display", "none");
$(".layui-layer-shade").remove();
$("#webusb-cancel").css("display", "unset");
if (Socket.connected)
endFunc();
} }
});
}
Socket.connect = (onopenFunc = (data) => {}, doFunc = () => {}) => { try {
if (Socket.connected) { const encodedCommand = MJson.encode(command);
doFunc(); if (Socket.debug) {
return; console.log('send -> ', encodedCommand);
}
// 使用 Socket.io emit 发送命令
Socket.obj.emit('command', encodedCommand);
} catch (e) {
console.log(e);
return;
}
} }
let title = '连接中...';
Socket.openLoadingBox(title, () => {
setTimeout(() => {
Socket.init(onopenFunc);
}, 1000);
}, doFunc);
}
Socket.disconnect = () => { Socket.clickConnect = () => {
if (!Socket.connected) if (Socket.connected) {
return; Socket.disconnect();
let title = '断开中...'; } else {
Socket.openLoadingBox(title, () => { Socket.connect((WS) => {
Socket.obj.close(); layer.closeAll();
}); layer.msg(WS.url + '连接成功', { time: 1000 });
} });
}
Socket.reload = () => { }
if (!Socket.updating && Socket.disconnectTimes) {
window.location.reload(); Socket.openLoadingBox = (title, successFunc = () => { }, endFunc = () => { }) => {
layer.open({
type: 1,
title: title,
content: $('#mixly-loader-div'),
shade: LayerExt.SHADE_ALL,
closeBtn: 0,
success: function () {
$("#webusb-cancel").css("display", "none");
$(".layui-layer-page").css("z-index", "198910151");
successFunc();
},
end: function () {
$("#mixly-loader-div").css("display", "none");
$(".layui-layer-shade").remove();
$("#webusb-cancel").css("display", "unset");
if (Socket.connected)
endFunc();
}
});
}
Socket.connect = (onopenFunc = (data) => { }, doFunc = () => { }) => {
if (Socket.connected) {
doFunc();
return;
}
let title = '连接中...';
Socket.openLoadingBox(title, () => {
setTimeout(() => {
Socket.init(onopenFunc);
}, 1000);
}, doFunc);
}
Socket.disconnect = () => {
if (!Socket.connected)
return;
let title = '断开中...';
Socket.openLoadingBox(title, () => {
Socket.obj.disconnect();
});
}
Socket.reload = () => {
if (!Socket.updating && Socket.disconnectTimes) {
window.location.reload();
}
} }
}
})(); })();