ARTICLE DETAIL

资讯详情

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

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

Java Socket 直连爱普生 9100 端口:ESC/POS 指令与 Web 打印服务实战 简介这是一套面向Java后端开发者与打印系统集成人员的Web打印服务源码方案聚焦爱普生打印机9100端口的ESC/POS指令通信解决网络环境下打印任务提交、队列调度与设备控制的工程化问题。资源包共4个文件包含Java核心实现、Markdown说明文档、txt说明文件与docx附赠资料压缩包约37KB体量轻便便于快速阅读与二次开发。系统通过Socket与打印机建立稳定连接支持文本、图形与条码打印并集成自动切纸位置设置、钱箱开启控制以及打印队列的排队、优先级调整与错误重试机制同时提供用户认证与授权以保障访问安全。目前已有37人学习下载适合需要理解ESC/POS指令封装、Web端打印任务管理或搭建企业打印服务的读者参考可从中获取通信协议实现思路、队列调度逻辑与安全控制设计等可复用经验。1. 从一张小票说起Java Socket 直连爱普生 9100 端口到底解决了什么电商后台点一下「打印发货单」仓库那台爱普生 TM 系列针式/热敏机就吐纸、切纸、弹钱箱——这套动作背后很多团队最后都收敛到同一个方案用 Java 的 Socket 直接连打印机的 9100 端口把 ESC/POS 指令字节流怼进去。它绕开了操作系统驱动、绕开了浏览器打印对话框也绕开了「共享打印机 0x0000011b」这类玄学报错。适合谁做 Web 打印服务、门店收银、仓储面单、餐饮后厨单的 Java 工程师。这篇就把这条链路从端口、指令、切纸、钱箱到队列管理按能复现的粒度讲透。标题里几个词得先对齐9100 是打印机常见的 RAW 打印端口走的是 TCPESC/POS 是爱普生定义的指令集切纸、开钱箱、加粗、二维码都是靠特定字节序列触发的Web 打印服务系统则是把「浏览器请求 → 后端组指令 → Socket 发字节」串起来的那层。热词里 socket 网络编程、web 页面 pdf 打印、打印机脱机这些本质都是这条链路上不同环节的痛点。下面按「先立原理、再动手、最后避坑」推。2. 9100 端口与 ESC/POS先搞懂字节是怎么变成纸的2.1 为什么是 RAW 9100 而不是驱动打印传统做法是装厂商驱动应用调系统打印 API驱动把内容转成打印机语言。问题在于Web 服务跑在 Linux 服务器上服务器未必装得上爱普生驱动就算装了多门店、多型号驱动版本一乱就是「打印机脱机」「0x000000040 无法连接」的连环坑。9100 RAW 模式把中间层砍掉——打印机内置网络模块监听 TCP 9100谁连上来、发什么字节它就按 ESC/POS 解释。你发的是指令不是「文档」所以不依赖驱动、不依赖操作系统打印子系统。代价是排版得自己算。字体大小、行距、对齐、条码全要拼字节。这也是为什么很多人第一次直连打出来是一堆乱码或者半张纸——因为发的是 UTF-8 文本而打印机在等 ESC/POS 指令。2.2 ESC/POS 指令的最小结构ESC/POS 指令基本是「引导字节 参数」的形式。ESC 是 0x1BGS 是 0x1DFS 是 0x1C。几个必须记住的功能指令字节说明初始化1B 40复位打印机状态每次任务开头建议发切纸1D 56 42 00全切42 是走纸后切00 是切当前位置开钱箱1B 70 00 19 FA引脚2脉冲常见配置居中对齐1B 61 0100左 01中 02右加粗1B 45 0101开 00关中文模式1C 26进入汉字模式部分机型注意不同爱普生型号对切纸、钱箱引脚的支持有差异TM-T88 和 TM-U220 的切纸行为就不完全一样。所以参数不能照抄得对着机型手册核。2.3 中文与编码乱码的根因ESC/POS 打印机大多不认 UTF-8。中文要么走 GBK/GB18030 编码要么走打印机内置字库的区位码。常见做法是文本用 GBK 编码成字节再拼进指令流。如果打印机是纯英文机型中文会直接变问号或乱码这时候要么换机型要么用位图模式把文字渲染成图片再打。这一步是新手翻车最集中的地方。3. 用 Java Socket 打通打印链路从连接池到指令拼装3.1 最小可跑通的连接与发送先写一个能连上、能发字节、能关掉的最小版本别一上来就上框架。import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; public class RawPrintClient { public static void main(String[] args) throws Exception { String ip 192.168.1.87; int port 9100; // 连接超时和读超时都要设否则打印机掉线会挂死线程 try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(ip, port), 3000); socket.setSoTimeout(3000); OutputStream out socket.getOutputStream(); out.write(new byte[]{0x1B, 0x40}); // 初始化 out.write(测试小票\n.getBytes(GBK)); // 中文用 GBK out.write(new byte[]{0x1B, 0x61, 0x01}); // 居中 out.write(订单号 20240501\n.getBytes(GBK)); out.write(new byte[]{0x1B, 0x61, 0x00}); // 左对齐 out.write(new byte[]{0x1D, 0x56, 0x42, 0x00}); // 切纸 out.write(new byte[]{0x1B, 0x70, 0x00, 0x19, 0xFA}); // 开钱箱 out.flush(); } } }逻辑说明connect带超时避免打印机不在线时线程卡死setSoTimeout防止读阻塞所有指令按顺序写入同一个 OutputStream最后 flush。参数说明IP 换成打印机实际地址端口固定 9100GBK 是编码关键UTF-8 会乱码切纸指令1D 56 42 00里的 42 表示「走纸到切刀位置再切」如果只想切当前位置用1D 56 00。3.2 连接池与并发别每次打印都新建 Socket单机测试没问题一上生产就暴露每次打印 new 一个 Socket高并发下端口耗尽报「通常每个套接字地址只允许使用一次」。正确做法是每台打印机维护一个连接池或者至少复用长连接。import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; public class PrinterPool { private static final ConcurrentHashMapString, LinkedBlockingQueueSocket POOLS new ConcurrentHashMap(); public static Socket borrow(String ip) throws Exception { LinkedBlockingQueueSocket q POOLS.computeIfAbsent(ip, k - new LinkedBlockingQueue(5)); Socket s q.poll(); if (s null || s.isClosed()) { s new Socket(); s.connect(new java.net.InetSocketAddress(ip, 9100), 3000); s.setSoTimeout(3000); } return s; } public static void release(String ip, Socket s) { if (s null || s.isClosed()) return; LinkedBlockingQueueSocket q POOLS.get(ip); if (q ! null !q.offer(s)) { try { s.close(); } catch (Exception ignored) {} } } }逻辑说明按 IP 分池每池上限 5 个连接借出时若连接已关闭则重建归还时池满就关掉。参数说明池大小按打印机并发量调热敏机一般 2~3 就够针式机慢池开大反而堆积。注意打印机长时间空闲会主动断连所以借出后建议先探测一次或者发送前捕获异常重建。3.3 指令拼装封装成 Builder散落的out.write难维护封装成链式 Builder把切纸、钱箱、对齐都变成方法。import java.io.ByteArrayOutputStream; import java.io.IOException; public class EscPosBuilder { private final ByteArrayOutputStream buf new ByteArrayOutputStream(); public EscPosBuilder init() { buf.write(new byte[]{0x1B, 0x40}, 0, 2); return this; } public EscPosBuilder align(int a) { buf.write(new byte[]{0x1B, 0x61, (byte) a}, 0, 3); return this; } public EscPosBuilder bold(boolean on) { buf.write(new byte[]{0x1B, 0x45, (byte)(on?1:0)}, 0, 3); return this; } public EscPosBuilder text(String s) { try { buf.write(s.getBytes(GBK)); } catch (IOException e) { throw new RuntimeException(e); } return this; } public EscPosBuilder cut() { buf.write(new byte[]{0x1D, 0x56, 0x42, 0x00}, 0, 4); return this; } public EscPosBuilder cashDrawer() { buf.write(new byte[]{0x1B, 0x70, 0x00, 0x19, 0xFA}, 0, 5); return this; } public byte[] build() { return buf.toByteArray(); } }逻辑说明所有指令写进内存缓冲最后一次性build()拿到完整字节数组再交给 Socket 发送。参数说明align传 0/1/2bold传布尔切纸和钱箱指令固定但钱箱引脚参数19 FA需按机型确认。这样业务层只关心「打什么」不关心字节。4. Web 打印服务与队列管理把请求变成有序的纸4.1 后端接口设计Web 打印服务的典型形态前端 POST 一个打印任务 JSON后端组指令、入队、异步发送。接口要幂等避免用户狂点导致重复打印。// 简化示意非完整 Spring 代码 public class PrintTask { public String printerIp; public String content; public boolean needCut; public boolean openDrawer; public String taskId; // 幂等键 }逻辑说明taskId 用于去重同一 taskId 在队列未完成前不重复入队。参数说明printerIp 决定路由到哪台机器needCut/openDrawer 控制是否追加切纸和钱箱指令。4.2 队列与顺序保证同一台打印机必须串行发送否则两张小票的字节会交错打出来就是乱码。做法是每台打印机一个单线程执行器任务进阻塞队列。import java.util.concurrent.*; public class PrinterQueue { private static final ConcurrentHashMapString, ExecutorService EXECUTORS new ConcurrentHashMap(); public static void submit(String ip, Runnable task) { ExecutorService es EXECUTORS.computeIfAbsent(ip, k - Executors.newSingleThreadExecutor()); es.submit(task); } }逻辑说明newSingleThreadExecutor保证同一打印机任务串行。参数说明执行器按 IP 缓存进程退出时记得 shutdown。注意队列要有上限否则打印机卡纸时任务无限堆积内存爆掉。4.3 失败重试与状态回传打印失败要能感知。发送后可以读打印机状态部分机型支持 DLE EOT 状态查询或者简单点捕获 IO 异常标记任务失败前端轮询任务状态。public void sendWithRetry(String ip, byte[] data, int maxRetry) { for (int i 0; i maxRetry; i) { try (Socket s new Socket()) { s.connect(new java.net.InetSocketAddress(ip, 9100), 3000); s.getOutputStream().write(data); s.getOutputStream().flush(); return; // 成功 } catch (Exception e) { if (i maxRetry) throw new RuntimeException(打印失败, e); } } }逻辑说明重试要新建连接因为旧连接可能已坏。参数说明maxRetry 建议 1~2重试太多会重复出纸。注意重试前最好确认上一次是否真的没打出来否则会重复打印——这是血泪经验切纸指令发出去但连接断在中间纸可能已经吐了。5. 避坑与排查那些让打印服务半夜报警的细节5.1 中文乱码或打问号现象小票上中文变成????或方块。原因用了 UTF-8 编码打印机字库不认。解决文本统一getBytes(GBK)若机型无中文字库改用位图指令GS v 0把文字渲染成图片发送。5.2 切纸不动作或切一半现象指令发了纸没切或只切一半。原因机型不支持1D 56 42 00或切刀位置参数不对。解决查机型手册TM-T88 系列常用1D 56 42 00部分机型用1D 56 00确认纸张类型热敏 vs 针式和切刀模式。5.3 钱箱不弹现象开钱箱指令发了没反应。原因钱箱接的是打印机 RJ11 口引脚定义2 或 5和脉冲参数不匹配。解决确认钱箱接线引脚调整1B 70后的参数有些机型需要先发初始化再发开箱。5.4 连接被拒或超时现象Connection refused或SocketTimeoutException。原因打印机 IP 变了、脱机、或 9100 端口被防火墙拦。解决先telnet ip 9100测通检查打印机网络配置服务器出站规则放行 9100。5.5 并发下字节交错现象两张单子内容混在一起。原因多线程共用一个 Socket 或没做串行。解决每台打印机单线程队列或发送时加锁连接池借出的连接同一时刻只给一个任务用。6. 进阶状态查询、模板化与灰度验证做到能打只是及格生产环境还得能「知道打没打成功」。爱普生部分机型支持实时状态查询发DLE EOT n10 04 nn 取 1 查打印机状态、2 查脱机、3 查错误、4 查纸传感器。返回一个字节按位解析就能知道缺纸、开盖、切刀错误。这比「发完就当成功」靠谱得多尤其仓储场景缺纸了得立刻告警。public int queryStatus(Socket s) throws Exception { s.getOutputStream().write(new byte[]{0x10, 0x04, 0x01}); s.getOutputStream().flush(); return s.getInputStream().read(); // 返回状态字节按位解析 }逻辑说明发查询指令后读一个字节bit 解析参考手册。参数说明n1 是打印机状态n4 是纸状态。注意不是所有爱普生机型都支持发之前确认。模板化是另一个提效点。把「发货单」「后厨单」「收银小票」做成模板配置字段占位符替换避免每加一种单据就改代码。我一般用简单的{orderNo}占位渲染时替换成实际值再拼指令。灰度验证则是在新机型上线时先用一台测试机跑固定用例初始化、中文、切纸、钱箱、连续 100 张不交错全过了再切流量。最后说个习惯每次改指令参数我都会先在测试机上打一张「参数说明单」把当前用的切纸、钱箱、编码参数打出来贴在机器旁。因为过两周你一定会忘了当时为什么用42而不是00。这套方案值不值得做如果你的打印需求超过「偶尔打一张」且有多门店、多机型、要自动化那直连 9100 加队列管理几乎是绕不开的路前期把指令和队列踩透后面省下的是无数个「打印机又脱机了」的深夜电话。希望帮到你。本文还有配套的精品资源点击获取
返回列表