ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于clawpdf二次开发KKPrinter:跨网段虚拟打印机共享与打印路由网关实战

基于clawpdf二次开发KKPrinter:跨网段虚拟打印机共享与打印路由网关实战 简介本资源为基于开源项目clawpdf二次开发的虚拟打印机KKPrinter完整源码包面向需要实现打印机共享、远程跨网络打印的开发者与运维人员。项目通过虚拟打印机截取打印文件并转发至物理打印机可解决不同网络环境下打印机无法共享的痛点适用于企业办公、云打印等场景。压缩包共2351个文件约389.87MB以C#源码595个cs、C/C头文件319个h、动态库262个dll及XAML界面文件、配置文件、编译中间产物为主包含完整工程与依赖库确保可直接运行。目前已有2824人学习下载。作者在clawpdf基础上完成业务改造并处理了签名与编译坑点读者可获得可运行的KKPrinter源码、虚拟打印机截取与转发实现思路、跨网络共享方案及排错参考便于二次开发与部署。1. 从 clawpdf 到 KKPrinter虚拟打印机共享到底在解决什么问题办公室里的场景往往很具体财务室只有一台针式打印机仓库的标签机在另一栋楼设计部的同事想在自己工位上直接调用车间的绘图仪。物理距离和网络隔离摆在那里Windows 自带的共享打印机又经常在跨网段、跨系统时翻车报错代码从 0x0000011b 到 0x00000709 轮番出现。基于开源项目 clawpdf 二次开发虚拟打印机 KKPrinter核心思路就是把「打印机」这件事从物理设备上解耦出来——本地装一个虚拟打印机驱动文档先打到虚拟队列再由 KKPrinter 把任务转发到真正连着目标打印机的机器上。这样不同网络、不同系统之间共享的不再是 Windows 的 SMB 共享通道而是一条由你自己控制的转发链路。适合谁适合手里有多个隔离网段、又不想在每个工位都配一台实体打印机的运维和开发人员也适合想基于 clawpdf 做二次开发、把打印能力嵌入自己业务系统的团队。2. clawpdf 的架构拆解与 KKPrinter 的改造切入点2.1 clawpdf 原本是怎么把文档变成 PDF 的clawpdf 本身是一个开源的虚拟打印机项目它的工作链路可以粗略分成三层打印驱动层、后台服务层、输出处理层。打印驱动层是一个标准的 Windows 打印机驱动安装后在「设备和打印机」里会出现一个虚拟打印机图标。当用户在任意应用里选择这个虚拟打印机并点击打印时Windows 的打印后台会生成一个 EMF 或 RAW 格式的打印任务交给 clawpdf 的后台服务。后台服务层是 clawpdf 的核心它监听打印队列拿到打印任务后调用内部的渲染引擎把页面内容转成 PDF。这一步依赖的是 Windows 的 XPS 或 GDI 渲染管线clawpdf 在中间做了一层封装把页面描述语言转换成 PDF 对象。输出处理层则负责决定 PDF 存到哪里、叫什么名字、要不要触发后续动作。理解这个分层很关键因为 KKPrinter 要做的「共享」改造本质上是在输出处理层做文章。原本 clawpdf 把 PDF 存到本地磁盘就结束了KKPrinter 需要在这个位置插入一个转发模块把生成的 PDF 或者原始打印任务通过网络送到远端。2.2 为什么选 clawpdf 而不是从零写一个虚拟打印机驱动从零写一个 Windows 打印机驱动光是 WDK 的版本适配和 WHQL 签名就能耗掉几个月。clawpdf 已经把驱动层和服务层的脏活累活干完了代码结构也相对清晰二次开发的入口主要在服务层的输出逻辑和配置管理上。另一个现实原因是 clawpdf 用的是 .NET 技术栈对于大多数做企业内工具开发的团队来说C# 的维护成本远低于 C 驱动开发。改造 KKPrinter 时我一般会把 clawpdf 的源码先跑通一遍确认本地虚拟打印到 PDF 的链路没有问题然后再动转发逻辑。具体做法是找到 clawpdf 服务里处理完 PDF 生成之后的那个回调点通常是一个名为ProcessPrintJob或类似的方法在里面插入自己的转发调用。// 在 clawpdf 服务处理完打印任务后插入 KKPrinter 的转发逻辑 // 这段代码通常放在 clawpdf 的 PrintJobProcessor 类中 public void OnPrintJobCompleted(string pdfFilePath, string printerName) { // 1. 读取 KKPrinter 的配置文件确定目标打印机和远端地址 var config KKPrinterConfig.Load(); var target config.GetTargetFor(printerName); if (target null) { // 没有配置转发规则保持 clawpdf 原本的本地保存行为 return; } // 2. 把生成的 PDF 文件通过 HTTP 或 TCP 发送到远端 KKPrinter 接收端 // 这里用 HTTP 是为了穿透大多数企业防火墙端口走 80 或自定义高位端口 using (var client new HttpClient()) { var content new MultipartFormDataContent(); var fileContent new ByteArrayContent(File.ReadAllBytes(pdfFilePath)); content.Add(fileContent, file, Path.GetFileName(pdfFilePath)); content.Add(new StringContent(target.RemotePrinterName), printer); // 3. 远端接收端收到后调用本地打印机驱动完成实际打印 var response client.PostAsync(target.RemoteUrl, content).Result; if (!response.IsSuccessStatusCode) { // 转发失败时记录日志并把 PDF 留在本地待重试 Log.Error($转发到 {target.RemoteUrl} 失败: {response.StatusCode}); } } }这段代码的逻辑很直白clawpdf 生成 PDF 之后KKPrinter 根据配置判断这个打印任务要不要转发。如果要转发就把 PDF 文件通过 HTTP multipart 发到远端。远端跑一个轻量的接收服务收到文件后调用本地打印机驱动打印。参数方面有几个关键点。target.RemoteUrl是远端接收端的地址建议用 IP 加端口的形式避免依赖 DNS。target.RemotePrinterName是远端机器上实际打印机的名称必须和远端「设备和打印机」里显示的名字完全一致否则会报「打印机不存在」。HTTP 超时时间建议设成 30 秒以上因为 PDF 文件可能比较大尤其是多页图纸。2.3 远端接收端的最小实现远端接收端不需要装 clawpdf它只需要一个能接收文件并调用打印的轻量服务。用 C# 写一个控制台程序或者 Windows 服务都行核心是调用System.Drawing.Printing.PrintDocument或者直接用Process.Start调用系统打印命令。// KKPrinter 远端接收端的最小实现 // 部署在真正连着目标打印机的那台机器上 [HttpPost] [Route(api/print)] public async TaskIHttpActionResult ReceiveAndPrint() { var httpRequest HttpContext.Current.Request; if (httpRequest.Files.Count 0) { return BadRequest(没有收到文件); } var file httpRequest.Files[0]; var printerName httpRequest.Form[printer]; // 保存到临时目录避免文件名冲突 var tempPath Path.Combine(Path.GetTempPath(), $kkprinter_{Guid.NewGuid()}.pdf); file.SaveAs(tempPath); // 调用本地打印机打印 PDF // 注意Windows 原生不支持直接命令行打印 PDF // 常见做法是调用 Adobe Reader 或 SumatraPDF 的命令行 var psi new ProcessStartInfo { FileName C:\Program Files\SumatraPDF\SumatraPDF.exe, Arguments $-print-to \{printerName}\ \{tempPath}\, UseShellExecute false, CreateNoWindow true }; Process.Start(psi); // 等待打印任务进入队列后清理临时文件 // 这里不能立即删除否则打印驱动可能读不到文件 await Task.Delay(TimeSpan.FromSeconds(30)); File.Delete(tempPath); return Ok(已提交打印); }远端接收端的参数配置要注意几点。SumatraPDF 的路径要写绝对路径-print-to后面的打印机名称如果有空格必须用双引号包起来。临时文件不能收到就删因为打印驱动是异步读取文件的删早了会打印出空白页。延迟时间根据 PDF 大小调整一般 30 秒够用大图纸可以设到 60 秒。提示如果远端机器上装的是 Adobe Reader命令行参数是/t 文件路径 打印机名称但 Adobe Reader 在无人值守场景下容易弹窗SumatraPDF 更稳定。3. 跨网络共享的转发链路怎么搭从 HTTP 到端口映射3.1 同网段和跨网段的两种部署形态KKPrinter 的部署形态取决于网络环境。如果所有机器都在同一个局域网内事情最简单每台需要虚拟打印的机器装 KKPrinter 客户端真正连着打印机的机器装 KKPrinter 接收端客户端直接通过内网 IP 访问接收端。跨网段的情况就复杂一些。常见的场景是办公网和产线网隔离办公网的机器不能直接访问产线网的打印机。这时候需要在两个网段之间找一个能双向通信的中间节点通常是一台双网卡的服务器或者一台有路由策略的机器。KKPrinter 的转发链路可以做成两级办公网客户端把打印任务发到中间节点中间节点再转发到产线网的接收端。这种两级转发的好处是中间节点可以做任务队列和重试。如果产线网的接收端暂时不可达中间节点先把 PDF 存下来等网络恢复再发。实现上就是在中间节点跑一个 KKPrinter 中继服务它既接收客户端的打印任务又负责向最终接收端投递。3.2 端口选择和防火墙配置的实操细节KKPrinter 的通信端口建议不要用 80 或 443因为这两个端口在企业网络里经常被其他服务占用而且有些安全策略会拦截非标准 HTTP 流量。我一般会选 9100 或 9101 这类打印机常用的端口号防火墙规则也好写。配置 Windows 防火墙放行端口的命令如下# 在 KKPrinter 接收端所在机器上放行 9100 端口 netsh advfirewall firewall add rule nameKKPrinter dirin actionallow protocolTCP localport9100 # 如果需要限制来源 IP可以加上 remoteip 参数 netsh advfirewall firewall add rule nameKKPrinter dirin actionallow protocolTCP localport9100 remoteip192.168.1.0/24dirin表示入站规则actionallow是允许protocolTCP指定协议。remoteip参数用来限制只有特定网段能访问这个在生产环境里很有必要避免接收端被随意调用。如果中间节点需要做端口映射比如把公网端口映射到内网接收端那就在路由器或防火墙上做 NAT 规则。但要注意打印机共享这种场景一般不建议直接暴露到公网最好还是走内网专线或者加密隧道。如果确实需要跨公网KKPrinter 的 HTTP 传输层可以加一层 TLS用自签名证书就行主要是防嗅探。3.3 打印任务的重试和幂等设计网络转发最怕的就是任务丢了或者重复打印。KKPrinter 在客户端转发失败时会把 PDF 留在本地一个待重试目录并记录目标打印机和远端地址。后台起一个定时任务每隔几分钟扫描这个目录重新尝试转发。重试次数超过阈值后把任务移到失败目录并告警。幂等性方面每个打印任务生成一个唯一的 JobId远端接收端在处理前先检查这个 JobId 是否已经打印过。已经处理过的直接返回成功避免重复打印。JobId 可以存在远端的 SQLite 或者内存缓存里保留最近 24 小时的记录就够了。// 客户端生成 JobId 并随文件一起发送 var jobId ${Environment.MachineName}_{DateTime.UtcNow.Ticks}; content.Add(new StringContent(jobId), jobId); // 远端接收端检查 JobId 是否已处理 private static ConcurrentDictionarystring, DateTime _processedJobs new(); public async TaskIHttpActionResult ReceiveAndPrint() { var jobId HttpContext.Current.Request.Form[jobId]; if (_processedJobs.ContainsKey(jobId)) { return Ok(重复任务已忽略); } // ... 打印逻辑 ... _processedJobs[jobId] DateTime.UtcNow; // 清理超过 24 小时的记录 foreach (var key in _processedJobs.Keys) { if (_processedJobs[key] DateTime.UtcNow.AddHours(-24)) _processedJobs.TryRemove(key, out _); } return Ok(已提交打印); }JobId 的生成规则用机器名加时间戳基本不会重复。远端用ConcurrentDictionary做内存缓存重启后缓存丢失但重启期间重复打印的概率很低可以接受。如果要求更严格可以把 JobId 持久化到本地文件或数据库。4. 避坑与排查那些让共享打印机翻车的典型问题4.1 现象客户端提示转发成功但远端打印机没反应原因通常出在远端接收端调用打印命令的那一步。SumatraPDF 的-print-to参数如果打印机名称写错了进程会正常退出但不会打印。另外如果远端接收端是以 Windows 服务身份运行的它可能没有权限访问打印机。解决方法是先用命令行手动测试一遍打印命令确认打印机名称和路径都正确。然后把 KKPrinter 接收端服务的登录账户改成有打印机访问权限的用户或者在服务属性里勾选「允许服务与桌面交互」。更稳妥的做法是接收端不用 Windows 服务而是用计划任务在用户登录时启动。4.2 现象跨网段转发时好时坏偶尔超时这种间歇性故障多半是网络抖动或者 MTU 不匹配导致的。如果 KKPrinter 走的是 HTTP大文件在跨网段传输时可能被分片某些网络设备对分片的处理不一致就会丢包。解决方法是把 HTTP 客户端的超时时间调大同时在应用层做分块传输。另一个办法是压缩 PDF 再发送减少传输量。如果网络质量确实差可以在中间节点做本地缓存客户端只管往中间节点发中间节点负责可靠投递。4.3 现象Windows 7 客户端安装 KKPrinter 后虚拟打印机不出现Windows 7 对 .NET 版本和驱动签名有额外要求。clawpdf 的驱动在 Win7 上需要 .NET Framework 4.5 以上而且驱动本身要有有效的数字签名否则安装后设备管理器里会显示黄色感叹号。解决办法是确认 Win7 装了 .NET Framework 4.5 或更高版本然后检查 clawpdf 的驱动签名。如果是自己编译的驱动需要用测试签名模式或者购买代码签名证书。测试签名模式的命令是bcdedit /set testsigning on重启后生效但桌面右下角会显示测试模式水印。4.4 现象远端接收端报「无效的句柄」错误这个错误通常出现在接收端尝试打开打印机句柄的时候。原因是打印机名称里有特殊字符或者打印机驱动本身有问题。Windows 的打印机名称如果包含中文或空格在某些 API 调用下会出问题。解决方法是把远端打印机的名称改成纯英文加数字不要有空格和特殊符号。如果打印机名称改不了就在 KKPrinter 的配置里做一个名称映射把客户端的打印机名映射到远端的实际打印机名。4.5 现象打印任务重复提交同一份文档打印了两次重复打印的根源一般是重试机制没有配合幂等检查。客户端转发超时后触发重试但第一次的请求其实已经到达远端并打印了重试又打了一次。解决办法就是前面提到的 JobId 机制。客户端每次转发都带同一个 JobId远端处理前先查重。另外客户端的重试逻辑要区分「连接失败」和「请求已发出但响应超时」后者不应该立即重试而是先查询远端状态。5. 进阶技巧把 KKPrinter 做成可配置的打印路由网关5.1 用配置文件驱动打印路由规则KKPrinter 的转发规则不应该硬编码在代码里而是放在一个 JSON 配置文件里这样运维人员改规则不用重新编译。配置文件的结构可以设计成按客户端机器名、按打印机名、按用户组来匹配。{ routes: [ { match: { clientMachine: DESIGN-PC-01, virtualPrinter: KKPrinter }, target: { remoteUrl: http://192.168.10.50:9100/api/print, remotePrinterName: HP LaserJet M1005, retryCount: 3, timeoutSeconds: 60 } }, { match: { clientMachine: *, virtualPrinter: KKPrinter-Warehouse }, target: { remoteUrl: http://192.168.20.30:9100/api/print, remotePrinterName: Zebra ZT410, retryCount: 5, timeoutSeconds: 120 } } ] }match里的clientMachine支持通配符*表示匹配所有客户端。virtualPrinter是客户端上安装的虚拟打印机名称可以装多个 KKPrinter 虚拟打印机实例分别对应不同的远端目标。retryCount和timeoutSeconds按目标打印机的特性来设标签打印机响应快超时可以短一些绘图仪处理大文件慢超时要设长。5.2 打印日志和状态回传KKPrinter 的客户端和接收端都要记录详细的日志包括 JobId、文件名、目标打印机、转发时间、远端响应状态。日志用 JSON 格式写方便后续用 ELK 或者简单的脚本做分析。状态回传方面客户端可以在转发成功后弹一个托盘通知告诉用户「已发送到仓库标签机」。转发失败时弹窗提示并给出重试按钮。这个体验比 Windows 原生共享打印机报一个 0x0000011b 要友好得多。5.3 验证 KKPrinter 是否真正生效的三个方法第一个方法是看日志。客户端日志里应该有「生成 PDF 成功」和「转发成功」两条记录接收端日志里应该有「收到任务」和「提交打印成功」两条记录。四条记录齐全说明链路通了。第二个方法是用一个已知内容的测试页打印比如打印一个带时间戳的文本文件然后去远端打印机取纸核对时间戳和内容。第三个方法是模拟故障。把远端接收端服务停掉然后在客户端打印观察客户端是否进入重试队列日志里是否有重试记录。再把接收端启动看任务是否自动补打。这个测试能验证重试机制是否可靠。5.4 我踩过的一个坑打印机驱动版本不一致导致排版错乱有一次客户反馈同一份 Word 文档在本地打印正常通过 KKPrinter 转发到远端打印出来页边距不对。排查了半天发现是远端机器上的打印机驱动版本和本地不一致导致 PDF 渲染时的页面描述有差异。后来我的习惯是KKPrinter 客户端和接收端所在机器上的打印机驱动版本尽量保持一致尤其是同一型号的打印机。如果做不到就在客户端生成 PDF 时固定页面尺寸和边距不要依赖打印机驱动的默认设置。这个坑让我养成了一个习惯——每次部署 KKPrinter 之前先核对两端打印机驱动的版本号不一致就先统一。希望帮到你。本文还有配套的精品资源点击获取
返回列表