广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何用Node.js实现STOMP协议的Mock2Image消息转图片服务?

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

如何用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消息流和图片渲染流水线串起来,前者的重点是稳定连接和可靠订阅,后者的重点是字段提取和绘图容错。两部分都做好后,排查消息问题时就有了一张张清晰的可视化卡片,比盯着控制台文本要舒服得多。

广告位 · 文章底部(后台-模块管理替换此区域)