Rabbitmq WebSocket 自动断开问题的解决方案

博客讲述使用stompJS从web连接RabbitMQ时出现连接丢失报错。指出连接时设置心跳包参数很关键,填0或数值太大都不行,应在服务器主动断开前发送心跳包,且时间不超服务器默认自动断开时长,默认约10秒。

关于报错,Whoops! Lost connection to ws://XXX.XXX.XXX.XXX:15684/ws
在玩rabbitMQ时候,用stompJS从web连接ranbbitMQ时,报了标题的错误消息!

我把我这个html页面代码贴上

最主要的是这两个参数

	client.heartbeat.outgoing = 8000; // 毫秒
	client.heartbeat.incoming = 8000;// 毫秒

这两个参数, 填0是不行的, 0代表永不发送心跳包. Rabbitmq服务需要心跳包来维持正常状态.

填的数值太大也不行. 发送时间太长,导致服务器端认为连接已断开. 服务器端会主动断开连接 .
所以正确方式是在, 服务器主动断开之前发送一个心跳包. 这个时间不要超过服务器默认的自动断开连接时长. 服务器默认好像是10秒断开. 可以自己用秒表测试一下.

<html>  
    <head>  
        <title>WebSocket</title>  
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
    </head>  
    <body>  
    
<think> 我们需要将WebSocketRabbitMQ结合使用,以实现分布式环境下的实时消息推送。常见场景是:多个WebSocket服务器实例需要共享客户端连接状态,并通过RabbitMQ进行消息路由和广播。 解决方案: 1. 使用RabbitMQ作为消息代理,处理服务器实例间的消息传递。 2. 每个WebSocket服务器实例订阅一个或多个RabbitMQ队列或主题,以接收需要广播给客户端的消息。 3. 当客户端连接到WebSocket服务器时,服务器将该连接保存在本地(由于分布式,不能全局保存,所以需要借助RabbitMQ将消息路由到所有实例)。 4. 当需要向所有客户端广播消息时,将消息发送到RabbitMQ交换机,然后由各个服务器实例消费该消息并推送给本地连接的客户端。 具体实现(Node.js + ws + amqplib): 步骤: - 创建WebSocket服务器 - 连接RabbitMQ - 每个WebSocket服务器实例声明一个独占队列(或使用扇出交换机广播到所有实例) - 当有消息需要广播时,发送到RabbitMQ的交换机 - 每个WebSocket服务器实例消费自己的队列,将收到的消息推送给所有本地连接的客户端 代码示例: ```javascript const WebSocket = require('ws'); const amqp = require('amqplib'); const http = require('http'); // WebSocket服务器配置 const server = http.createServer(); const wss = new WebSocket.Server({ server }); const PORT = 18083; // RabbitMQ配置 const RABBITMQ_URL = 'amqp://localhost'; const EXCHANGE_NAME = 'websocket_exchange'; const EXCHANGE_TYPE = 'fanout'; // 使用fanout交换机广播到所有队列 // 本地客户端连接集合 const clients = new Set(); // 连接RabbitMQ并设置消息消费 async function setupRabbitMQ() { try { const conn = await amqp.connect(RABBITMQ_URL); const channel = await conn.createChannel(); // 声明交换机 await channel.assertExchange(EXCHANGE_NAME, EXCHANGE_TYPE, { durable: false }); // 声明一个独占队列(非持久化,独占,自动删除) const queue = await channel.assertQueue('', { exclusive: true }); // 将队列绑定到交换机 await channel.bindQueue(queue.queue, EXCHANGE_NAME, ''); // 消费消息 channel.consume(queue.queue, (msg) => { if (msg) { // 将消息推送给所有本地连接的客户端 clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(msg.content.toString()); } }); channel.ack(msg); } }); } catch (err) { console.error('RabbitMQ连接出错:', err); } } // WebSocket连接处理 wss.on('connection', (ws) => { clients.add(ws); console.log('新的客户端连接,当前连接数:', clients.size); ws.on('message', (message) => { console.log(`收到客户端消息: ${message}`); // 这里可以选择将消息通过RabbitMQ广播出去(如果需要广播所有实例的客户端) // 注意:如果只是本实例的客户端,则直接使用clients广播即可。但跨实例需要发送到RabbitMQ // 我们假设所有消息都需要广播到所有实例的客户端 broadcastToAllInstances(message); }); ws.on('close', () => { clients.delete(ws); console.log('客户端断开,当前连接数:', clients.size); }); }); // 广播消息到所有实例(通过RabbitMQ) async function broadcastToAllInstances(message) { try { const conn = await amqp.connect(RABBITMQ_URL); const channel = await conn.createChannel(); await channel.assertExchange(EXCHANGE_NAME, EXCHANGE_TYPE, { durable: false }); // 发送到交换机,fanout交换机会忽略routing key,所以第二个参数可以是空字符串 channel.publish(EXCHANGE_NAME, '', Buffer.from(message)); console.log(`已广播消息到RabbitMQ: ${message}`); // 关闭通道和连接(实际应用中应复用连接) setTimeout(() => { channel.close(); conn.close(); }, 500); } catch (err) { console.error('广播消息到RabbitMQ失败:', err); } } // 启动 server.listen(PORT, () => { console.log(`WebSocket服务器运行在: ws://localhost:${PORT}`); setupRabbitMQ(); }); ``` ### 代码解释: 1. **WebSocket服务器**: - 使用`ws`库创建WebSocket服务器。 - 维护一个本地的`clients`集合来存储当前服务器实例的客户端连接。 2. **RabbitMQ连接**: - 使用`amqplib`库连接RabbitMQ。 - 声明一个`fanout`类型的交换机(`fanout`会将消息广播到所有绑定的队列)。 - 每个WebSocket服务器实例创建一个独占队列(名称由RabbitMQ生成),并将队列绑定到该交换机。 3. **消息消费**: - 每个服务器实例消费自己队列的消息,当收到消息时,遍历本地连接的客户端并发送消息。 4. **消息广播**: - 当某个服务器实例收到客户端消息时,调用`broadcastToAllInstances`函数将消息发布到RabbitMQ交换机。 - 所有绑定到该交换机的队列(即所有服务器实例)都会收到消息,然后各自推送给本地客户端。 ### 注意事项: - **性能**:频繁创建和关闭RabbitMQ通道(在`broadcastToAllInstances`中)会影响性能,实际应用中应该复用通道或连接池。 - **可靠性**:根据需求选择消息持久化和确认机制。 - **扩展性**:使用`fanout`交换机可以轻松扩展多个服务器实例。 ### 优化建议: 1. **复用RabbitMQ连接和通道**:在应用启动时创建连接和通道,并在整个生命周期内复用。 2. **错误处理**:添加更完善的错误处理和重连机制。 3. **消息格式**:定义协议格式(如JSON)来区分消息类型和目标。 §§相关问题§§: 1. 如何确保RabbitMQ消息在广播时不丢失? 2. 如果WebSocket服务器实例宕机,如何处理未发送的消息? 3. 如何通过RabbitMQ实现点对点的WebSocket消息(而非广播)? 4. 在WebSocketRabbitMQ的集成中,如何实现客户端的身份验证? </think> # WebSocketRabbitMQ 集成指南 WebSocketRabbitMQ 结合使用可以实现高效的分布式实时通信系统。这种架构常用于需要处理大量并发连接和实时消息传递的场景,如聊天应用、实时数据分析推送等。 ## 核心架构模式 ```mermaid graph LR A[Web客户端] -->|WebSocket连接| B(WebSocket服务器) B -->|发布消息| C[RabbitMQ] C -->|消息路由| D1[后台服务1] C -->|消息路由| D2[后台服务2] D1 -->|处理结果| C D2 -->|处理结果| C C -->|推送结果| B B -->|实时推送| A ``` ## Node.js 实现方案(使用 ws 和 amqplib) ```javascript const WebSocket = require('ws'); const amqp = require('amqplib'); const http = require('http'); const server = http.createServer(); const wss = new WebSocket.Server({ server }); const PORT = 8080; // RabbitMQ 连接配置 const RABBIT_HOST = 'amqp://localhost'; const INPUT_QUEUE = 'ws_input_queue'; const OUTPUT_EXCHANGE = 'ws_output_exchange'; // 管理客户端连接 const clients = new Map(); // <clientId, WebSocket> // 连接 RabbitMQ async function setupRabbitMQ() { try { // 1. 连接 RabbitMQ const conn = await amqp.connect(RABBIT_HOST); // 2. 创建通道 const channel = await conn.createChannel(); // 3. 声明输入队列(接收客户端消息) await channel.assertQueue(INPUT_QUEUE, { durable: true }); // 4. 声明输出交换器(广播到所有WebSocket服务器) await channel.assertExchange(OUTPUT_EXCHANGE, 'fanout', { durable: true }); // 5. 消费输入队列的消息 channel.consume(INPUT_QUEUE, (msg) => { if (msg) { const { clientId, payload } = JSON.parse(msg.content.toString()); console.log(`收到来自后台处理的消息: ${payload}`); // 推送给指定客户端 const ws = clients.get(clientId); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(`处理结果: ${payload}`); } channel.ack(msg); } }); return channel; } catch (err) { console.error('RabbitMQ连接失败:', err); process.exit(1); } } // WebSocket连接处理 wss.on('connection', (ws, req) => { // 生成唯一客户端ID const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; clients.set(clientId, ws); console.log(`客户端连接: ${clientId}`); // 设置消息处理 ws.on('message', async (message) => { console.log(`收到客户端消息: ${message}`); try { const rabbitChannel = await setupRabbitMQ(); // 转发消息到RabbitMQ rabbitChannel.sendToQueue( INPUT_QUEUE, Buffer.from(JSON.stringify({ clientId, message, timestamp: Date.now() })), { persistent: true } ); // 发送响应 ws.send(`消息已接收并转发: ${message}`); } catch (err) { console.error('消息转发失败:', err); ws.send('错误: 消息处理失败'); } }); // 连接关闭处理 ws.on('close', () => { clients.delete(clientId); console.log(`客户端断开: ${clientId}`); }); }); // 启动服务器 server.listen(PORT, async () => { console.log(`WebSocket服务器运行在: ws://localhost:${PORT}`); await setupRabbitMQ(); }); ``` ## Python 实现方案(使用 FastAPI 和 aio-pika) ```python from fastapi import FastAPI, WebSocket from fastapi.websockets import WebSocketDisconnect import uvicorn import aio_pika import asyncio import json app = FastAPI() # 存储活动连接 active_connections = {} # RabbitMQ设置 RABBIT_HOST = "amqp://guest:guest@localhost/" INPUT_QUEUE = "ws_input_queue" OUTPUT_EXCHANGE = "ws_output_exchange" async def setup_rabbitmq(): # 连接RabbitMQ connection = await aio_pika.connect_robust(RABBIT_HOST) channel = await connection.channel() # 声明队列和交换器 input_queue = await channel.declare_queue(INPUT_QUEUE, durable=True) output_exchange = await channel.declare_exchange( OUTPUT_EXCHANGE, aio_pika.ExchangeType.FANOUT, durable=True ) # 消息处理回调 async def message_handler(message: aio_pika.IncomingMessage): async with message.process(): data = json.loads(message.body.decode()) client_id = data.get("clientId") payload = data.get("payload") print(f"收到后台处理的消息: {payload}") # 推送给客户端 if client_id in active_connections: ws = active_connections[client_id] await ws.send_text(f"处理结果: {payload}") # 设置消息处理 await input_queue.consume(message_handler) return {"connection": connection, "channel": channel} @app.on_event("startup") async def startup_event(): await setup_rabbitmq() @app.websocket("/ws/{client_id}") async def websocket_endpoint(websocket: WebSocket, client_id: str): await websocket.accept() active_connections[client_id] = websocket print(f"客户端连接: {client_id}") rabbit = await setup_rabbitmq() channel = rabbit["channel"] try: while True: message = await websocket.receive_text() print(f"收到客户端消息: {message}") # 转发消息到RabbitMQ await channel.default_exchange.publish( aio_pika.Message( body=json.dumps({ "clientId": client_id, "message": message }).encode(), delivery_mode=aio_pika.DeliveryMode.PERSISTENT ), routing_key=INPUT_QUEUE ) await websocket.send_text(f"消息已接收并转发: {message}") except WebSocketDisconnect: del active_connections[client_id] print(f"客户端断开: {client_id}") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) ``` ## 关键集成点解释 ### 1. 消息流动方向 - **客户端 → WebSocket服务器 → RabbitMQ**:处理用户请求 - **RabbitMQ → 后台服务**:执行实际业务逻辑 - **后台服务 → RabbitMQWebSocket服务器 → 客户端**:返回处理结果 ### 2. RabbitMQ路由模式 - **Fanout交换器**:用于广播消息到所有WebSocket实例 - **Direct交换器**:用于点对点消息传递 - **Topic交换器**:用于基于主题的模式匹配 ### 3. 会话管理策略 - **UUID映射**:为每个WebSocket连接生成唯一ID - **连接缓存**:在内存中存储WebSocket实例 - **心跳检测**:防止僵尸连接 ```javascript // Node.js中的心跳检测 setInterval(() => { clients.forEach((ws, clientId) => { if (ws.isAlive === false) { clients.delete(clientId); return ws.terminate(); } ws.isAlive = false; ws.ping(null, false, true); }); }, 30000); ``` ## 生产环境优化建议 ### 1. 集群部署 ```mermaid graph BT subgraph RabbitMQ集群 R1[节点1] R2[节点2] R3[节点3] end subgraph WebSocket服务器集群 W1[WS1] --> R1 W2[WS2] --> R2 W3[WS3] --> R3 end Clients[Clients] --> W1 Clients --> W2 Clients --> W3 ``` ### 2. 消息格式规范 ```json { "event": "chat_message", "sender": "user123", "recipient": "room456", "content": "Hello world", "timestamp": 1648800000000, "version": "1.0" } ``` ### 3. 错误处理机制 ```javascript // RabbitMQ错误处理 rabbitChannel.on('error', (err) => { console.error('RabbitMQ通道错误:', err.message); // 重启连接或切换到备用节点 }); // WebSocket错误处理 ws.on('error', (err) => { console.error('WebSocket错误:', err.message); clients.delete(clientId); }); ``` ### 4. 性能优化 - **连接池**:复用RabbitMQ连接 - **消息批处理**:合并多个小消息 - **压缩**:对大型消息使用Gzip压缩 - **QoS控制**:限制未确认消息数量 ```javascript // RabbitMQ QoS配置 await channel.prefetch(100); // 每个连接最多100条未确认消息 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值