Uniapp APP 端实现 TCP Socket 通信(ZPL 打印实战)
前言
在混合开发中,Uniapp 默认不支持原生 TCP Socket 通信,但在 IoT、打印等场景下(如 Zebra 打印机 ZPL 指令),直接操作 Socket 是刚需。本文基于 APP-PLUS
环境,通过 plus.android
调用 Java 原生 API 实现 Socket 通信,并封装为可复用的工具类。
一、核心实现:SocketManager 工具类
1. 基础功能封装
- 将之前的代码封装成一个 可复用的模块 (如
socket.js
),并在页面中调用。
socket.js
(封装 TCP Socket 逻辑)
js
// #ifdef APP-PLUS
const SocketManager = {
socket: null,
outputStream: null,
isConnected: false,
/**
* 异步连接 Socket(返回 Promise)
* @param {string} ip 服务器 IP
* @param {number} port 服务器端口
* @param {number} [timeout=5000] 超时时间(毫秒)
*/
connect(ip, port, timeout = 5000) {
return new Promise((resolve, reject) => {
// 使用 setTimeout 模拟子线程执行(避免 AsyncTask 兼容性问题)
setTimeout(() => {
try {
// 1. 导入 Java 类
const Socket = plus.android.importClass("java.net.Socket");
const InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress");
const DataOutputStream = plus.android.importClass("java.io.DataOutputStream");
// 2. 创建 Socket 并连接
this.socket = new Socket();
this.socket.connect(new InetSocketAddress(ip, port), timeout);
// 3. 初始化 OutputStream
this.outputStream = new DataOutputStream(this.socket.getOutputStream());
this.isConnected = true;
this.socket.setKeepAlive(true);
console.log("Socket 连接成功", ip + ":" + port);
resolve(); // 直接 resolve(已在主线程)
} catch (e) {
console.error("Socket 连接失败:", e);
this.close(); // 失败时清理资源
reject(e); // 直接 reject(已在主线程)
}
}, 0);
});
},
/**
* 发送字节数组(Uint8Array)
* @param {Uint8Array} data 字节数组
*/
send(data) {
if (!this.isConnected || !this.outputStream) {
return Promise.reject(new Error("Socket 未连接"));
}
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
// 1. 转换 Uint8Array 为 Java byte[]
const JavaArray = plus.android.importClass("java.lang.reflect.Array");
const Byte = plus.android.importClass("java.lang.Byte");
const javaBytes = JavaArray.newInstance(Byte.TYPE, data.length);
for (let i = 0; i < data.length; i++) {
//JavaScript 的 `TypedArray`(如 `Uint8Array`)范围是 `0~255`,而 Java 的 `byte` 范围是 `-128~127`。直接传递会导致数据截断。
JavaArray.setByte(javaBytes, i, data[i] > 127 ? data[i] - 256 : data[i]);
}
// 2. 写入数据并刷新
this.outputStream.write(javaBytes);
this.outputStream.flush();
resolve();
} catch (e) {
console.error("发送数据失败:", e);
this.close();
reject(e);
}
}, 0);
});
},
/**
* 发送字符串(UTF-8)
* @param {string} str 字符串
*/
sendString(str) {
if (!this.isConnected || !this.outputStream) {
return Promise.reject(new Error("Socket 未连接"));
}
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
// 1. 转换字符串为 Java byte[](UTF-8)
const JavaString = plus.android.importClass("java.lang.String");
const javaStr = new JavaString(str);
const bytes = javaStr.getBytes("UTF-8");
// 2. 写入数据并刷新
this.outputStream.write(bytes);
this.outputStream.flush();
resolve();
} catch (e) {
console.error("发送字符串失败:", e);
this.close();
reject(e);
}
}, 0);
});
},
/**
* 关闭连接
*/
close() {
// 关闭操作可以在主线程执行
try {
this.isConnected = false;
if (this.outputStream) {
this.outputStream.close();
this.outputStream = null;
}
if (this.socket) {
this.socket.close();
this.socket = null;
}
console.log("Socket 已断开");
} catch (e) {
console.error("关闭 Socket 时出错:", e);
}
},
/**
* 检查连接状态
*/
checkConnection() {
try {
return this.isConnected &&
this.socket &&
!this.socket.isClosed() &&
this.socket.isConnected();
} catch (e) {
return false;
}
}
};
export default SocketManager;
// #endif
2. 关键设计说明
-
setTimeout
的作用- 将 Socket 操作放到事件队列下一轮执行,避免阻塞 UI 线程(Android 原生方法调用是同步的)。
- 替代方案:可使用
plus.android.invoke
的异步 API(需验证兼容性)。
-
数据类型转换
Uint8Array
转 Javabyte[]
时需处理符号位(data[i] > 127 ? data[i] - 256 : data[i]
)。- 字符串发送使用
String.getBytes("UTF-8")
确保编码正确。
-
资源管理
- 每次操作后检查
isConnected
状态,避免崩溃。 close()
方法中置空对象引用,防止内存泄漏
- 每次操作后检查
二、实战:ZPL 打印指令发送
1. 打印方法封装
js
//引入(放入的文件的位置)
import SocketManager from "./socket.js"
async printMaterial(item, ip = "192.168.50.192", port = 9100) {
if (!item?.itemnum) throw new Error("物资信息不完整");
const zplTemplate = `
^XA
^FO50,50^A0N,50,50^FD物资编码:${item.itemnum}^FS
^FO50,120^A0N,50,50^FD名称:${item.itemDesc.substring(0, 30)}^FS
^XZ
`;
try {
// #ifdef APP-PLUS
await SocketManager.connect(ip, port);
await SocketManager.sendString(zplTemplate);
uni.showToast({
title: "打印成功"
});
// #endif
} catch (error) {
uni.showToast({
title: `打印失败: ${error.message}`,
icon: "error"
});
throw error;
} finally {
// #ifdef APP-PLUS
SocketManager.close(); // 确保连接关闭
// #endif
}
}
2. 避坑指南
-
连接超时处理
- Socket建立连接是异步的,默认设置
timeout=5000
,避免网络问题导致无响应。 - 错误捕获后自动关闭连接,防止僵尸连接。
- Socket建立连接是异步的,默认设置
-
ZPL 指令格式
- 使用
^XA
和^XZ
包裹指令,^FD
插入动态数据。 - 字段截断(如
substring(0, 30)
)防止标签溢出。
- 使用
-
平台兼容性
- 通过
#ifdef APP-PLUS
确保代码仅在 App 端运行。 - 非 App 端需降级处理(如调用 Web API 或提示用户)。
- 通过
-
Android 权限
- 在
manifest.json
中声明网络权限:。
- 在
js
"app-plus": {
"permissions": ["android.permission.INTERNET"] ,
/* 模块配置 */
"modules" : {
"Socket" : {} // 显式声明使用 Socket 模块
},
// ...其他配置
}
三、性能优化与扩展建议
-
连接复用
- 当前实现每次打印都新建连接,高频场景可改为长连接(需处理断线重连)。
-
数据接收
- 如需读取打印机响应,需通过
socket.getInputStream()
扩展SocketManager
。
- 如需读取打印机响应,需通过
-
跨平台方案
- 非 App 端可用
uni.connectSocket
(仅支持 WebSocket)或原生插件(如 cordova-plugin-chrome-apps-sockets-tcp)。
- 非 App 端可用
总结
本文通过封装 SocketManager
实现了 Uniapp 在 App 端的 TCP 通信,重点解决了以下问题:
- 原生 Socket 的异步调用封装。
- 数据类型转换与资源管理。
- 结合 ZPL 打印的实际业务场景。