MQTT 通信
约 2263 字大约 8 分钟
...
2026-09-29
说明
sv-print 客户端内置一个本地 MQTT Broker(基于 aedes 与 aedes-server-factory 实现),供网页、小程序等业务端通过 MQTT 协议与客户端通信,完成打印机查询、配置读写、打印任务下发等操作。
Broker 与配置
| 项目 | 说明 |
|---|---|
| Broker 实现 | aedes,broker id 固定为 sv-print/client |
| 监听地址 | 0.0.0.0,同一端口同时支持 TCP 与 WebSocket 连接(ws: true) |
| 端口 | 配置项 mqttPort,默认 18521 |
| 启用开关 | 配置项 enableMqtt,默认 true;为 false 时不启动 MQTT 服务 |
| 鉴权 Token | 配置项 token,默认空(不鉴权) |
修改 enableMqtt、mqttPort 后需重启 MQTT 服务(refreshConfig.mqtt = true 或重启客户端)。Broker 初始化失败(如端口被占用)时,客户端会弹窗提示,可选择退出应用。
鉴权规则
通过 MQTT 连接时的 preConnect 钩子校验(源码:service/mqtt.ts):
- 服务端只校验
password:若客户端设置了token,则连接密码必须与之相等,否则拒绝连接(Token error); token为空时不做鉴权,任意密码均可连接;username不校验,传sv-print;clientId由调用方自定义,需保证唯一(客户端按 clientId 管理连接)。
连接示例
使用 mqtt.js 连接本地 Broker(与服务端测试页 test-mqtt.html 一致):
import mqtt from 'mqtt';
const client = mqtt.connect('mqtt://localhost:18521', {
clientId: 'my-web-' + Date.now(), // 自定义唯一标识
username: 'sv-print', // 不校验,习惯写法
password: '', // 客户端设置了 token 时必须传入
clean: true,
reconnectPeriod: 5000,
});
client.on('connect', () => {
// 按需订阅回调 topic
client.subscribe(['printerList', 'clientInfo', 'success', 'error', 'config'], { qos: 1 });
});
client.on('message', (topic, message) => {
const payload = JSON.parse(message.toString());
console.log(topic, payload);
});WebSocket 方式连接(同一端口):
const client = mqtt.connect('ws://localhost:18521', {
clientId: 'my-web-' + Date.now(),
password: '',
});Topic 总表
| topic | 方向 | 说明 | 源码文件 |
|---|---|---|---|
refreshPrinterList | 业务端 -> 服务 | 刷新并推送打印机列表 | events/mqtt/client.ts |
printerList | 业务端 -> 服务 / 服务 -> 业务端 | 请求刷新 / 返回打印机列表 | events/mqtt/client.ts |
getClientInfo | 业务端 -> 服务 | 获取客户端基础信息 | events/mqtt/client.ts |
clientInfo | 服务 -> 业务端 | 返回客户端基础信息(连接后主动推送) | events/mqtt/client.ts |
address | 业务端 -> 服务 / 服务 -> 业务端 | 获取 / 返回客户端地址信息 | events/mqtt/client.ts |
print | 业务端 -> 服务 | 打印(新统一入口) | events/mqtt/print.ts |
news | 业务端 -> 服务 | 打印(兼容旧版,处理逻辑同 print) | events/mqtt/print.ts |
success | 服务 -> 业务端 | 打印成功回调 | events/mqtt/print.ts |
error | 服务 -> 业务端 | 打印失败回调 | events/mqtt/print.ts |
{eventKey}-success / {eventKey}-error | 服务 -> 业务端 | 消息携带 eventKey 时的兼容回调(如中转服务 render-print) | printer.service.ts |
config | 业务端 -> 服务 / 服务 -> 业务端 | 获取配置 / 返回配置 | events/mqtt/config.ts |
updateConfig | 业务端 -> 服务 | 更新配置 | events/mqtt/config.ts |
ippPrint | 业务端 -> 服务 | IPP 网络打印 | events/mqtt/ipp.ts |
ippRequest | 业务端 -> 服务 | IPP 原始请求 | events/mqtt/ipp.ts |
ippPrinterConnected | 服务 -> 业务端 | IPP 打印机连接事件 | events/mqtt/ipp.ts |
ippPrinterCallback | 服务 -> 业务端 | IPP 执行成功 / 失败回调 | events/mqtt/ipp.ts |
io | 业务端 -> 服务 / 服务 -> 业务端 | 文件 IO 处理(转 base64 / 上传) | events/mqtt/io.ts |
getPaperSizeInfo | 业务端 -> 服务 | 获取纸张大小(兼容占位,未实现回复) | events/mqtt/print.ts |
paperSizeInfo | 服务 -> 业务端 | 返回纸张大小(预留,MQTT 链路未实现) | constant/index.ts |
基础信息(client.ts)
printerList 打印机列表
- 连接成功后服务端主动推送一次;
- 业务端 publish 到
refreshPrinterList或printerList(payload 可为{})可触发重新推送。
列表元素字段(Electron 打印机对象附加客户端状态):
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 打印机名称(打印时传此值) |
displayName | string | 显示名称 |
isDefault | boolean | 是否默认打印机(受客户端 defaultPrinter 设置影响) |
status | number | 系统状态码 |
disabled | boolean | 是否被客户端禁用 |
isOk | boolean | 状态是否正常(win32 判定 status === 0,其余平台 status === 3) |
clientInfo 客户端基础信息
- 连接成功后服务端主动推送一次;publish 到
getClientInfo可触发重新推送。
| 字段 | 类型 | 说明 |
|---|---|---|
hostname | string | 主机名 |
version | string | 客户端版本号 |
platform | string | 平台(win32 / darwin / linux) |
arch | string | 系统架构 |
mac | string | MAC 地址 |
ip | string | IPv4 地址 |
ipv6 | string | IPv6 地址 |
clientUrl | string | 客户端 HTTP 服务地址,如 http://192.168.1.2:17521 |
machineId | string | 客户端设备 id |
id | string | 客户端 id |
nickName | string | 客户端别名 |
address 客户端地址信息
业务端 publish 到 address(payload 可为 {}),服务端以同名 topic 回复:
| 字段 | 类型 | 说明 |
|---|---|---|
platform | string | 平台 |
arch | string | 系统架构 |
mac | string | MAC 地址 |
ip | string | IPv4 地址 |
ipv6 | string | IPv6 地址 |
打印(print.ts)
请求
向 print(或兼容的 news)publish 打印消息,两者处理逻辑一致,均进入 choosePrintRouter 统一排队执行。
打印类型由消息数据自动判定:有 html 走 HTML 打印;有 template(或 templateId、tempId)走模板渲染打印;type 指定为 pdf / url_pdf / capture 时走对应专用链路。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
printer | string | 否 | 打印机名称,缺省用客户端默认打印机 |
html | string | 否 | HTML 内容,走 HTML 打印 |
template | string | object | 否 | 模板 JSON(面板需含 panels),走模板渲染打印 |
templateId | string | 否 | 模板 id,用于回调匹配 |
tempId | string | number | 否 | 客户端本地预置模板 id |
type | string | 否 | 兼容打印类型:pdf / url_pdf / capture |
pdf_path | string | 否 | 网络 pdf 地址(type: 'url_pdf' 时使用) |
toNet | boolean | 否 | pdf / capture 结果是否存入本地可访问 |
buffer | boolean | 否 | pdf / capture 打印是否返回 buffer 数据 |
captureType | string | 否 | capture 截图类型,jpeg / png,同时作为扩展名 |
captureOptions | object | 否 | 传给 snapdom.toCanvas 的参数 |
print_options | object | string | 否 | 打印选项(云打印下发时为 JSON 字符串),展开后可携带 copies、landscape、pageSize、duplexMode 等 |
pageNum | number | 否 | 打印页数 |
eventKey | string | 否 | 兼容回调 key,设置后额外向 {eventKey}-success / {eventKey}-error 回调 |
replyId | string | 否 | 中转服务请求标识,随回调原样返回 |
回调
- 打印成功:publish
success,payload 为任务结果数据; - 打印失败:publish
error; - 数据校验失败(如打印机已禁用、模板格式错误)时直接 publish
error,payload 为{ msg, templateId, replyId }; - 消息携带
eventKey时,任务完成后额外 publish{eventKey}-success或{eventKey}-error(兼容中转服务的render-print、render-jpeg、render-pdf渲染回调)。
示例
HTML 打印:
client.publish(
'print',
JSON.stringify({
printer: '', // 留空使用默认打印机
html: '<h1>测试打印</h1>',
}),
{ qos: 1 },
);模板打印(完整模板 JSON):
client.publish(
'print',
JSON.stringify({
template: { panels: [/* 模板面板数据 */] },
templateId: 'aec8bc1a-f53a-44d4-81dd-13c5a363f612',
}),
{ qos: 1 },
);网络 PDF 打印:
client.publish(
'print',
JSON.stringify({
type: 'url_pdf',
pdf_path: 'http://127.0.0.1:7071/public/1749804654179wcl4h87ft.pdf',
}),
{ qos: 1 },
);配置(config.ts)
config 获取配置
publish 到 config(payload 可为 {}),服务端以同名 topic 回复客户端全部配置(appService.getConfig())。
updateConfig 更新配置
publish 到 updateConfig,更新完成后以 config topic 回复最新配置。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
config | object | 是 | 要更新的配置项,如 { nickName: 'sv-print-test' } |
refreshConfig | object | 否 | 指定需要重启刷新的子服务:{ socket, cloud, mqtt, printer },均默认 false |
client.publish(
'updateConfig',
JSON.stringify({
config: { nickName: 'sv-print-test' },
refreshConfig: { mqtt: true },
}),
{ qos: 1 },
);IPP 网络打印(ipp.ts)
ippPrint IPP 打印
publish 到 ippPrint:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | IPP 打印机地址,如 http://192.168.1.100:631/ipp/print |
opt | object | 否 | ipp.Printer 初始化选项 |
action | string | 是 | Get-Printer-Attributes(查询打印机参数)/ Print-Job(新建打印任务)/ Cancel-Job(取消任务) |
message | object | 是 | IPP 操作消息;message.data 若非 Buffer,字符串按 message.encoding(默认 utf8)自动转 Buffer |
服务端先以 ippPrinterConnected topic 返回打印机连接信息(printer 实例对象),执行完成后以 ippPrinterCallback topic 返回结果。
ippRequest IPP 原始请求
publish 到 ippRequest,payload 为 { url, data };data 经 ipp.serialize 序列化后发送,结果以 ippPrinterCallback topic 返回。
ippPrinterCallback 回调
- 失败:payload 为
{ type: 错误名, msg: 错误信息 }; - 成功:payload 为
null(IPP 响应结果兼容 socket.io 双参回调写法,随发布包属性附带)。
client.publish(
'ippPrint',
JSON.stringify({
url: 'http://192.168.1.100:631/ipp/print',
action: 'Print-Job',
message: {
'operation-attributes-tag': {
'requesting-user-name': 'sv-print',
},
data: '<h1>ipp test</h1>',
encoding: 'utf8',
},
}),
{ qos: 1 },
);IO 文件处理(io.ts)
IO 请求
publish 到 io:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 目前仅支持 image |
list | string[] | 是 | file:// 协议的本地文件路径列表 |
to | string | 是 | base64(转 base64)/ upload(上传到指定接口) |
options | object | 否 | to: 'upload' 时的上传配置:url(必填)、data(附加表单字段)、headers、params、timeout(默认 60s) |
对无读取权限(EPERM / EACCES / ENOENT)的文件,客户端会自动复制到应用安全目录(userData/io-files)后再读取,处理完成后删除临时副本。
IO 回调
以 io topic 回复结果数组,每个元素:
| 字段 | 类型 | 说明 |
|---|---|---|
key | string | 原始 file:// 路径 |
result | string | base64 时为 data:image/png;base64,... 数据 URL;upload 时为上传接口返回的 url(取 res.data.data.url) |
client.publish(
'io',
JSON.stringify({
type: 'image',
to: 'base64',
list: ['file:///Users/cc/Desktop/test.png'],
}),
{ qos: 1 },
);版权所有
版权归属:sv-print
