uniapp调用Java原生Socket:实现Android TCP通信插件

发布时间:2026/9/11 20:09:16
uniapp调用Java原生Socket:实现Android TCP通信插件 简介这是一份基于Java开发的uniapp原生Android TCP通信插件工程包面向使用uniapp跨端框架、需要在Android端实现稳定TCP数据传输的开发者。插件直接调用Android SDK原生API可支持即时通讯、设备数据同步、游戏联机等对可靠性要求较高的网络场景适合具备一定Vue.js与Android基础的开发者参考与二次开发。资源包共710个文件约124.38MB文件类型涵盖xml布局与配置、json数据、so动态库、jar依赖、js接口、java源码及gradle构建脚本等基本覆盖Android原生插件从编译、封装到集成uniapp的完整链条目录结构清晰便于按需查阅。目前已有375人学习下载。通过解压可获取完整工程源码与资源配置能直观学习uniapp原生插件开发流程、Socket编程、数据编码解码及异常处理等关键实现有助于快速将TCP能力嵌入自身应用。1. 为什么uniapp应用需要一个Java写的原生Android TCP插件在uniapp里做物联网或工控项目时最尴尬的莫过于协议栈不在标准API里。你面对的是一个只开裸露TCP端口的PLC或采集网关而JS侧只有uni.request和WebSocketWebSocket的握手和帧格式往往不被设备端接受。这时候就必须下沉到Android原生层用Java写一个Socket连接。这套源码做的正是这件事一个uniapp可加载的Android原生TCP插件JS端调用connect、send、close数据收发走原生线程避免JS引擎对二进制流的低效处理。适合做设备调试、私有协议对接、长连接推送的开发者。2. UniModule插件机制与TCP通信的选型边界2.1 UniModule还是UniComponent在uni-app Android原生插件体系里插件分两类Module和Component。Module不涉及UI比如网络、数据库、蓝牙Component是自定义控件比如扫码框、地图。TCP通信是纯功能逻辑没有界面用Module即可。选择错误会把简单问题复杂化如果你用Component去承载Socket还得管理View的创建、测量、销毁在后台服务场景下View根本没有宿主。对比维度UniModuleUniComponent典型用途网络通信、数据存储、硬件访问自定义View、地图、相机预览是否必须绑定UI否是生命周期随uni实例创建/销毁随页面View attach/detach后台存活可以脱离页面运行页面销毁后View不可用适合TCP插件是否所以这个源码里插件核心类继承自UniModule而不是实现某个View类。isRunOnBaseThread默认走子线程执行方法对接网络模块更安全。2.2 为什么WebSocket替代不了裸TCP不少开发者会问uniapp内置了plus.net或WebSocket为什么还要写原生插件WebSocket是在TCP之上做HTTP Upgrade、二进制帧掩码、协议扩展它适合浏览器环境。但很多工业设备、传感器网关、Modbus TCP服务端不会去实现RFC 6455它们直接暴露端口接收你拼好的字节。你用WebSocket连上去第一帧就会被当成非法握手包丢弃。另外TCP字节流的可控性更直接你可以自定义帧结构比如4字节长度头加业务数据在原生层做拆包处理。JS侧虽然也有ArrayBuffer但高频收发时的GC压力和数据拷贝比Java要重用原生插件把内核缓冲区数据一次性交给约定回调再转成base64或hex给JS性能更稳。2.3 JS到原生的调用形式先在JS侧确定调用契约再回过去写Java是减少返工的习惯。下面这段代码是这个插件的预期用法// 在uniapp页面中引入原生插件 const tcp uni.requireNativePlugin(TCPModule) tcp.connect({ host: 192.168.1.100, port: 502, timeout: 5000 }, (res) { if (res.code 0) { console.log(TCP已连接, res.msg) tcp.send({ hex: 01030000000A }) } else { console.error(连接失败, res.msg) } })这段代码里connect的返回值通过Callback异步回调。注意这里没有用Promise因为原生插件在Android端Callback可以多次回调而Promise只能resolve一次。如果插件内部把回调包装成Promise也行但遇到断线通知类事件就不方便。所以connect、send用CallbackonData这类持续事件用全局事件广播。3. 用Java实现TCP连接核心模块与线程模型3.1 建立连接超时与Nagle算法这里给出插件中最关键的一段。首先定义TCPModule继承UniModule在unbind()里释放资源。连接方法必须放在子线程避免阻塞UI// TCPModule.java package com.example.tcp; import io.dcloud.feature.uniapp.common.UniModule; import io.dcloud.feature.uniapp.bridge.UniJSCallback; import org.json.JSONObject; import java.net.InetSocketAddress; import java.net.Socket; public class TCPModule extends UniModule { private Socket socket; private DataInputStream dataInputStream; private DataOutputStream dataOutputStream; private volatile boolean running; private Thread readThread; private Thread connectThread; public void connect(String host, int port, UniJSCallback callback) { connectThread new Thread(() - { try { socket new Socket(); socket.connect(new InetSocketAddress(host, port), 5000); socket.setTcpNoDelay(true); // 关闭Nagle降低小包延迟 socket.setKeepAlive(true); socket.setSoTimeout(0); // 读超时交给业务层控制 dataInputStream new DataInputStream(socket.getInputStream()); dataOutputStream new DataOutputStream(socket.getOutputStream()); running true; callback.invoke(new JSONObject().put(code, 0).put(msg, ok)); startReadLoop(); } catch (Exception e) { callback.invoke(new JSONObject().put(code, -1).put(msg, e.getMessage())); } }); connectThread.setName(tcp-connect); connectThread.start(); } }要点说明Socket()先创建再connect是Android上最稳妥的方式可以用超时参数控制连接时长。setTcpNoDelay(true)对工控场景非常重要默认Nagle算法会把小报文组合后发送导致Modbus这类一问一答协议多出几百毫秒延迟。setKeepAlive是TCP层探测不能替代业务心跳。连接线程单独命名方便崩溃日志定位。3.2 读线程不阻塞主线程事件回调到JS连接建立后立刻启动readThread持续读流。Java的InputStream.read()是阻塞的正好适合长连接等待报文。读到数据后转成hex字符串或base64再发给uni端private void startReadLoop() { readThread new Thread(() - { byte[] buffer new byte[4096]; while (running) { try { int len dataInputStream.read(buffer); if (len -1) { // 对端关闭连接 onDisconnect(remote closed); break; } byte[] data new byte[len]; System.arraycopy(buffer, 0, data, 0, len); String hex bytesToHex(data); // 通过Vue2/Vue3都能收到的全局事件回传 mUniSDKInstance.fireGlobalEventCallback(tcp.onData, new JSONObject().put(data, hex)); } catch (Exception e) { if (running) { onDisconnect(e.getMessage()); } break; } } }); readThread.setName(tcp-read); readThread.start(); }注意这里read的buffer固定4KB不代表应用层一个包只有4KBTCP是流数据可能被拆成多次到达。fireGlobalEventCallback会把事件抛到uni框架JS侧用uni.$on(tcp.onData)接收。事件名带上tcp前缀避免和其他插件冲突。bytesToHex方法可以把二进制转成十六进制字符串JS端再做hexToArrayBuffer还原比直接传字节数组在uni桥接里更可靠。3.3 发送与关闭发送二进制数据需要从JS端拿到ArrayBuffer或hex字符串在原生还原成byte[]后调用DataOutputStream.writepublic void send(JSONObject options, UniJSCallback callback) { try { String data options.optString(hex); byte[] bytes hexToBytes(data); dataOutputStream.write(bytes); dataOutputStream.flush(); callback.invoke(new JSONObject().put(code, 0).put(len, bytes.length)); } catch (Exception e) { callback.invoke(new JSONObject().put(code, -1).put(msg, e.getMessage())); } } public void close() { running false; try { if (dataOutputStream ! null) dataOutputStream.close(); if (dataInputStream ! null) dataInputStream.close(); if (socket ! null) socket.close(); } catch (Exception ignored) {} }write和flush都在调用线程执行。如果担心频繁send阻塞JS可以在插件内部维护一个发送队列由独立发送线程取数据。但设备通信一般QPS不高直接调用也能满足。close时把running置false让read线程退出然后依次关闭输出流、输入流、socket。注意关闭顺序先关输出再关输入否则对端可能收到RST包。4. manifest、混淆与uniapp端调用配置4.1 插件目录与注册文件这个zip解压后android工程里需要维护dcloud_uniplugins.json文件。该文件告诉uni-app运行时有哪些原生插件要加载{ nativePlugins: [ { plugins: [ { type: module, name: TCPModule, class: com.example.tcp.TCPModule } ] } ] }把这个json放到app/src/main/assets目录下与dcloud_uniplugins.json同名合并而不是覆盖。常见错误是直接放在src根目录导致uniapp离线打包时识别不到插件运行后uni.requireNativePlugin返回null。4.2 AndroidManifest与网络安全TCP连接需要INTERNET权限。Android 9及以上默认禁止明文流量如果服务端是局域网内无TLS的裸TCP需要显式允许uses-permission android:nameandroid.permission.INTERNET / application android:usesCleartextTraffictrue android:networkSecurityConfigxml/network_security_config /applicationnetwork_security_config.xml里配置特定域允许明文network-security-config domain-config cleartextTrafficPermittedtrue domain includeSubdomainstrue192.168.1.100/domain /domain-config /network-security-config注意domain配置不支持IP段通配写IP地址只能精确匹配。最简单就是usesCleartextTraffictrue但上架敏感权限审核时可能会被问建议按应用场景限定范围。4.3 Proguard混淆规则如果离线打包开启混淆Java插件类会被改名字导致uni运行时找不到。在proguard-rules.pro里加-keep class com.example.tcp.** { *; } -keepclasseswithmembers class * implements io.dcloud.feature.uniapp.common.UniModule { public methods; }第二行保留所有UniModule子类的public方法这是插件的方法被JS调用的关键。很多人只加了keep class结果内部类被混淆回调构造JSONObject失败运行时报ClassNotFoundException。4.4 JS侧调用参数表把参数定义清楚后端对接时不会乱。下面表格列出connect需要关注的字段参数类型默认值说明hoststring无服务端IP或域名不要带http前缀portnumber无TCP端口范围1-65535timeoutnumber5000连接超时毫秒数tcpNoDelaybooleantrue是否关闭Nagle算法keepAlivebooleantrueTCP层keepalive探活soTimeoutnumber0读超时0表示阻塞等待hexstring无send时的十六进制报文JS调用侧使用uni.requireNativePlugin时注意插件名要和json中的name一致。Vue2工程和Vue3工程在uni-app 3.x中写法相同但如果是自定义基座调试必须先重新制作基座否则提示plugin not found。调试时我会先执行hexToBytes确认字节数对不对再接设备节省时间。5. 断线重连、粘包拆包与异常定位5.1 粘包与拆包TCP是字节流不是消息流很多初次用TCP的人会在view端发现收到的报文和发送的不一样多包黏在一起或一包被拆成两半。这是TCP面向字节流的特性决定的不是插件bug。常见做法是在应用层定义帧协议比如4字节长度头2字节命令数据。原生层拆包时可以维护一个ByteArrayOutputStream累积数据private ByteArrayOutputStream frameBuffer new ByteArrayOutputStream(); private void handleFrame(byte[] bytes) { frameBuffer.write(bytes, 0, bytes.length); byte[] pending frameBuffer.toByteArray(); while (pending.length 4) { int length ((pending[0] 0xff) 24) | ((pending[1] 0xff) 16) | ((pending[2] 0xff) 8) | (pending[3] 0xff); if (pending.length 4 length) break; byte[] frame Arrays.copyOfRange(pending, 4, 4 length); // 把完整帧交给上层处理 onFrame(frame); byte[] remain Arrays.copyOfRange(pending, 4 length, pending.length); frameBuffer.reset(); frameBuffer.write(remain, 0, remain.length); pending remain; } }这段代码把前4字节当作大端整数长度循环取出完整帧。注意length要加上最大包限制防止恶意报文把长度设成0xFFFFFFFF导致内存膨胀。生产环境建议限制单帧不超过64KB。5.2 心跳与断线重连的线程设计TCP连接在WiFi切换、路由器空闲断开时可能处于假死状态。只靠keepAlive探测太慢业务层要做心跳。常见做法是每30秒发送一个心跳包超过90秒没有服务端响应就主动重连。在插件里可以放一个ScheduledExecutorServiceprivate ScheduledExecutorService heartBeatExecutor; private void startHeartbeat() { heartBeatExecutor Executors.newSingleThreadScheduledExecutor(); heartBeatExecutor.scheduleAtFixedRate(() - { if (running System.currentTimeMillis() - lastRecvTime 30000) { try { dataOutputStream.write(heartbeatBytes); dataOutputStream.flush(); } catch (Exception e) { close(); } } }, 10, 10, TimeUnit.SECONDS); }这里的lastRecvTime在readThread每收到一个字节时更新通过判断收包时间决定是否发送心跳。注意不要在心跳线程里直接调close因为close里要关闭流而读线程正在阻塞读会交叉抛异常。稳妥做法是用volatile标志让read线程自己退出后在合适的时机重连。5.3 用错误码和日志快速定位问题插件边开发边调试时建议把错误码统一方便前端和后端分析。下面表格是我常用的错误码约定错误码含义检查方向0成功无-1参数错误host为空、port越界-2连接超时防火墙、服务监听状态-3连接被拒服务器未启动、端口不对-4已断开对端主动关闭、网络中断-5发送失败连接失效SocketException定位问题时在Android Studio的Logcat过滤tcp-plugin标签把每个关键节点打log。用本地工具也可以快速验证先准备一台Linux或macOS机器用nc -l 502监听端口然后从App连上去直接看裸报文是否符合预期nc -l 502Windows上可以用socat或netcat替代没有就装一个。这个步骤能把服务端问题、插件问题、协议问题快速切分比直接对接PLC效率高得多。6. 进阶TCP连接池的NIO改造与局域网验证技巧6.1 NIO是否值得代替BIO当同时管理几十个设备连接时每连接一线程的BIO模型线程数过多。Java NIO的Selector可以让一个线程管多个Channel但代码复杂度上升明显。uniapp插件最终跑在Android设备上一个App同时维护的TCP连接通常只有几个用BIO加线程池更直观也更好排查问题。如果你确实要接入几十个设备建议把网络层独立成一个Java库用Netty或NIO封装不走插件框架的线程回调避免主线程卡顿。6.2 数据完整性验证插件写完后不要急着接真实设备。先用本机ServerSocket写一个回显服务把收到的字节反转或加1后返回App端发固定序列并校验。这样能验证收发、拆包、重连逻辑是否正常。比如发送01 03 00 00 00 0A回显服务原样返回在插件断点处检查hex字符串是否一致。6.3 在uniapp端保存连接状态断线重连不要靠原生层自说自话最好把状态同步到Vuex或Pinia。在JS侧收到tcp.onStatus事件时更新store重新发包时先检查状态避免在连接未建立时send。插件里每次状态变化调用fireGlobalEventCallback事件名用tcp.onStatus参数code1连接code0断开。这样前后端逻辑就串起来了后续扩展TLS、多连接管理也都能沿用同样的事件流。把这个回显服务写进插件示例工程每次改完协议先跑一遍再上设备能省下大量抓包时间。本文还有配套的精品资源点击获取