在调试基于STOMP协议的消息系统时,往控制台打印一长串JSON文本经常让人眼花缭乱,尤其是当消息结构嵌套较深或者字段数量很多的时候。如果把每条消息渲染成一张带标题、时间戳和正文的图片,排查效率会高很多。Node.js生态里正好有成熟的STOMP客户端和图片生成库,两者结合就能搭出一个小型Mock2Image服务。

这个服务的核心逻辑并不复杂:作为一个STOMP客户端连接到broker,订阅指定的队列或主题,收到MESSAGE帧后取出body,解析出需要展示的字段,然后调用绘图模块生成PNG或JPEG图片。接下来把它拆成几个关键环节细说。
STOMP帧结构与Mock2Image的对接点
STOMP是一种基于文本的简单消息协议,客户端和broker之间通过帧来通信。一个典型的MESSAGE帧由command行、若干个header键值对、一个空行和body组成。command标明帧类型,headers携带路由和元数据,body则是业务负载。Mock2Image要做的就是从body中提取关键字段,把它们画到图片上。
MESSAGE
destination:/queue/mock2image
message-id:abc123
subscription:0
content-type:application/json
{"title":"订单通知","content":"用户10001已下单","timestamp":1716000000}
上面这个帧里,destination告诉消息来自哪个队列,content-type声明body是JSON格式。body中的title、content和timestamp正好可以映射成图片的标题、正文和时间显示。实际业务中body结构可能更复杂,比如嵌套对象或数组,但只要解析出核心字段,绘图逻辑就能复用。
还要留意一个细节:如果消息body是二进制或者启用了压缩,需要根据content-length和content-encoding头来处理。不过大多数调试场景下,body都是纯文本JSON,用JSON.parse解析即可。解析失败时不能让整个进程崩溃,后面会提到容错处理。
用Node.js连接STOMP Broker并订阅消息
Node.js里最常用的STOMP客户端库是@stomp/stompjs,它同时支持浏览器和Node环境。在Node端还需要配合ws库来建立WebSocket连接。先安装依赖,然后创建一个Client实例,配置broker地址、认证信息和心跳参数。
const { Client } = require('@stomp/stompjs');
const WebSocket = require('ws');
Object.assign(global, { WebSocket });
const client = new Client({
brokerURL: 'ws://127.0.0.1:15674/ws',
connectHeaders: {
login: 'guest',
passcode: 'guest'
},
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
reconnectDelay: 5000
});
client.onConnect = function(frame) {
console.log('已连接到 STOMP broker');
client.subscribe('/queue/mock2image', (message) => {
console.log('收到消息:', message.body);
// 这里调用生成图片的函数
});
};
client.onStompError = function(frame) {
console.error('Broker 返回错误:', frame.headers['message']);
console.error('详细内容:', frame.body);
};
client.activate();
heartbeatIncoming和heartbeatOutgoing分别控制客户端期望的心跳发送和接收间隔,单位是毫秒。如果broker长时间没收到心跳,会主动断开连接;客户端的reconnectDelay则负责在断线后自动重连,避免网络抖动导致服务停止。订阅路径要和消息发送方保持一致,比如发送到/queue/mock2image的消息,订阅地址也必须完全匹配。
onConnect回调里调用subscribe方法,第二个参数是一个处理函数,每次收到MESSAGE帧都会触发。message.body就是帧的body字符串,可以直接传给JSON.parse。建议在解析时用try-catch包起来,因为生产者偶尔会发一些非JSON的测试消息,比如纯文本或者空body,直接parse会抛出异常并中断整个Node进程。
另外,如果broker需要虚拟主机或者使用不同版本的STOMP,connectHeaders里可以加host头。对RabbitMQ来说,STOMP over WebSocket的默认路径是/ws,而ActiveMQ则可能是其他地址,部署前需要确认。
把消息负载绘制成图片的两种方式
拿到解析后的JSON对象后,就可以开始画图了。第一种方式是使用node-canvas,它提供了和浏览器Canvas几乎一致的API,但需要在系统上安装cairo等原生依赖。优点是生成像素图速度快,适合批量处理。
const { createCanvas } = require('canvas');
function drawMessageCard(payload) {
const width = 800;
const height = 400;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// 背景
ctx.fillStyle = '#1e1e2f';
ctx.fillRect(0, 0, width, height);
// 标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 32px sans-serif';
ctx.fillText(payload.title || '未命名消息', 40, 80);
// 正文
ctx.font = '24px sans-serif';
ctx.fillStyle = '#cccccc';
const lines = (payload.content || '').split('\n').slice(0, 5);
lines.forEach((line, index) => {
ctx.fillText(line, 40, 140 + index * 36);
});
// 时间戳
ctx.font = '18px sans-serif';
ctx.fillStyle = '#888888';
ctx.fillText(new Date(payload.timestamp * 1000).toLocaleString(), 40, 360);
return canvas.toBuffer('image/png');
}
这个函数根据payload生成一张800x400的深色卡片,标题、正文和时间戳分别用不同字体和颜色绘制。正文按换行符拆成多行,最多显示5行,避免内容过长溢出画布。生成结果是一个PNG格式的Buffer,可以直接写入文件或者通过HTTP接口返回。
第二种方式是用SVG拼出矢量图,再用sharp库转换成PNG。SVG是文本格式,不需要原生canvas依赖,部署更轻量,而且文字和图形可以无损缩放。缺点是复杂排版需要手动处理,比如自动换行和文本截断。
const sharp = require('sharp');
function buildSvg(payload) {
return `<svg width="800" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#1e1e2f"/>
<text x="40" y="80" font-size="32" fill="#ffffff">${payload.title}</text>
<text x="40" y="140" font-size="24" fill="#cccccc">${payload.content}</text>
<text x="40" y="360" font-size="18" fill="#888888">${new Date(payload.timestamp * 1000).toLocaleString()}</text>
</svg>`;
}
async function svgToPng(payload) {
const svg = buildSvg(payload);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
SVG方案里,所有标签和属性都是字符串拼接,然后交给sharp处理。如果内容中包含特殊字符,比如小于号或与号,需要先做XML转义,否则SVG会解析失败。文本换行可以用多个text元素模拟,但不如canvas的fillText配合split方便。两种方案各有优劣,单机调试推荐node-canvas,容器化部署或不想装原生依赖时可以选SVG加sharp。
测试Mock2Image服务与常见问题排查
服务搭好后,启动本地broker并用另一个客户端发送一条测试消息。下面是一个简单的publisher脚本,连接同一个broker,往/queue/mock2image发布一条JSON消息,然后断开连接。
const { Client } = require('@stomp/stompjs');
const WebSocket = require('ws');
Object.assign(global, { WebSocket });
const client = new Client({
brokerURL: 'ws://127.0.0.1:15674/ws',
connectHeaders: { login: 'guest', passcode: 'guest' }
});
client.onConnect = () => {
const payload = JSON.stringify({
title: '订单通知',
content: '用户10001已下单,金额299元',
timestamp: Math.floor(Date.now() / 1000)
});
client.publish({
destination: '/queue/mock2image',
body: payload,
headers: { 'content-type': 'application/json' }
});
console.log('测试消息已发送');
setTimeout(() => client.deactivate(), 500);
};
client.activate();
运行publisher后,Mock2Image服务应该会在控制台打印收到消息,随后生成PNG文件。如果图片没有生成,优先检查broker地址和路径。RabbitMQ开启STOMP插件后,WebSocket端点通常是/ws而不是/stomp,端口也可能是15674而非61613。连接成功但收不到消息时,确认订阅目标与发送destination完全一致,包括前缀是/queue还是/topic。
另一个常见的问题是JSON解析异常。生产者偶尔会发送空body或非JSON内容,建议把JSON.parse封装成安全函数,解析失败时返回一个默认对象并记录日志。还有一点值得注意:图片生成如果耗时较长,会阻塞Node事件循环,导致STOMP心跳无法按时发送,可能被broker断开连接。对于低频调试场景问题不大,但如果要提高并发性能,可以把绘图任务丢给worker线程或者独立队列处理。
整个Mock2Image服务的核心就是把STOMP消息流和图片渲染流水线串起来,前者的重点是稳定连接和可靠订阅,后者的重点是字段提取和绘图容错。两部分都做好后,排查消息问题时就有了一张张清晰的可视化卡片,比盯着控制台文本要舒服得多。

