ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IPP网络打印协议实战:从协议栈解析到自动化运维排错

IPP网络打印协议实战:从协议栈解析到自动化运维排错 简介这份资源是IPPInternet Printing Protocol网络打印协议的源码实现包面向网络编程学习者、打印服务开发者以及需要自定义打印方案的系统集成人员。它围绕HTTP/1.1之上的打印作业提交、打印机状态查询、作业控制与属性查询等核心流程展开帮助读者理解跨平台远程打印的报文结构与状态机设计。压缩包共32个文件约35KB以8个c源文件与5个h头文件为主体另有15个svn-base版本文件、Makefile、README及format、entries等辅助文件覆盖协议解析、数据编码、错误处理与连接管理等模块。已有3852人学习下载适合作为研读网络协议与网络编程的实例读者可从中掌握请求响应流程、状态跟踪与安全传输的实现思路并在此基础上扩展自己的打印服务或客户端应用。1. 从「点了打印没反应」说起IPP 协议到底在管什么办公室那台网络打印机又卡住了。电脑上点了打印任务进了队列状态显示「正在打印」然后就没有然后了。重启打印机、重装驱动、换 USB 线折腾一圈最后发现是打印后台服务把任务发到了一个已经失效的 IPP 端口上。这种场景做运维的应该都不陌生。IPP全称 Internet Printing Protocol中文叫网络打印协议。它跑在 HTTP 之上默认端口 631用一套标准的请求-响应模型把「打印什么、打到哪、怎么打」这三件事说清楚。你可以把它理解成打印世界的 HTTP客户端发一个 POST 请求把打印任务描述和文档数据打包塞进 body服务端解析后决定是直接打、排队还是拒绝。它不关心你用的是 Windows、macOS 还是 Linux也不关心打印机是惠普还是兄弟只要双方都认 IPP就能对话。这份资源围绕 IPP 协议展开适合三类人一是被网络打印问题反复折磨的运维想搞清楚底层到底发生了什么二是需要把打印能力集成进自己系统的开发者比如做 SaaS 工单系统要支持直接出票三是想用脚本批量管理打印队列的自动化工程师。它解决的核心问题是让你不再依赖厂商驱动和图形界面直接用协议层的能力控制打印流程。2. IPP 协议栈拆解从 HTTP 报文到打印语义2.1 为什么是 HTTP 而不是自定义 TCP 协议IPP 最早在 RFC 2566 里定义后来演进到 RFC 8010 和 8011。它选择 HTTP/1.1 作为传输层不是偷懒是深思熟虑的结果。HTTP 有成熟的代理穿透能力、认证框架和内容协商机制打印场景需要的「大文件上传」「状态轮询」「访问控制」都能直接复用。更关键的是防火墙对 631 端口的放行策略通常比自定义端口好配。一个典型的 IPP 请求长这样POST /ipp/print HTTP/1.1 Host: 192.168.1.200:631 Content-Type: application/ipp Content-Length: 1234 二进制 IPP 载荷注意 Content-Type 是application/ipp不是常见的application/json或text/plain。服务端看到这个类型就知道 body 里是 IPP 编码的数据需要按协议规范解析。响应同样是 HTTP 报文状态码可能是 200成功、400请求格式错、401未认证或 500打印机内部错误。2.2 IPP 载荷的二进制结构IPP 载荷不是人类可读的文本而是一套紧凑的二进制编码。它由三部分组成版本号、操作码/状态码、请求 ID然后是属性组最后是文档数据如果有。属性组又分操作属性、作业属性、打印机属性等每个属性用「标签 名称长度 名称 值长度 值」的格式排列。用 Python 解析一个 IPP 响应的开头部分import struct def parse_ipp_header(data): # 前两个字节主版本号和次版本号 major, minor data[0], data[1] # 接下来两个字节操作码请求或状态码响应 status struct.unpack(H, data[2:4])[0] # 请求 ID用于匹配请求和响应 request_id struct.unpack(I, data[4:8])[0] return { version: f{major}.{minor}, status: hex(status), request_id: request_id } # 假设 raw 是从 HTTP body 里读到的二进制数据 # result parse_ipp_header(raw) # print(result) # {version: 2.0, status: 0x0, request_id: 1}这段代码只解析了头部 8 个字节。status为0x0表示成功0x0400到0x04FF是客户端错误0x0500以上是服务端错误。实际排错时先看这个状态码能快速判断是请求发错了还是打印机自己有问题。2.3 操作码Print-Job 与 Get-Printer-AttributesIPP 定义了几十种操作码日常用得最多的两个是Print-Job0x0002和Get-Printer-Attributes0x000B。前者提交打印任务后者查询打印机能力比如支持哪些纸张尺寸、是否支持双面、彩色还是黑白。用curl发一个 Get-Printer-Attributes 请求# 构造一个最小的 IPP 请求体查询打印机属性 # 版本 2.0操作码 0x000B请求 ID 1 printf \x02\x00\x00\x0b\x00\x00\x00\x01\x01\x47\x00\x12attributes-charset\x00\x05utf-8\x03 /tmp/ipp_query.bin curl -X POST http://192.168.1.200:631/ipp/print \ -H Content-Type: application/ipp \ --data-binary /tmp/ipp_query.bin \ -o /tmp/ipp_response.bin \ -w %{http_code}\nprintf那行手工拼了一个 IPP 请求\x02\x00是版本 2.0\x00\x0b是操作码\x00\x00\x00\x01是请求 ID后面跟着属性组。\x01是操作属性组的起始标签\x47是 charset 属性的标签后面是名称长度和值。这种手工拼包的方式适合调试生产环境建议用现成库。2.4 用 Python 库简化操作手工拼二进制容易出错pyipp这类库把细节封装好了import asyncio from pyipp import IPP async def query_printer(): # 连接打印机注意端口和路径 printer IPP(host192.168.1.200, port631, base_path/ipp/print) # 获取打印机属性 attrs await printer.get_printer_attributes() # 打印支持的文档格式 print(attrs.get(document-format-supported)) # 打印打印机状态 print(attrs.get(printer-state)) asyncio.run(query_printer())get_printer_attributes()返回一个字典键是属性名值是解析后的 Python 对象。document-format-supported告诉你打印机认哪些格式常见的有application/pdf、image/jpeg、application/postscript。如果传了不在列表里的格式打印机会返回client-error-document-format-not-supported任务直接失败。3. 从零跑通一次 IPP 打印环境、代码与验证3.1 环境准备与打印机发现在动手之前先确认打印机开了 IPP。大多数网络打印机在 Web 管理界面里有个「IPP」或「AirPrint」开关打开后 631 端口才会监听。用nmap扫一下nmap -p 631 192.168.1.200 # 如果显示 open说明 IPP 服务在跑如果不知道打印机 IP可以用avahi-browse或ippfind发现局域网里的 IPP 设备# ippfind 是 CUPS 自带的工具能列出所有可发现的 IPP 打印机 ippfind --timeout 5 # 输出类似ipp://HP-LaserJet.local:631/ipp/printippfind依赖 mDNS/DNS-SD如果网络里禁了多播可能扫不到。这时候只能手动查打印机面板上的 IP或者翻路由器 DHCP 客户端列表。3.2 提交一个 PDF 打印任务假设打印机 IP 是192.168.1.200要打一个test.pdf。用pyipp提交import asyncio from pyipp import IPP async def print_pdf(file_path): printer IPP(host192.168.1.200, port631, base_path/ipp/print) # 读取 PDF 文件 with open(file_path, rb) as f: document f.read() # 提交打印任务指定文档格式和任务名称 job await printer.print_job( documentdocument, document_formatapplication/pdf, job_nametest-print ) # 返回任务 ID可以用来查状态 print(fJob ID: {job.job_id}) print(fJob State: {job.job_state}) asyncio.run(print_pdf(test.pdf))print_job()内部会构造一个 Print-Job 请求把 PDF 二进制塞进载荷末尾。document_format必须和打印机支持的格式匹配否则会被拒。job_name是显示在打印机面板上的任务名方便识别。提交成功后返回的job_id可以用来轮询任务状态比如查是pending、processing还是completed。3.3 轮询任务状态与取消任务提交完不是就没事了得确认任务真的打完了async def check_job(printer, job_id): # 查询指定任务的状态 job_attrs await printer.get_job_attributes(job_id) state job_attrs.get(job-state) # job-state 是数字4 表示 pending5 表示 processing9 表示 completed state_map {3: pending, 4: pending-held, 5: processing, 9: completed, 8: canceled} print(fJob {job_id}: {state_map.get(state, state)}) return state async def cancel_job(printer, job_id): # 取消一个正在排队的任务 await printer.cancel_job(job_id) print(fJob {job_id} canceled)job-state的取值在 RFC 8011 里有完整定义。常见的是 3pending、5processing、9completed。如果卡在 5 很久不动可能是打印机缺纸、卡纸或者网络断了。这时候用cancel_job()把任务撤掉避免队列堵死。3.4 用 CUPS 做中间层如果不想在代码里直接处理 IPP可以用 CUPS 当中间层。CUPS 本身就是一个 IPP 服务端和客户端能把 IPP 请求转成 USB 或并口输出。配置方法# 添加一台 IPP 打印机到 CUPS lpadmin -p OfficePrinter -E -v ipp://192.168.1.200:631/ipp/print -m everywhere # 设置默认打印机 lpoptions -d OfficePrinter # 打印测试页 lp -d OfficePrinter /usr/share/cups/data/testprint-m everywhere让 CUPS 自动从打印机拉取 PPDPostScript Printer Description信息不用手动选驱动。lp命令提交的任务会先到 CUPS 队列再由 CUPS 转成 IPP 发给打印机。这种架构的好处是统一管理多台打印机坏处是多了一层出问题时排查链路变长。4. IPP 排错避坑从端口不通到任务卡死4.1 端口 631 不通但打印机 ping 得通现象ping 192.168.1.200有回应但curl http://192.168.1.200:631超时。原因打印机 Web 管理界面能开不代表 IPP 服务在跑。很多打印机默认只开 9100RAW 打印和 80Web631 需要手动启用。另外有些企业级交换机做了端口隔离只放行特定端口。解决登录打印机 Web 界面找「网络设置」或「协议设置」把 IPP 或 AirPrint 打开。如果是交换机隔离找网管放行 631。临时验证可以用telnet 192.168.1.200 631通了再上 curl。4.2 返回 401 未认证但没设过密码现象IPP 请求返回 HTTP 401打印机要求认证。原因部分打印机默认开启 IPP 认证要求客户端提供用户名密码但管理员没配过。或者打印机固件升级后重置了认证策略。解决在打印机 Web 界面关掉 IPP 认证或者在请求里带上 Basic Auth。用pyipp的话构造 IPP 对象时传username和password参数。如果不知道密码试试默认的admin/admin或root/root但别在生产环境用默认密码。4.3 任务提交成功但打印机没动静现象print_job()返回了 job_id但打印机面板上没显示任务纸也没出来。原因最常见的是文档格式不匹配。打印机声明支持application/pdf但实际固件只认application/postscript。另一个可能是任务被发到了错误的队列路径比如/ipp/print写成了/ipp/printer。解决先用get_printer_attributes()确认document-format-supported列表选一个打印机真正支持的格式。路径问题可以用ippfind输出的 URL 做基准别自己猜。如果格式和路径都对查打印机日志看是不是内存不足导致任务被静默丢弃。4.4 大文件打印到一半卡住现象打印一个 50MB 的 PDF打到第 10 页停了打印机显示「正在接收数据」。原因IPP 默认用 HTTP/1.1没有分块传输时整个文档要一次性发完。打印机缓冲区小的话大文件会撑爆内存。另外网络抖动导致 TCP 重传也会让传输卡住。解决在请求里启用分块传输或者把大文件拆成多个小任务。pyipp目前不支持分块可以用requests库手动构造带Transfer-Encoding: chunked的请求。更简单的办法是先用gs把 PDF 压缩一遍减小体积。4.5 打印出来的中文是乱码现象PDF 里的中文在打印机上变成方块或问号。原因打印机固件没有内置中文字体或者 IPP 请求里指定的attributes-charset不是utf-8。有些打印机只认us-ascii遇到非 ASCII 字符就替换成占位符。解决确认打印机支持utf-8在请求里显式设置attributes-charset为utf-8。如果打印机就是不支持中文只能把 PDF 转成图片再打印用pdftoppm把每页转成 PNG然后按图片格式提交。5. 进阶用 IPP 做打印队列监控与自动化5.1 订阅打印机事件IPP 有个不太常用但很有用的操作Create-Printer-Subscriptions0x0016。它让客户端订阅打印机的事件通知比如「任务完成」「缺纸」「卡纸」。这样不用轮询打印机主动推状态过来。async def subscribe_events(printer): # 订阅打印机事件通知方式为 URI subscription await printer.create_subscription( recipient_urihttp://192.168.1.100:8080/ipp-events, event_groups[job-state-changed, printer-state-changed] ) print(fSubscription ID: {subscription.subscription_id}) # 之后打印机有状态变化会 POST 到 recipient_urirecipient_uri是你自己起的一个 HTTP 服务用来接收打印机发来的事件通知。event_groups指定订阅哪些事件类型。这种方式适合做集中监控一台服务器订阅多台打印机状态变化实时上报。5.2 批量查询多台打印机状态运维场景里经常要巡检一批打印机。用asyncio并发查import asyncio from pyipp import IPP PRINTERS [ 192.168.1.200, 192.168.1.201, 192.168.1.202, ] async def check_one(ip): try: printer IPP(hostip, port631, base_path/ipp/print) attrs await printer.get_printer_attributes() state attrs.get(printer-state) # printer-state: 3idle, 4processing, 5stopped return ip, state except Exception as e: return ip, ferror: {e} async def check_all(): tasks [check_one(ip) for ip in PRINTERS] results await asyncio.gather(*tasks) for ip, state in results: print(f{ip}: {state}) asyncio.run(check_all())printer-state为 3 是空闲4 是工作中5 是停止通常意味着缺纸、卡纸或门没关。巡检脚本可以定时跑发现状态 5 就告警。注意asyncio.gather会等所有任务完成如果某台打印机超时整个巡检会被拖慢。可以加asyncio.wait_for设超时。5.3 用 IPP 实现「打印即归档」一个实际需求每次打印任务提交后自动把文档存一份到归档目录。可以在print_job()之前先保存文件或者用 CUPS 的tea过滤器在打印流程中拦截。更干净的做法是封装一个函数import hashlib from datetime import datetime async def print_and_archive(printer, file_path, archive_dir): with open(file_path, rb) as f: document f.read() # 计算文件哈希用于去重和校验 file_hash hashlib.sha256(document).hexdigest()[:16] # 归档文件名带时间戳和哈希 timestamp datetime.now().strftime(%Y%m%d%H%M%S) archive_path f{archive_dir}/{timestamp}_{file_hash}.pdf with open(archive_path, wb) as f: f.write(document) # 提交打印 job await printer.print_job( documentdocument, document_formatapplication/pdf, job_namefarchive-{file_hash} ) return job.job_id, archive_path这个模式在医疗、金融场景里很常见打印和归档必须同时成功或同时失败。实际部署时归档目录要放在共享存储上避免单点故障。哈希值可以用来查重防止同一份文档被重复打印多次。5.4 验证 IPP 实现是否符合规范如果你在开发 IPP 客户端或服务端可以用ipptool做一致性测试。它是 CUPS 自带的工具能发标准请求并校验响应# 测试打印机是否支持 Get-Printer-Attributes ipptool -tv ipp://192.168.1.200:631/ipp/print get-printer-attributes.test # 测试 Print-Job 操作 ipptool -tv ipp://192.168.1.200:631/ipp/print print-job.testipptool的测试文件在/usr/share/cups/ipptool/目录下可以自己改。输出里会标出哪些属性缺失、哪些值不符合预期。我一般会在集成前跑一遍get-printer-attributes.test确认打印机至少支持 IPP/2.0 的核心属性集。从那以后我每次接入新打印机都强制走一遍ippfind发现、ipptool验证、pyipp提交测试页这三步不再凭经验猜端口和路径。希望帮到你。本文还有配套的精品资源点击获取
返回列表