- Added Socket.IO dependency to enable real-time communication. - Initialized Socket.IO in the server and bound it to the HTTP server. - Implemented functionality to allow clients to join device channels using KV tokens. - Added endpoints to retrieve online devices and broadcast key changes. - Enhanced existing routes to include device names in responses. - Implemented broadcasting of key changes for KV operations. - Updated documentation to reflect the new Socket.IO integration and usage.
12 KiB
Socket.IO 实时频道接口文档(前端)
概述
ClassworksKV 提供基于 Socket.IO 的实时键值变更通知服务。前端使用 KV token(应用安装 token)加入频道,服务端会自动将 token 映射到对应设备的 uuid 房间。同一设备的不同 token 会被归入同一频道,因此多个客户端/应用可以共享实时更新。
重要变更:不再支持直接使用 uuid 加入频道,所有连接必须使用有效的 KV token。
安装依赖
前端项目安装 Socket.IO 客户端:
# npm
npm install socket.io-client
# pnpm
pnpm add socket.io-client
# yarn
yarn add socket.io-client
连接服务器
基础连接
import { io, Socket } from 'socket.io-client';
const SERVER_URL = 'http://localhost:3000'; // 替换为实际服务器地址
const socket: Socket = io(SERVER_URL, {
transports: ['websocket'],
});
连接时自动加入频道(推荐)
在连接握手时通过 query 参数传入 token,自动加入对应设备频道:
const socket = io(SERVER_URL, {
transports: ['websocket'],
query: {
token: '<your-kv-app-token>', // 或使用 apptoken 参数
},
});
// 监听加入成功
socket.on('joined', (info) => {
console.log('已加入频道:', info);
// { by: 'token', uuid: 'device-uuid-xxx' }
});
// 监听加入失败
socket.on('join-error', (error) => {
console.error('加入频道失败:', error);
// { by: 'token', reason: 'invalid_token' }
});
事件接口
1. 客户端发送的事件
join-token - 使用 token 加入频道
连接后按需加入频道。
载荷格式:
{
token?: string; // KV token(二选一)
apptoken?: string; // 或使用 apptoken 字段
}
示例:
socket.emit('join-token', { token: '<your-kv-app-token>' });
leave-token - 使用 token 离开频道
离开指定 token 对应的设备频道。
载荷格式:
{
token?: string;
apptoken?: string;
}
示例:
socket.emit('leave-token', { token: '<your-kv-app-token>' });
leave-all - 离开所有频道
断开前清理,离开该连接加入的所有设备频道。
载荷: 无
示例:
socket.emit('leave-all');
2. 服务端发送的事件
joined - 加入成功通知
当成功加入频道后,服务端会发送此事件。
载荷格式:
{
by: 'token';
uuid: string; // 设备 uuid(用于调试/日志)
}
示例:
socket.on('joined', (info) => {
console.log(`成功加入设备 ${info.uuid} 的频道`);
});
join-error - 加入失败通知
token 无效或查询失败时触发。
载荷格式:
{
by: 'token';
reason: 'invalid_token'; // 失败原因
}
示例:
socket.on('join-error', (error) => {
console.error('Token 无效,无法加入频道');
});
kv-key-changed - 键值变更广播
当设备下的 KV 键被创建/更新/删除时,向该设备频道内所有连接广播此事件。
载荷格式:
{
uuid: string; // 设备 uuid
key: string; // 变更的键名
action: 'upsert' | 'delete'; // 操作类型
// 仅 action='upsert' 时存在:
created?: boolean; // 是否首次创建
updatedAt?: string; // 更新时间(ISO 8601)
batch?: boolean; // 是否为批量导入中的单条
// 仅 action='delete' 时存在:
deletedAt?: string; // 删除时间(ISO 8601)
}
示例:
socket.on('kv-key-changed', (msg) => {
if (msg.action === 'upsert') {
console.log(`键 ${msg.key} 已${msg.created ? '创建' : '更新'}`);
// 刷新本地缓存或重新获取数据
} else if (msg.action === 'delete') {
console.log(`键 ${msg.key} 已删除`);
// 从本地缓存移除
}
});
载荷示例:
-
新建/更新键:
{ "uuid": "device-001", "key": "settings/theme", "action": "upsert", "created": false, "updatedAt": "2025-10-25T08:30:00.000Z" } -
删除键:
{ "uuid": "device-001", "key": "settings/theme", "action": "delete", "deletedAt": "2025-10-25T08:35:00.000Z" } -
批量导入中的单条:
{ "uuid": "device-001", "key": "config/version", "action": "upsert", "created": true, "updatedAt": "2025-10-25T08:40:00.000Z", "batch": true }
device-joined - 设备频道连接数变化(可选)
当有新连接加入某设备频道时广播,用于显示在线人数。
载荷格式:
{
uuid: string; // 设备 uuid
connections: number; // 当前连接数
}
示例:
socket.on('device-joined', (info) => {
console.log(`设备 ${info.uuid} 当前有 ${info.connections} 个连接`);
});
完整使用示例
React Hook 封装
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
interface KvKeyChange {
uuid: string;
key: string;
action: 'upsert' | 'delete';
created?: boolean;
updatedAt?: string;
deletedAt?: string;
batch?: boolean;
}
export function useKvChannel(
token: string | null,
onKeyChanged?: (event: KvKeyChange) => void
) {
const socketRef = useRef<Socket | null>(null);
useEffect(() => {
if (!token) return;
// 创建连接并加入频道
const socket = io(SERVER_URL, {
transports: ['websocket'],
query: { token },
});
socket.on('joined', (info) => {
console.log('已加入设备频道:', info.uuid);
});
socket.on('join-error', (err) => {
console.error('加入频道失败:', err.reason);
});
socket.on('kv-key-changed', (msg: KvKeyChange) => {
onKeyChanged?.(msg);
});
socketRef.current = socket;
return () => {
socket.emit('leave-all');
socket.close();
};
}, [token]);
return socketRef.current;
}
Vue Composable 封装
import { ref, watch, onUnmounted } from 'vue';
import { io, Socket } from 'socket.io-client';
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3000';
export function useKvChannel(token: Ref<string | null>) {
const socket = ref<Socket | null>(null);
const isConnected = ref(false);
const deviceUuid = ref<string | null>(null);
watch(token, (newToken) => {
// 清理旧连接
if (socket.value) {
socket.value.emit('leave-all');
socket.value.close();
socket.value = null;
}
if (!newToken) return;
// 创建新连接
const s = io(SERVER_URL, {
transports: ['websocket'],
query: { token: newToken },
});
s.on('connect', () => {
isConnected.value = true;
});
s.on('disconnect', () => {
isConnected.value = false;
});
s.on('joined', (info) => {
deviceUuid.value = info.uuid;
console.log('已加入设备频道:', info.uuid);
});
s.on('join-error', (err) => {
console.error('加入失败:', err.reason);
});
socket.value = s;
}, { immediate: true });
onUnmounted(() => {
if (socket.value) {
socket.value.emit('leave-all');
socket.value.close();
}
});
return { socket, isConnected, deviceUuid };
}
使用示例(React)
import { useKvChannel } from './hooks/useKvChannel';
function MyComponent() {
const token = localStorage.getItem('kv-token');
useKvChannel(token, (event) => {
console.log('KV 变更:', event);
if (event.action === 'upsert') {
// 更新本地状态或重新获取数据
fetchKeyValue(event.key);
} else if (event.action === 'delete') {
// 从本地移除
removeFromCache(event.key);
}
});
return <div>实时监听中...</div>;
}
REST API:查询在线设备
除了 Socket.IO 实时事件,还提供 HTTP 接口查询当前在线设备列表。
GET /devices/online
响应格式:
{
success: true;
devices: Array<{
uuid: string; // 设备 uuid
connections: number; // 当前连接数
name: string | null; // 设备名称(若已设置)
}>;
}
示例:
const response = await fetch(`${SERVER_URL}/devices/online`);
const data = await response.json();
console.log('在线设备:', data.devices);
// [{ uuid: 'device-001', connections: 3, name: 'My Device' }, ...]
获取 KV Token
前端需要先获取有效的 KV token 才能加入频道。Token 通过以下接口获取:
安装应用获取 token
接口: POST /apps/devices/:uuid/install/:appId
认证: 需要设备 UUID 认证(密码或账户 JWT)
响应包含:
{
id: string;
appId: string;
token: string; // 用于 KV 操作和加入频道
note: string | null;
name: string | null; // 等同于 note,便于展示
installedAt: string;
}
列出设备已有的 token
接口: GET /apps/tokens?uuid=<device-uuid>
响应:
{
success: true;
tokens: Array<{
id: string;
token: string;
appId: string;
installedAt: string;
note: string | null;
name: string | null; // 等同于 note
}>;
deviceUuid: string;
}
注意事项与最佳实践
-
Token 必需:所有连接必须提供有效的 KV token,不再支持直接使用 uuid。
-
频道归并:同一设备的不同 token 会自动归入同一房间(以设备 uuid 为房间名),因此多个应用/客户端可以共享实时更新。
-
连接管理:
- 组件卸载时调用
leave-all或leave-token清理连接 - 避免频繁创建/销毁连接,建议在应用全局维护单个 socket 实例
- 组件卸载时调用
-
重连处理:
- Socket.IO 客户端内置自动重连
- 在
connect事件后重新 emitjoin-token确保重连后仍在频道内(或在握手时传 token 自动加入)
-
CORS 配置:
- 服务端通过环境变量
FRONTEND_URL控制允许的来源 - 未设置时默认为
*(允许所有来源) - 生产环境建议设置为前端实际域名
- 服务端通过环境变量
-
错误处理:
- 监听
join-error事件处理 token 无效情况 - 监听
connect_error处理网络连接失败
- 监听
-
性能优化:
- 批量导入时会逐条广播,前端可根据
batch: true标记做去抖处理 - 建议在本地维护 KV 缓存,收到变更通知时增量更新而非全量刷新
- 批量导入时会逐条广播,前端可根据
环境变量配置
服务端需要配置以下环境变量:
# Socket.IO CORS 允许的来源
FRONTEND_URL=http://localhost:5173
# 服务器端口(可选,默认 3000)
PORT=3000
常见问题
Q: 如何支持多个设备?
A: 对每个设备的 token 分别调用 join-token,或在连接时传入一个 token,后续通过事件加入其他设备。
socket.emit('join-token', { token: token1 });
socket.emit('join-token', { token: token2 });
Q: 广播延迟有多大?
A: 通常在毫秒级,取决于网络状况。WebSocket 连接建立后,广播几乎实时。
Q: Token 过期怎么办?
A: Token 本身不会过期,除非手动删除应用安装记录。如收到 join-error,检查 token 是否已被卸载。
Q: 可以在 Node.js 后端使用吗?
A: 可以,使用相同的 socket.io-client 包,接口完全一致。
更新日志
v1.1.0 (2025-10-25)
破坏性变更:
- 移除直接使用 uuid 加入频道的接口(
join-device/leave-device) - 现在必须使用 KV token 通过
join-token或握手 query 加入
新增:
leave-all事件:离开所有已加入的频道- 握手时支持
token和apptoken两种参数名
改进:
- 同一设备的不同 token 自动归入同一房间
- 优化在线设备计数准确性
技术支持
如有问题,请查阅:
- 服务端源码:
utils/socket.js - KV 路由:
routes/kv-token.js - 设备管理:
routes/device.js
或提交 Issue 到项目仓库。