Socket.IO 通信
约 2853 字大约 10 分钟
...
2026-09-29
通信模型
sv-print打印客户端(Electron)内置一个 Socket.IO 服务端。
页面端(Web 页面、sv-print 前端组件等)作为 Socket.IO 客户端连接该服务,所有打印指令与回执都通过这条连接传递。
- 打印客户端:Socket.IO Server。监听打印事件(
on),处理完成后在同一个 socket 上回发结果(emit) - 页面端:Socket.IO Client。通过
emit发送指令(如print、refreshPrinterList),并监听回执事件(如printerList、success、error)
通信是点对点的:页面端 emit 请求事件,打印客户端处理后将回执 emit 给发起请求的那个 socket,不会广播。集成 sv-print 打印组件(vue/react/jquery)时,组件默认会自动连接 localhost:17521,并全局提供 io 对象,无需手动建连。
服务端配置
| 配置 | 值 | 说明 |
|---|---|---|
| 端口 | 客户端配置 port,默认 17521 | 页面端连接地址使用该端口 |
| 协议 | enableHttps 默认 true | 走 HTTPS(自签证书 localhost.key/localhost.pem),地址形如 https://localhost:17521;关闭后为 http://localhost:17521 |
| pingInterval / pingTimeout | 10000 / 5000 | 心跳参数 |
| maxHttpBufferSize | 10000000000 | 单消息大小上限 |
| allowEIO3 | true | 兼容 Socket.IO 2.x 客户端 |
| cors | origin 回调放行所有来源,allowedHeaders: '*' | 允许任意页面跨域连接 |
鉴权(auth token)
- 客户端配置了
token时,页面端必须在握手参数auth.token中传入相同值,否则连接被拒绝,错误信息为Token error - 客户端未配置
token(空字符串)时,任何连接均可接入 - 握手
auth中的全部字段(如自定义clientId)会被并入socket.data保存
连接示例
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>const socket = io('https://localhost:17521', {
auth: {
clientId: 'my-client-id', // 自定义标识,可选
token: '', // 客户端配置了 token 时必须一致
},
reconnection: true,
reconnectionDelay: 5000,
// 自签证书的 https 场景,浏览器端需信任证书或配置 rejectUnauthorized: false
});
socket.on('connect', () => {
console.log('连接成功', socket.id);
});
socket.on('connect_error', (err) => {
// token 不一致时 err.message === 'Token error'
console.error('连接失败', err.message);
});
socket.on('disconnect', (reason) => {
console.log('已断开', reason);
});Node.js 环境使用 socket.io-client:
const { io } = require('socket.io-client');
const socket = io('https://localhost:17521', {
auth: { token: '' },
});事件总览
方向说明:“页面端 → 客户端” 表示页面端 emit、打印客户端 on;“客户端 → 页面端” 表示打印客户端 emit、页面端 on。
| 分类 | 事件名 | 方向 | 说明 |
|---|---|---|---|
| 打印机 | printerList | 双向 | 连接成功后主动推送;页面端 emit 同名事件或 refreshPrinterList 可请求刷新,回执仍为 printerList |
| 打印机 | refreshPrinterList | 页面端 → 客户端 | 请求获取/刷新打印机列表 |
| 信息 | clientInfo | 客户端 → 页面端 | 连接成功后主动推送;getClientInfo 的回执 |
| 信息 | getClientInfo | 页面端 → 客户端 | 请求客户端基础信息 |
| 信息 | address | 双向 | 页面端 emit 请求,客户端以同名事件返回地址信息 |
| 配置 | config | 双向 | 页面端 emit 请求配置;客户端返回配置也用该事件名 |
| 配置 | updateConfig | 页面端 → 客户端 | 更新配置,完成后回发 config |
| 打印 | print | 页面端 → 客户端 | 新统一打印事件 |
| 打印 | news | 页面端 → 客户端 | 兼容旧版打印事件,处理逻辑同 print |
| 打印 | success / error | 客户端 → 页面端 | 打印成功/失败回执 |
| 打印 | getPaperSizeInfo | 页面端 → 客户端 | 兼容保留,当前仅记录日志,无回执 |
| 兼容渲染 | render-print / render-jpeg / render-pdf | 页面端 → 客户端 | 兼容中转服务的渲染打印事件 |
| 兼容渲染 | render-print-success 等 | 客户端 → 页面端 | 原事件名追加 -success/-error 的结果回执 |
| IPP | ippPrint / ippRequest | 页面端 → 客户端 | IPP 网络打印 / 原始 IPP 请求 |
| IPP | ippPrinterConnected / ippPrinterCallback | 客户端 → 页面端 | IPP 实例创建回执 / 执行结果回执 |
| 文件 | io | 双向 | file:// 文件转 base64 / 上传,同名事件返回结果 |
打印机列表
回执 printerList 为 Electron 打印机信息数组,每项在 getPrintersAsync() 原始字段基础上附加:
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 打印机名称,打印时传给 printer 字段 |
disabled | boolean | 是否被客户端禁用 |
isDefault | boolean | 是否默认打印机(客户端配置了 defaultPrinter 时按其判定) |
isOk | boolean | 状态是否正常(win32 判定 status === 0,其他平台 status === 3) |
socket.on('printerList', (printers) => {
console.log(printers);
});
// 请求刷新(推荐)
socket.emit('refreshPrinterList');
// 兼容写法:emit 同名事件,效果相同
socket.emit('printerList');客户端信息与地址
clientInfo(连接后主动推送,或 emit('getClientInfo') 获取):
| 字段 | 说明 |
|---|---|
hostname | 主机名 |
version | 客户端版本号 |
platform / arch | 操作系统平台 / 系统架构 |
mac / ip / ipv6 | 网卡 MAC / IPv4 / IPv6 地址 |
clientUrl | 客户端地址,如 http://192.168.x.x:17521 |
machineId | 机器唯一标识 |
id | 客户端 id |
nickName | 客户端昵称 |
address(请求-应答,同名回执):platform、arch、mac、ip、ipv6。
socket.on('clientInfo', (info) => {
console.log(info.version, info.ip);
});
socket.emit('getClientInfo');
socket.on('address', (addr) => {
console.log(addr.mac, addr.ip);
});
socket.emit('address');配置读取与更新
emit('config'):回执config,payload 为客户端全量配置(store 键值,含token、port、nickName、authKey、enableHttps等)emit('updateConfig', { config, refreshConfig }):更新配置,完成后同样回发config
updateConfig 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
config | object | 需要更新的配置键值 |
refreshConfig.socket | boolean | 重启 Socket.IO 服务(修改 port、token、enableHttps 后需要) |
refreshConfig.cloud | boolean | 重启中转服务连接 |
refreshConfig.cloudPrint | boolean | 重启云打印 SaaS 连接 |
refreshConfig.mqtt | boolean | 重启 MQTT 服务 |
refreshConfig.printer | boolean | 重新初始化打印窗口(修改 authKey 后需要) |
refreshConfig.window | boolean | 应用窗口显示等设置 |
refreshConfig.socket 为 true 时服务会重启,当前连接断开,页面端需要重新连接。
socket.on('config', (config) => {
console.log(config.port, config.token);
});
socket.emit('config');
socket.emit('updateConfig', {
config: { nickName: 'my-printer', token: 'my-token' },
refreshConfig: { socket: true },
});打印(print / news)
源码:events/socket/print.ts、core/index.ts。print 与 news 处理逻辑一致,经 choosePrintRouter 按传入数据自动判定打印类型:
| 判定条件 | 类型 | 说明 |
|---|---|---|
传 html | html | 渲染 HTML 后调用打印机打印 |
传 template 或 tempId | template | 渲染模板 JSON(tempId 为客户端预置模板 id)后打印 |
type: 'pdf' | 渲染后 printToPDF 再送打印机 | |
type: 'url_pdf' | url_pdf | 获取 pdf_path 指向的 PDF 直接打印 |
type: 'capture' | capture | 渲染后截图,返回 base64/buffer/url |
请求字段(常用):
| 字段 | 类型 | 说明 |
|---|---|---|
printer | string | 打印机名称,缺省使用客户端默认打印机 |
templateId | string | 本次打印标识,回执中原样返回,用于匹配结果 |
html | string | HTML 内容 |
template | object/string | 模板 JSON(字符串会自动 parse) |
tempId | string/number | 客户端预置模板 id |
printData | object | 模板打印数据 |
type | string | 'pdf' / 'url_pdf' / 'capture' |
pdf_path | string | url_pdf 时 PDF 地址(http/https 或本地路径) |
toNet | boolean | pdf/capture 结果是否存入本地可访问目录(回执返回 url) |
buffer | boolean | pdf/capture 是否返回 buffer |
captureType | string | capture 输出类型 'jpeg' / 'png' |
captureOptions | object | 传给 snapdom.toCanvas 的参数 |
print_options | object/string | 打印选项(JSON 字符串会解析并展开到 data 顶层,供云打印下发使用) |
replyId | string | 中转/云打印链路的回执标识 |
Electron 打印选项直接平铺在 data 顶层(均有默认值):
对应版本的文档 https://github.com/electron/electron/blob/v34.5.8/docs/api/web-contents.md
silent(true)、 // 默认 true 静默打印printBackground(true)、 // 默认 true 打印背景color(true)、 // 默认 true 打印彩色margins({ marginType: 'none' })、 // 默认 none 无边距landscape(false)、 // 默认 false 是否横屏打印scaleFactor(100)、 // 默认 100%pagesPerSheet(1)、 // 默认 1 张纸collate(true)、 // 默认 true 合并打印copies(1)、 // 默认 1 打印份数pageRanges、// 打印页面范围,如 '1-3,5-7'duplexMode(simplex/shortEdge/longEdge)、 // 默认 simplexdpi、 // 默认 300 DPIheader、 // 页眉内容footer、 // 页脚内容pageSize('A4')、 // 默认 A4 页面大小:单位微米: { width: 210 * 1000, height: 297 * 1000 }preferCSSPageSize(true)、 // 是否优先使用 CSS 页面大小generateTaggedPDF(true)。 // 是否生成标签 PDF
回执 success / error:
| 字段 | 说明 |
|---|---|
msg | 成功为“打印成功”,失败为“打印失败: 原因” |
templateId | 请求中的 templateId |
replyId | 请求中的 replyId(如有) |
url | pdf/capture 且 toNet 时的文件访问地址 |
buffer | pdf/capture 且 buffer: true 时返回 |
参数校验失败(打印机不存在/已禁用、模板格式错误、未知打印类型等)会直接 emit error,payload 为 { msg: '打印失败: xxx', templateId, replyId }。
socket.on('success', (data) => {
if (data.templateId === tplId) console.log('打印成功', data);
});
socket.on('error', (data) => {
console.error('打印失败', data.msg);
});
// HTML 打印
socket.emit('print', {
templateId: `print-${Date.now()}`,
html: '<h1>hello sv-print</h1>',
copies: 1,
});
// 模板打印
socket.emit('print', {
templateId: 'tpl-1',
template: { panels: [] }, // 模板 JSON
printData: { name: '张三' },
});
// PDF 打印
socket.emit('print', {
type: 'url_pdf',
pdf_path: 'http://127.0.0.1:7071/public/xxx.pdf',
});兼容渲染事件(render-*)
源码:events/socket/print.ts。为兼容旧中转服务/插件保留:
| 事件 | 等价行为 |
|---|---|
render-print | 同 print |
render-jpeg | 同 type: 'capture' 且 buffer: true |
render-pdf | 同 type: 'pdf' 且 buffer: true |
回执事件名在原事件名后追加结果:
render-print→render-print-success/render-print-errorrender-jpeg→render-jpeg-success/render-jpeg-errorrender-pdf→render-pdf-success/render-pdf-error
socket.on('render-pdf-success', (data) => {
console.log('pdf 生成成功', data);
});
socket.on('render-pdf-error', (data) => {
console.error('pdf 生成失败', data.msg);
});
socket.emit('render-pdf', { templateId: 'tpl-1', template: { panels: [] } });IPP 网络打印
源码:events/socket/ipp.ts。用于直连支持 IPP 协议的网络打印机。
ippPrint 请求字段:
| 字段 | 说明 |
|---|---|
url | 打印机 IPP 地址,如 http://192.168.1.100:631/ipp/print |
opt | ipp.Printer 选项(可选) |
action | IPP 操作:Get-Printer-Attributes 查询能力 / Print-Job 打印 / Cancel-Job 取消任务 |
message | IPP 消息体;message.data 自动转为 Buffer(encoding 默认 utf8) |
回执:
ippPrinterConnected:打印机实例创建后返回ippPrinterCallback:执行结果,参数(err, res);失败时err为{ type, msg },成功时err为null
socket.on('ippPrinterConnected', (printer) => {
console.log('IPP 实例已创建');
});
socket.on('ippPrinterCallback', (err, res) => {
if (err) console.error(err.type, err.msg);
else console.log(res);
});
socket.emit('ippPrint', {
url: 'http://192.168.1.100:631/ipp/print',
action: 'Print-Job',
message: {
'operation-attributes-tag': { 'requesting-user-name': 'web' },
data: 'IPP 打印内容',
},
});ippRequest:请求字段 { url, data },data 经 ipp.serialize 后发送原始 IPP 请求,结果同样通过 ippPrinterCallback 回执。
文件转换(io)
源码:events/socket/io.ts。将页面端无法直接读取的本地 file:// 文件转为 base64 或上传到指定接口。
请求字段:
| 字段 | 说明 |
|---|---|
type | 固定 'image' |
list | file:// 路径数组 |
to | 'base64' 或 'upload' |
options | to: 'upload' 时使用:url(上传接口,必填)、data(附加表单字段)、headers、params、timeout(默认 60s) |
回执 io 为数组:[{ key: 原始 fileUrl, result: 结果 }]。base64 结果形如 data:image/png;base64,...;upload 结果为接口返回的文件 url。文件无读取权限时,客户端会先复制到用户数据目录(userData/io-files)再处理,处理完自动清理临时文件。
socket.on('io', (list) => {
console.log(list[0].key, list[0].result);
});
socket.emit('io', {
type: 'image',
list: ['file:///C:/imgs/a.png'],
to: 'base64',
});云打印连接
除本地服务外,打印客户端自身还会按配置以 socket.io-client 反连云端。连接绑定与本地服务相同的事件处理器(onCloudClientConnect),因此云端可直接下发 print 等事件,打印结果沿原连接回传:
| 连接 | 开关(客户端配置) | 地址 | 握手参数 | 说明 |
|---|---|---|---|---|
| 中转服务 | connectTransit | transitUrl | query.client: 'electron-hiprint'、auth.token | 传统中转链路 |
| 云打印 SaaS | connectCloud | cloudUrl | query.client: 'sv-print-client'、auth.token(device_token) | 独立的云打印服务 |
页面端集成无需关心这些云端连接,直接对接本地 17521 服务即可。
版权所有
版权归属:sv-print
