多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java Socket 直连爱普生 9100 端口:ESC/POS 打印服务实战

Java Socket 直连爱普生 9100 端口:ESC/POS 打印服务实战 简介这份资源面向Java后端开发者、企业信息化实施人员及需要对接票据打印场景的技术人员提供一套基于Java Socket与爱普生打印机9100端口通信的Web打印服务方案解决网络环境下打印任务提交、指令控制与队列调度的实际问题。压缩包共4个文件包含1个Java源码、1个Markdown说明、1个txt文档和1个docx资料整体约37KB体量轻便便于快速阅读与二次开发。系统以ESC/POS指令集为核心覆盖打印头控制、字符与图形输出、条码打印并集成自动切纸、钱箱开启、打印队列管理与优先级调整等高级功能同时设计了用户认证授权与网络异常下的任务重提机制。目前已有37人学习下载适合希望理解Socket打印通信原理、掌握ESC/POS指令封装思路或搭建Web打印服务的读者参考可从中获取完整的通信流程设计、队列调度逻辑与安全控制实现要点。1. 从一张小票到一套服务Java Socket 直连爱普生 9100 端口到底能干什么很多做零售、餐饮、仓储系统的同行都遇到过这种需求Web 后台点一下「打印」前台那台爱普生热敏小票机就得出纸还要顺带切纸、弹钱箱。用厂商驱动吧部署到 Linux 服务器上各种水土不服用 CUPS 吧切纸和钱箱指令又不好塞进去。真正稳的做法是绕开驱动用 Java 的 Socket 直接连打印机的 9100 端口把 ESC/POS 指令裸发过去。这份资源就是围绕这套思路做的一个 Web 打印服务系统把打印控制、切纸、钱箱开启和网络打印队列管理打包在一起。它适合谁适合手里有爱普生网络小票机、后端是 Java、又不想被驱动和操作系统绑死的团队。下面我按「这东西怎么跑起来、参数怎么设、坑在哪」拆开讲。2. 9100 端口与 ESC/POS先搞懂数据是怎么到打印机的2.1 为什么是 9100 端口而不是共享打印机爱普生这类网络小票机出厂基本都带一个 RAW 打印端口默认就是 9100。它的本质是一个极简的 TCP 服务你连上去把字节流写进去打印机就照着字节流干活中间没有驱动、没有假脱机、没有操作系统那一层。这跟 Windows 共享打印机那个经常报 0x0000011b、0x000000040 的玩意儿完全是两条路。共享打印走的是 SMB 协议栈依赖主机在线、依赖驱动匹配跨平台一塌糊涂而 9100 是纯 TCPJava 一个Socket就能搞定Linux、Windows、容器里行为一致。代价是9100 端口只认字节不认「文档」。你发过去的是 PDF它就打出一堆乱码你发过去的是 ESC/POS 指令它才正常出票。所以这套方案的核心不是「打印文件」而是「拼指令」。常见做法是后端把订单数据渲染成 ESC/POS 字节数组再通过 Socket 推给打印机。这也是为什么这份资源值得看——它把「拼指令」和「发指令」这两件事工程化了。2.2 ESC/POS 指令的最小可用集合ESC/POS 是爱普生的一套控制指令集指令以ESC0x1B、GS0x1D等控制字符开头。你不需要背全先掌握四类就够跑通业务功能指令十六进制说明初始化1B 40每次打印前发一次清状态对齐1B 61 nn0 左、1 中、2 右切纸1D 56 mm0 全切、1 半切开钱箱1B 70 m tm0 对应钱箱口 2t 为脉冲时间中文1C 26等需配合 GBK 编码见后文这里有个反直觉的点切纸和开钱箱不是「打印内容」而是独立指令可以单独发。也就是说你完全可以只连一次 Socket先发开钱箱指令再发小票内容最后发切纸指令一气呵成。很多新手会分三次连接去发结果钱箱弹了票没出或者票出了钱箱没弹——问题就出在连接时序上。2.3 用 Java 建立第一条打印连接先写一个最小可跑的发送类验证网络和指令通路import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; public class RawPrinterClient { // 打印机 IP 与 9100 端口 private static final String PRINTER_IP 192.168.1.50; private static final int PRINTER_PORT 9100; public static void send(byte[] payload) throws Exception { try (Socket socket new Socket()) { // 连接超时 3 秒写超时 5 秒避免线程被挂死 socket.connect(new InetSocketAddress(PRINTER_IP, PRINTER_PORT), 3000); socket.setSoTimeout(5000); OutputStream out socket.getOutputStream(); out.write(payload); out.flush(); } } public static void main(String[] args) throws Exception { byte[] init {0x1B, 0x40}; // 初始化 byte[] text 测试小票\n.getBytes(GBK); // 中文用 GBK byte[] cut {0x1D, 0x56, 0x00}; // 全切 byte[] all new byte[init.length text.length cut.length]; System.arraycopy(init, 0, all, 0, init.length); System.arraycopy(text, 0, all, init.length, text.length); System.arraycopy(cut, 0, all, init.length text.length, cut.length); send(all); } }逻辑说明connect带超时是必须的否则打印机断电时线程会一直阻塞。setSoTimeout控制写阻塞的上限。指令拼接用System.arraycopy顺序拼成一个大字节数组一次性写出避免多次write之间被打印机缓冲打断。参数说明PRINTER_IP换成你打印机的实际地址中文务必用GBK用UTF-8会打出乱码这是血泪经验。跑通这一步说明网络和指令通路没问题再往上叠业务逻辑。3. 把裸指令封装成 Web 打印服务队列、切纸与钱箱3.1 打印队列为什么不能省单机测试时你直接new Socket发就完事了。但上了生产多个收银台、多个订单并发过来如果每个请求都自己开 Socket会出现两个问题一是打印机同一时刻只处理一条连接并发写会互相插队打出来的票内容错乱二是打印机短暂离线时请求直接失败订单丢了。所以必须有一个队列层把「要打印的任务」排好队串行地喂给打印机。常见做法是用BlockingQueue做内存队列配一个常驻消费线程。任务对象里存字节数组和目标打印机标识。消费线程从队列取任务取不到就阻塞等待取到了就发。这样并发请求进来只是入队不会直接碰 Socket。要注意队列要有上限防止打印机长时间离线导致内存被任务撑爆——我一般设 1000 条满了就拒绝并记录而不是无限堆积。3.2 切纸与钱箱的指令时序切纸和开钱箱最容易翻车的地方是时序。正确顺序是初始化 → 打印内容 → 走纸可选→ 切纸 → 开钱箱。钱箱指令放在切纸之后是因为很多机型在切纸动作完成前不响应钱箱脉冲。如果你把钱箱指令放在最前面可能票还没打完钱箱就弹了或者干脆不弹。public class EscPosBuilder { private final ByteArrayOutputStream buffer new ByteArrayOutputStream(); public EscPosBuilder init() { buffer.write(0x1B); buffer.write(0x40); return this; } public EscPosBuilder text(String s) throws Exception { buffer.write(s.getBytes(GBK)); return this; } public EscPosBuilder feed(int lines) { for (int i 0; i lines; i) buffer.write(0x0A); return this; } public EscPosBuilder cut() { buffer.write(0x1D); buffer.write(0x56); buffer.write(0x00); return this; } public EscPosBuilder openCashDrawer() { // m0 钱箱口2t50 脉冲时间 buffer.write(0x1B); buffer.write(0x70); buffer.write(0x00); buffer.write(0x32); return this; } public byte[] build() { return buffer.toByteArray(); } }逻辑说明用ByteArrayOutputStream做链式拼装比手动arraycopy可读性好得多。feed走纸几行再切能避免切到最后一行的字。参数说明openCashDrawer里的0x32是脉冲时间 50ms太短钱箱弹不开太长可能损伤线圈50 是常见值。切纸指令0x00是全切如果打印机是半切机型改成0x01。3.3 队列消费线程与失败重试消费线程的核心是「取任务、发任务、失败处理」三步。失败处理不能简单丢弃也不能无限重试。我的习惯是发送失败后把任务重新入队但给任务加一个重试计数超过 3 次就落到一个失败表里人工介入。这样既不会因为打印机临时抖动丢单也不会因为打印机彻底坏了把队列堵死。public class PrintWorker implements Runnable { private final BlockingQueuePrintTask queue; private final RawPrinterClient client new RawPrinterClient(); public PrintWorker(BlockingQueuePrintTask queue) { this.queue queue; } Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { PrintTask task queue.take(); // 阻塞取任务 try { client.send(task.getPayload()); } catch (Exception e) { task.increaseRetry(); if (task.getRetry() 3) { queue.offer(task); // 重新入队 } else { // 落失败表记录 task.getId() } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }逻辑说明queue.take()在没有任务时阻塞不消耗 CPU。重试用offer而不是put避免队列满时再次阻塞。参数说明重试上限 3 次是经验值打印机重启一般几秒内完成3 次足够覆盖。失败表建议存任务 ID 和原始字节方便补打。4. 避坑与排查那些让打印服务半夜挂掉的细节4.1 中文乱码编码不是 UTF-8 而是 GBK现象小票上中文全是问号或方块。原因ESC/POS 打印机内置字库多为 GBK你发 UTF-8 字节它按 GBK 解析自然乱码。解决所有中文内容统一getBytes(GBK)包括商品名、备注。如果确实要打生僻字得用位图指令那是另一个话题。4.2 端口占用报错每个套接字地址只允许使用一次现象日志里出现「通常每个套接字地址(协议/网络地址/端口)只允许使用一次」。原因上一个 Socket 没关闭或者你用了固定本地端口去连。解决客户端 Socket 不要绑定本地端口让系统自动分配确保try-with-resources关闭连接。这个报错在 Windows 上尤其常见Linux 上表现为连接超时。4.3 打印机离线导致线程挂死现象打印机断电后消费线程卡住后续任务全部积压。原因connect或write没有超时TCP 在等对端响应。解决connect设 3 秒超时setSoTimeout设 5 秒超时抛异常走重试逻辑。注意setSoTimeout对write不一定生效必要时用独立线程加Future.get(timeout)包一层。4.4 切纸切到一半或切不断现象小票最后一行被切掉或者纸没完全断。原因切纸前没有走纸或者切纸指令的 m 值跟机型不匹配。解决切纸前feed(3)走三行确认机型支持全切还是半切全切用0x00半切用0x01。部分机型还需要先发GS V 66 n带进纸的切纸指令。4.5 钱箱不弹指令对了但没反应现象指令发了钱箱纹丝不动。原因钱箱接口选错钱箱口 1 还是口 2或者脉冲时间太短。解决确认钱箱插在打印机的哪个口1B 70 00 32对应口 2口 1 是1B 70 01 32脉冲时间从 50ms 往上试到 100ms。另外有些机型在切纸动作完成前不响应钱箱务必把钱箱指令放在切纸之后。5. 进阶把打印服务做成可观测、可扩展的组件5.1 用状态探测代替盲目重试上面那套重试逻辑有个盲区打印机到底是「临时抖动」还是「彻底离线」队列并不知道。我后来加了一个轻量探测消费线程在连续失败 3 次后不再无脑重试而是先尝试连一次 9100 端口连不上就进入「离线等待」状态每 10 秒探一次恢复后再继续消费队列。这样避免了打印机拔线后队列空转重试把日志刷爆。private boolean isPrinterOnline() { try (Socket s new Socket()) { s.connect(new InetSocketAddress(PRINTER_IP, PRINTER_PORT), 2000); return true; } catch (Exception e) { return false; } }逻辑说明只做连接探测不发数据对打印机无副作用。参数说明探测超时 2 秒比业务发送的 3 秒短避免探测本身拖慢恢复判断。5.2 多打印机路由与队列隔离当系统里有前台小票机、后厨打印机、标签机多台设备时不能共用一个队列。做法是按打印机标识建多个队列每个队列配一个消费线程互不干扰。任务入队时根据业务类型路由到对应队列。这样后厨打印机坏了不会影响前台出票。路由表可以放配置文件改打印机 IP 不用改代码。队列标识打印机角色典型指令差异front前台小票机含切纸、钱箱kitchen后厨打印机只打印不切纸label标签机用 TSPL 而非 ESC/POS5.3 验证方法别等上线才发现打不出来我现在的习惯是任何打印相关改动上线前必须走一遍「三连测」第一用main方法直接发一条测试指令确认硬件通路第二灌 50 条并发任务进队列看是否串行、是否错乱第三拔掉打印机网线发 10 条任务再插回网线看是否自动恢复并补打。这三步能覆盖 90% 的翻车场景。从那以后我每次改打印逻辑都强制走一遍这个流程再也没在半夜被叫起来过。希望帮到你。本文还有配套的精品资源点击获取
返回列表