mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 00:53:54 +08:00
624 lines
20 KiB
JavaScript
624 lines
20 KiB
JavaScript
// Description: MQTT+UDP 到 WebSocket 的桥接
|
|
// Author: terrence@tenclass.com
|
|
// Date: 2025-03-12
|
|
|
|
require('dotenv').config();
|
|
const net = require('net');
|
|
const debugModule = require('debug');
|
|
const debug = debugModule('mqtt-server');
|
|
const crypto = require('crypto');
|
|
const dgram = require('dgram');
|
|
const Emitter = require('events');
|
|
const WebSocket = require('ws');
|
|
const { MQTTProtocol } = require('./mqtt-protocol');
|
|
const { ConfigManager } = require('./utils/config-manager');
|
|
const { validateMqttCredentials } = require('./utils/mqtt_config_v2');
|
|
|
|
|
|
function setDebugEnabled(enabled) {
|
|
if (enabled) {
|
|
debugModule.enable('mqtt-server');
|
|
} else {
|
|
debugModule.disable();
|
|
}
|
|
}
|
|
|
|
const configManager = new ConfigManager('mqtt.json');
|
|
configManager.on('configChanged', (config) => {
|
|
setDebugEnabled(config.debug);
|
|
});
|
|
|
|
setDebugEnabled(configManager.get('debug'));
|
|
|
|
class WebSocketBridge extends Emitter {
|
|
constructor(connection, protocolVersion, macAddress, uuid, userData) {
|
|
super();
|
|
this.connection = connection;
|
|
this.macAddress = macAddress;
|
|
this.uuid = uuid;
|
|
this.userData = userData;
|
|
this.wsClient = null;
|
|
this.protocolVersion = protocolVersion;
|
|
this.deviceSaidGoodbye = false;
|
|
this.initializeChatServer();
|
|
}
|
|
|
|
initializeChatServer() {
|
|
const devMacAddresss = configManager.get('development')?.mac_addresss || [];
|
|
let chatServers;
|
|
if (devMacAddresss.includes(this.macAddress)) {
|
|
chatServers = configManager.get('development')?.chat_servers;
|
|
} else {
|
|
chatServers = configManager.get('production')?.chat_servers;
|
|
}
|
|
if (!chatServers) {
|
|
throw new Error(`未找到 ${this.macAddress} 的聊天服务器`);
|
|
}
|
|
this.chatServer = chatServers[Math.floor(Math.random() * chatServers.length)];
|
|
}
|
|
|
|
async connect(audio_params, features) {
|
|
return new Promise((resolve, reject) => {
|
|
const headers = {
|
|
'device-id': this.macAddress,
|
|
'protocol-version': '2',
|
|
'authorization': `Bearer test-token`
|
|
};
|
|
if (this.uuid) {
|
|
headers['client-id'] = this.uuid;
|
|
}
|
|
if (this.userData && this.userData.ip) {
|
|
headers['x-forwarded-for'] = this.userData.ip;
|
|
}
|
|
this.wsClient = new WebSocket(this.chatServer, { headers });
|
|
|
|
this.wsClient.on('open', () => {
|
|
this.sendJson({
|
|
type: 'hello',
|
|
version: 2,
|
|
transport: 'websocket',
|
|
audio_params,
|
|
features
|
|
});
|
|
});
|
|
|
|
this.wsClient.on('message', (data, isBinary) => {
|
|
if (isBinary) {
|
|
const timestamp = data.readUInt32BE(8);
|
|
const opusLength = data.readUInt32BE(12);
|
|
const opus = data.subarray(16, 16 + opusLength);
|
|
// 二进制数据通过UDP发送
|
|
this.connection.sendUdpMessage(opus, timestamp);
|
|
} else {
|
|
// JSON数据通过MQTT发送
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'hello') {
|
|
resolve(message);
|
|
} else {
|
|
this.connection.sendMqttMessage(JSON.stringify(message));
|
|
}
|
|
}
|
|
});
|
|
|
|
this.wsClient.on('error', (error) => {
|
|
console.error(`WebSocket error for device ${this.macAddress}:`, error);
|
|
this.emit('close');
|
|
reject(error);
|
|
});
|
|
|
|
this.wsClient.on('close', () => {
|
|
this.emit('close');
|
|
});
|
|
});
|
|
}
|
|
|
|
sendJson(message) {
|
|
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
|
|
this.wsClient.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
sendAudio(opus, timestamp) {
|
|
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
|
|
const buffer = Buffer.alloc(16 + opus.length);
|
|
buffer.writeUInt32BE(timestamp, 8);
|
|
buffer.writeUInt32BE(opus.length, 12);
|
|
buffer.set(opus, 16);
|
|
this.wsClient.send(buffer, { binary: true });
|
|
}
|
|
}
|
|
|
|
isAlive() {
|
|
return this.wsClient && this.wsClient.readyState === WebSocket.OPEN;
|
|
}
|
|
|
|
close() {
|
|
if (this.wsClient) {
|
|
this.wsClient.close();
|
|
this.wsClient = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const MacAddressRegex = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/;
|
|
|
|
/**
|
|
* MQTT连接类
|
|
* 负责应用层逻辑处理
|
|
*/
|
|
class MQTTConnection {
|
|
constructor(socket, connectionId, server) {
|
|
this.server = server;
|
|
this.connectionId = connectionId;
|
|
this.clientId = null;
|
|
this.username = null;
|
|
this.password = null;
|
|
this.bridge = null;
|
|
this.udp = {
|
|
remoteAddress: null,
|
|
cookie: null,
|
|
localSequence: 0,
|
|
remoteSequence: 0
|
|
};
|
|
this.headerBuffer = Buffer.alloc(16);
|
|
|
|
// 创建协议处理器,并传入socket
|
|
this.protocol = new MQTTProtocol(socket);
|
|
|
|
this.setupProtocolHandlers();
|
|
}
|
|
|
|
setupProtocolHandlers() {
|
|
// 设置协议事件处理
|
|
this.protocol.on('connect', (connectData) => {
|
|
this.handleConnect(connectData);
|
|
});
|
|
|
|
this.protocol.on('publish', (publishData) => {
|
|
this.handlePublish(publishData);
|
|
});
|
|
|
|
this.protocol.on('subscribe', (subscribeData) => {
|
|
this.handleSubscribe(subscribeData);
|
|
});
|
|
|
|
this.protocol.on('disconnect', () => {
|
|
this.handleDisconnect();
|
|
});
|
|
|
|
this.protocol.on('close', () => {
|
|
debug(`${this.clientId} 客户端断开连接`);
|
|
this.server.removeConnection(this);
|
|
});
|
|
|
|
this.protocol.on('error', (err) => {
|
|
debug(`${this.clientId} 连接错误:`, err);
|
|
this.close();
|
|
});
|
|
|
|
this.protocol.on('protocolError', (err) => {
|
|
debug(`${this.clientId} 协议错误:`, err);
|
|
this.close();
|
|
});
|
|
}
|
|
|
|
handleConnect(connectData) {
|
|
this.clientId = connectData.clientId;
|
|
this.username = connectData.username;
|
|
this.password = connectData.password;
|
|
|
|
debug('客户端连接:', {
|
|
clientId: this.clientId,
|
|
username: this.username,
|
|
password: this.password,
|
|
protocol: connectData.protocol,
|
|
protocolLevel: connectData.protocolLevel,
|
|
keepAlive: connectData.keepAlive
|
|
});
|
|
|
|
const parts = this.clientId.split('@@@');
|
|
if (parts.length === 3) { // GID_test@@@mac_address@@@uuid
|
|
try {
|
|
const validated = validateMqttCredentials(this.clientId, this.username, this.password);
|
|
this.groupId = validated.groupId;
|
|
this.macAddress = validated.macAddress;
|
|
this.uuid = validated.uuid;
|
|
this.userData = validated.userData;
|
|
} catch (error) {
|
|
debug('MQTT凭据验证失败:', error.message);
|
|
this.close();
|
|
return;
|
|
}
|
|
} else if (parts.length === 2) { // GID_test@@@mac_address
|
|
this.groupId = parts[0];
|
|
this.macAddress = parts[1].replace(/_/g, ':');
|
|
if (!MacAddressRegex.test(this.macAddress)) {
|
|
debug('无效的 macAddress:', this.macAddress);
|
|
this.close();
|
|
return;
|
|
}
|
|
} else {
|
|
debug('无效的 clientId:', this.clientId);
|
|
this.close();
|
|
return;
|
|
}
|
|
this.replyTo = `devices/p2p/${parts[1]}`;
|
|
|
|
this.server.addConnection(this);
|
|
}
|
|
|
|
handleSubscribe(subscribeData) {
|
|
debug('客户端订阅主题:', {
|
|
clientId: this.clientId,
|
|
topic: subscribeData.topic,
|
|
packetId: subscribeData.packetId
|
|
});
|
|
|
|
// 发送 SUBACK
|
|
this.protocol.sendSuback(subscribeData.packetId, 0);
|
|
}
|
|
|
|
handleDisconnect() {
|
|
debug('收到断开连接请求:', { clientId: this.clientId });
|
|
// 清理连接
|
|
this.server.removeConnection(this);
|
|
}
|
|
|
|
close() {
|
|
this.closing = true;
|
|
if (this.bridge) {
|
|
this.bridge.close();
|
|
this.bridge = null;
|
|
} else {
|
|
this.protocol.close();
|
|
}
|
|
}
|
|
|
|
checkKeepAlive() {
|
|
const now = Date.now();
|
|
const keepAliveInterval = this.protocol.getKeepAliveInterval();
|
|
|
|
// 如果keepAliveInterval为0,表示不需要心跳检查
|
|
if (keepAliveInterval === 0 || !this.protocol.isConnected) return;
|
|
|
|
const lastActivity = this.protocol.getLastActivity();
|
|
const timeSinceLastActivity = now - lastActivity;
|
|
|
|
// 如果超过心跳间隔,关闭连接
|
|
if (timeSinceLastActivity > keepAliveInterval) {
|
|
debug('心跳超时,关闭连接:', this.clientId);
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
handlePublish(publishData) {
|
|
debug('收到发布消息:', {
|
|
clientId: this.clientId,
|
|
topic: publishData.topic,
|
|
payload: publishData.payload,
|
|
qos: publishData.qos
|
|
});
|
|
|
|
if (publishData.qos !== 0) {
|
|
debug('不支持的 QoS 级别:', publishData.qos, '关闭连接');
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
const json = JSON.parse(publishData.payload);
|
|
if (json.type === 'hello') {
|
|
if (json.version !== 3) {
|
|
debug('不支持的协议版本:', json.version, '关闭连接');
|
|
this.close();
|
|
return;
|
|
}
|
|
this.parseHelloMessage(json).catch(error => {
|
|
debug('处理 hello 消息失败:', error);
|
|
this.close();
|
|
});
|
|
} else {
|
|
this.parseOtherMessage(json).catch(error => {
|
|
debug('处理其他消息失败:', error);
|
|
this.close();
|
|
});
|
|
}
|
|
}
|
|
|
|
sendMqttMessage(payload) {
|
|
debug(`发送消息到 ${this.replyTo}: ${payload}`);
|
|
this.protocol.sendPublish(this.replyTo, payload, 0, false, false);
|
|
}
|
|
|
|
sendUdpMessage(payload, timestamp) {
|
|
if (!this.udp.remoteAddress) {
|
|
debug(`设备 ${this.clientId} 未连接,无法发送 UDP 消息`);
|
|
return;
|
|
}
|
|
this.udp.localSequence++;
|
|
const header = this.generateUdpHeader(payload.length, timestamp, this.udp.localSequence);
|
|
const cipher = crypto.createCipheriv(this.udp.encryption, this.udp.key, header);
|
|
const message = Buffer.concat([header, cipher.update(payload), cipher.final()]);
|
|
this.server.sendUdpMessage(message, this.udp.remoteAddress);
|
|
}
|
|
|
|
generateUdpHeader(length, timestamp, sequence) {
|
|
// 重用预分配的缓冲区
|
|
this.headerBuffer.writeUInt8(1, 0);
|
|
this.headerBuffer.writeUInt16BE(length, 2);
|
|
this.headerBuffer.writeUInt32BE(this.connectionId, 4);
|
|
this.headerBuffer.writeUInt32BE(timestamp, 8);
|
|
this.headerBuffer.writeUInt32BE(sequence, 12);
|
|
return Buffer.from(this.headerBuffer); // 返回副本以避免并发问题
|
|
}
|
|
|
|
async parseHelloMessage(json) {
|
|
this.udp = {
|
|
...this.udp,
|
|
key: crypto.randomBytes(16),
|
|
nonce: this.generateUdpHeader(0, 0, 0),
|
|
encryption: 'aes-128-ctr',
|
|
remoteSequence: 0,
|
|
localSequence: 0,
|
|
startTime: Date.now()
|
|
}
|
|
|
|
if (this.bridge) {
|
|
debug(`${this.clientId} 收到重复 hello 消息,关闭之前的 bridge`);
|
|
this.bridge.close();
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
this.bridge = new WebSocketBridge(this, json.version, this.macAddress, this.uuid, this.userData);
|
|
this.bridge.on('close', () => {
|
|
const seconds = (Date.now() - this.udp.startTime) / 1000;
|
|
console.log(`通话结束: ${this.clientId} Session: ${this.udp.session_id} Duration: ${seconds}s`);
|
|
this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: this.udp.session_id }));
|
|
this.bridge = null;
|
|
if (this.closing) {
|
|
this.protocol.close();
|
|
}
|
|
});
|
|
|
|
try {
|
|
console.log(`通话开始: ${this.clientId} Protocol: ${json.version} ${this.bridge.chatServer}`);
|
|
const helloReply = await this.bridge.connect(json.audio_params, json.features);
|
|
this.udp.session_id = helloReply.session_id;
|
|
this.sendMqttMessage(JSON.stringify({
|
|
type: 'hello',
|
|
version: json.version,
|
|
session_id: this.udp.session_id,
|
|
transport: 'udp',
|
|
udp: {
|
|
server: this.server.publicIp,
|
|
port: this.server.udpPort,
|
|
encryption: this.udp.encryption,
|
|
key: this.udp.key.toString('hex'),
|
|
nonce: this.udp.nonce.toString('hex'),
|
|
},
|
|
audio_params: helloReply.audio_params
|
|
}));
|
|
} catch (error) {
|
|
this.sendMqttMessage(JSON.stringify({ type: 'error', message: '处理 hello 消息失败' }));
|
|
console.error(`${this.clientId} 处理 hello 消息失败: ${error}`);
|
|
}
|
|
}
|
|
|
|
async parseOtherMessage(json) {
|
|
if (!this.bridge) {
|
|
if (json.type !== 'goodbye') {
|
|
this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: json.session_id }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (json.type === 'goodbye') {
|
|
this.bridge.close();
|
|
this.bridge = null;
|
|
return;
|
|
}
|
|
|
|
this.bridge.sendJson(json);
|
|
}
|
|
|
|
onUdpMessage(rinfo, message, payloadLength, timestamp, sequence) {
|
|
if (!this.bridge) {
|
|
return;
|
|
}
|
|
if (this.udp.remoteAddress !== rinfo) {
|
|
this.udp.remoteAddress = rinfo;
|
|
}
|
|
if (sequence < this.udp.remoteSequence) {
|
|
return;
|
|
}
|
|
|
|
// 处理加密数据
|
|
const header = message.slice(0, 16);
|
|
const encryptedPayload = message.slice(16, 16 + payloadLength);
|
|
const cipher = crypto.createDecipheriv(this.udp.encryption, this.udp.key, header);
|
|
const payload = Buffer.concat([cipher.update(encryptedPayload), cipher.final()]);
|
|
|
|
this.bridge.sendAudio(payload, timestamp);
|
|
this.udp.remoteSequence = sequence;
|
|
}
|
|
|
|
isAlive() {
|
|
return this.bridge && this.bridge.isAlive();
|
|
}
|
|
}
|
|
|
|
class MQTTServer {
|
|
constructor() {
|
|
this.mqttPort = parseInt(process.env.MQTT_PORT) || 1883;
|
|
this.udpPort = parseInt(process.env.UDP_PORT) || this.mqttPort;
|
|
this.publicIp = process.env.PUBLIC_IP || 'mqtt.xiaozhi.me';
|
|
this.connections = new Map(); // clientId -> MQTTConnection
|
|
this.keepAliveTimer = null;
|
|
this.keepAliveCheckInterval = 1000; // 默认每1秒检查一次
|
|
|
|
this.headerBuffer = Buffer.alloc(16);
|
|
}
|
|
|
|
generateNewConnectionId() {
|
|
// 生成一个32位不重复的整数
|
|
let id;
|
|
do {
|
|
id = Math.floor(Math.random() * 0xFFFFFFFF);
|
|
} while (this.connections.has(id));
|
|
return id;
|
|
}
|
|
|
|
start() {
|
|
this.mqttServer = net.createServer((socket) => {
|
|
const connectionId = this.generateNewConnectionId();
|
|
debug(`新客户端连接: ${connectionId}`);
|
|
new MQTTConnection(socket, connectionId, this);
|
|
});
|
|
|
|
this.mqttServer.listen(this.mqttPort, () => {
|
|
console.warn(`MQTT 服务器正在监听端口 ${this.mqttPort}`);
|
|
});
|
|
|
|
|
|
this.udpServer = dgram.createSocket('udp4');
|
|
this.udpServer.on('message', this.onUdpMessage.bind(this));
|
|
this.udpServer.on('error', err => {
|
|
console.error('UDP 错误', err);
|
|
setTimeout(() => { process.exit(1); }, 1000);
|
|
});
|
|
this.udpServer.bind(this.udpPort, () => {
|
|
console.warn(`UDP 服务器正在监听 ${this.publicIp}:${this.udpPort}`);
|
|
});
|
|
|
|
// 启动全局心跳检查定时器
|
|
this.setupKeepAliveTimer();
|
|
}
|
|
|
|
/**
|
|
* 设置全局心跳检查定时器
|
|
*/
|
|
setupKeepAliveTimer() {
|
|
// 清除现有定时器
|
|
this.clearKeepAliveTimer();
|
|
this.lastConnectionCount = 0;
|
|
this.lastActiveConnectionCount = 0;
|
|
|
|
// 设置新的定时器
|
|
this.keepAliveTimer = setInterval(() => {
|
|
// 检查所有连接的心跳状态
|
|
for (const connection of this.connections.values()) {
|
|
connection.checkKeepAlive();
|
|
}
|
|
|
|
const activeCount = Array.from(this.connections.values()).filter(connection => connection.isAlive()).length;
|
|
if (activeCount !== this.lastActiveConnectionCount || this.connections.size !== this.lastConnectionCount) {
|
|
console.log(`连接数: ${this.connections.size}, 活跃数: ${activeCount}`);
|
|
this.lastActiveConnectionCount = activeCount;
|
|
this.lastConnectionCount = this.connections.size;
|
|
}
|
|
}, this.keepAliveCheckInterval);
|
|
}
|
|
|
|
/**
|
|
* 清除心跳检查定时器
|
|
*/
|
|
clearKeepAliveTimer() {
|
|
if (this.keepAliveTimer) {
|
|
clearInterval(this.keepAliveTimer);
|
|
this.keepAliveTimer = null;
|
|
}
|
|
}
|
|
|
|
addConnection(connection) {
|
|
// 检查是否已存在相同 clientId 的连接
|
|
for (const [key, value] of this.connections.entries()) {
|
|
if (value.clientId === connection.clientId) {
|
|
debug(`${connection.clientId} 已存在连接,关闭旧连接`);
|
|
value.close();
|
|
}
|
|
}
|
|
this.connections.set(connection.connectionId, connection);
|
|
}
|
|
|
|
removeConnection(connection) {
|
|
debug(`关闭连接: ${connection.connectionId}`);
|
|
if (this.connections.has(connection.connectionId)) {
|
|
this.connections.delete(connection.connectionId);
|
|
}
|
|
}
|
|
|
|
sendUdpMessage(message, remoteAddress) {
|
|
this.udpServer.send(message, remoteAddress.port, remoteAddress.address);
|
|
}
|
|
|
|
onUdpMessage(message, rinfo) {
|
|
// message format: [type: 1u, flag: 1u, payloadLength: 2u, cookie: 4u, timestamp: 4u, sequence: 4u, payload: n]
|
|
if (message.length < 16) {
|
|
console.warn('收到不完整的 UDP Header', rinfo);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const type = message.readUInt8(0);
|
|
if (type !== 1) return;
|
|
|
|
const payloadLength = message.readUInt16BE(2);
|
|
if (message.length < 16 + payloadLength) return;
|
|
|
|
const connectionId = message.readUInt32BE(4);
|
|
const connection = this.connections.get(connectionId);
|
|
if (!connection) return;
|
|
|
|
const timestamp = message.readUInt32BE(8);
|
|
const sequence = message.readUInt32BE(12);
|
|
|
|
connection.onUdpMessage(rinfo, message, payloadLength, timestamp, sequence);
|
|
} catch (error) {
|
|
console.error('UDP 消息处理错误:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停止服务器
|
|
*/
|
|
async stop() {
|
|
if (this.stopping) {
|
|
return;
|
|
}
|
|
this.stopping = true;
|
|
// 清除心跳检查定时器
|
|
this.clearKeepAliveTimer();
|
|
|
|
if (this.connections.size > 0) {
|
|
console.warn(`等待 ${this.connections.size} 个连接关闭`);
|
|
for (const connection of this.connections.values()) {
|
|
connection.close();
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
debug('等待连接关闭完成');
|
|
this.connections.clear();
|
|
}
|
|
|
|
if (this.udpServer) {
|
|
this.udpServer.close();
|
|
this.udpServer = null;
|
|
console.warn('UDP 服务器已停止');
|
|
}
|
|
|
|
// 关闭MQTT服务器
|
|
if (this.mqttServer) {
|
|
this.mqttServer.close();
|
|
this.mqttServer = null;
|
|
console.warn('MQTT 服务器已停止');
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// 创建并启动服务器
|
|
const server = new MQTTServer();
|
|
server.start();
|
|
process.on('SIGINT', () => {
|
|
console.warn('收到 SIGINT 信号,开始关闭');
|
|
server.stop();
|
|
});
|