简介:面向Android开发初学者与网络编程爱好者,这份UDP包发送示例工程演示了如何在安卓设备上通过Java实现UDP通信,适合用于WiFi定位、网络延迟测试等对实时性要求高但允许丢包的场景。压缩包共40个文件,包含4个Java源码、17个XML布局与配置、Gradle构建脚本、PNG图标及说明文档,整体仅97KB,结构紧凑便于快速阅读。代码围绕DatagramSocket与DatagramPacket展开,清晰呈现创建套接字、封装目标IP和端口、调用send()发送以及关闭资源的完整流程;同时保留MainActivity、UDPClient等模块划分和AndroidManifest权限声明,部分版本还包含BroadcastReceiver用于监听UDP回应,方便直接导入调试或二次开发。已有505人学习/下载,对理解Android UDP通信原理和实际调测具有直接参考价值。 搞网络调试的人,大概都有过这种尴尬:电脑上UDP调试工具一抓一大把,可真到了设备现场,掏出手机才发现手里连一个能发包的工具都没有。UDPSend就是为解决这个问题写的安卓端小程序——它能把一串自定义数据从手机发出去,目标IP、端口随意指定,数据格式支持ASCII文本和十六进制两种,还带定时发送功能。我用它排查过串口服务器、智能灯光控制器、无人机飞控协议联调,甚至给LabVIEW、CODESYS、C#的FINS通信程序做过数据源。这篇文章把整个工具的定位、核心代码、踩坑记录一次讲清楚,适合做嵌入式、工业通信、物联网调试的兄弟们参考。
1. 项目思路与场景拆解:为什么非要在安卓上做UDP发包
先说为什么要做安卓版。很多人觉得UDP发包嘛,PC上一大堆工具,NetAssist、SocketTool、WireShark随便用,何必费劲在手机上搞。但实际干过现场调试的都知道,设备装上去之后,人往往要蹲在设备旁边,一手拿着手机,一手抱着笔记本,连个放电脑的地方都不一定有。手机端的UDP工具能让你在设备旁边一边看指示灯一边发测试包,效率完全不一样。
UDP协议在工业、嵌入式、物联网调试里的出镜率非常高。C#连三菱FINS走UDP,LabVIEW做上位机通信走UDP,CODESYS跑软PLC通信也可以走UDP,ROS2的DDS底层默认也是基于UDP传输。这些场景的共性是:两端约定好端口和数据格式,发送方把报文丢出去就完事。调试的时候需要的不是复杂的协议分析,而是“我只是想往某个端口发一段字节,看设备有没有反应”。UDPSend的核心价值就是把这个动作在手机上做到足够快、足够干净。
比UDPSend更“重”的方案是直接用iPerf3思路做UDP打流。iPerf3测的是网络带宽极限,发送的是海量包,用于摸底性能。而UDPSend这种小工具定位完全不同:它一个个包发,能精确控制内容,适合验证协议逻辑、测试设备响应。这两种工具其实是互补关系,性能问题用iPerf3,协议问题用UDPSend,我在实际测试中经常两个配合用。
2. 核心原理与安卓网络基础:UDP协议特征、权限、线程模型
2.1 UDP协议的关键特征
UDP是面向无连接的传输层协议,发送数据前不需要像TCP那样三次握手,每个数据报都是独立的,发完就扔。这种“无状态”的特点让它非常适合局域网内的小数据量探测——你不需要维护连接,不用担心连接断开,一个Socket就能反复发送。代价是它不保证到达、不保证顺序,所以在公网环境或高丢包环境下做关键数据推送,UDP方案需要自己设计可靠性机制。
单包大小上限是UDP调试中必须知道的知识点。IP包最大65535字节,去掉20字节IP头和8字节UDP头,UDP数据部分理论上最多传输65507字节。但实际局域网环境里,如果发给接收端的数据在路由器层面触发分片,性能会明显下降。我在内网联调时通常建议不超过1472字节(1500 MTU减去IP头20字节再减去UDP头8字节),这样基本不会产生IP分片,设备端处理也最稳。
2.2 安卓端的网络权限与线程限制
安卓上做UDP通信,权限这块很简单,只需要在AndroidManifest.xml里声明一个权限:
<uses-permission android:name="android.permission.INTERNET" />注意,安卓的联网权限不像定位、相机那样属于危险权限,不需要动态申请,安装后默认授予。但有个细节容易被忽略:如果需要在局域网里自动发现设备(比如扫描某个网段下面的设备),还会用到ACCESS_NETWORK_STATE权限,用来判断当前有没有连上Wi-Fi、网络是否可用。UDPSend这个工具早期版本就漏了这一步,导致没联网时点发送直接崩,后来加了这个权限判断才正常。
第二个重点是线程模型。安卓从4.0开始强制要求网络操作不允许放在主线程(UI线程),否则会直接抛NetworkOnMainThreadException。UDP发送本身开销很小,但如果放在主线程做了网络操作,轻则卡顿,重则崩溃。因此工程的发送逻辑必须放进子线程,发完再通过Handler或runOnUiThread把结果抛回主线程更新UI。后面给的示例代码里我会把这点做进去。
3. 核心实现与实操过程:从工程结构到完整发送代码
3.1 工程结构与界面布局
UDPSend采用最简单的单Activity结构,对一个小工具来说完全够用。整个工程包含三个文件:MainActivity(界面与交互)、UdpSender(发送逻辑封装)、DataHexUtil(ASCII与十六进制互转)。不引入任何第三方库,降低编译成本,也减少包体积。
界面遵循“从上到下自然流”的设计:最上方是发送数据输入区,支持多行输入;中间是目标IP、目标端口、发送间隔三个输入框;再往下是“发送一次”和“定时发送”两个按钮;底部放一个滚动日志区域,用来显示每次发送的字节数、耗时和接收端返回(如果做了接收功能)。这样的布局在现场拿着手机操作时,单手就能完成绝大多数操作。
3.2 核心发送代码实现
UDP发送的核心类是java.net.DatagramSocket和java.net.DatagramPacket。一个最简发送方法如下:
public class UdpSender { private DatagramSocket socket; public UdpSender() throws SocketException { // 不绑定端口,由系统分配随机本地端口 socket = new DatagramSocket(); socket.setSoTimeout(3000); } /** * 发送单条UDP数据报 */ public void send(byte[] data, String targetIp, int targetPort) throws IOException { InetAddress address = InetAddress.getByName(targetIp); DatagramPacket packet = new DatagramPacket(data, data.length, address, targetPort); socket.send(packet); } /** * 等待接收端回包(可选) */ public byte[] receive() throws IOException { byte[] buffer = new byte[2048]; DatagramPacket response = new DatagramPacket(buffer, buffer.length); socket.receive(response); byte[] result = new byte[response.getLength()]; System.arraycopy(response.getData(), response.getOffset(), result, 0, response.getLength()); return result; } public void close() { if (socket != null && !socket.isClosed()) { socket.close(); } } }这里有几个细节值得解释。第一,创建DatagramSocket时没有指定端口,系统会随机分配一个本地端口,这样每次发送都从不同源端口出去。某些对端设备会校验源端口,这种情况下就需要在构造函数里指定固定端口:new DatagramSocket(9527)。第二,setSoTimeout(3000)设置的是接收超时时间,如果调用receive()并且3秒内没有收到回包,会抛出SocketTimeoutException,这个异常在实际调试中非常常见,后面排查部分会细说。第三,发送UDP包之前没有“连接”动作,目标地址和端口是写在DatagramPacket里的,也因此同一个Socket可以不断地向不同目标发送,这是UDP灵活性的体现。
3.3 ASCII与HEX数据格式转换
UDP调试最大的坑之一是报文格式。很多设备手册上写的是十六进制报文,比如01 03 00 00 00 02 C4 0B,但人机交互输入框收的是字符串。所以要实现一个HEX转字节数组的工具方法:
public static byte[] hexStringToBytes(String hex) { String clean = hex.replaceAll("\\s+", ""); // 去掉空格和换行 if (clean.length() % 2 != 0) { throw new IllegalArgumentException("十六进制字符串长度必须是偶数"); } byte[] result = new byte[clean.length() / 2]; for (int i = 0; i < result.length; i++) { int val = Integer.parseInt(clean.substring(i * 2, i * 2 + 2), 16); result[i] = (byte) val; } return result; }这里有一个教训:一开始我处理HEX字符串时,没考虑输入中间可能夹杂空格或换行,用户粘了手册里的报文直接崩溃。后来在转换前先做一次正则清洗replaceAll("\\s+", ""),把空格、制表符、换行全部过滤掉,这个问题就解决了。转换时还要注意Integer.parseInt只支持两位十六进制,超过0x7F的值会以负号存入byte数组,这是Java有符号字节的正常表现,底层网络发送时字节本身是对的,不需要额外处理。
3.4 定时发送与界面交互
定时发送用ScheduledExecutorService实现比Timer更好控制。核心思路是点击按钮时启动一个单线程调度器,每隔N毫秒执行一次发送任务,再点击停止时shutdownNow()干净地终止。
private ScheduledExecutorService scheduler; private void startPeriodicSend() { if (scheduler != null) return; scheduler = Executors.newSingleThreadScheduledExecutor(); long intervalMs = Long.parseLong(etInterval.getText().toString()); scheduler.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { byte[] data = buildDataFromInput(); sender.send(data, targetIp, targetPort); runOnUiThread(() -> log("已发送 " + data.length + " 字节")); } catch (Exception e) { runOnUiThread(() -> log("发送失败: " + e.getMessage())); } } }, 0, intervalMs, TimeUnit.MILLISECONDS); } private void stopPeriodicSend() { if (scheduler != null) { scheduler.shutdownNow(); scheduler = null; } }需要注意,scheduleAtFixedRate如果任务执行时间超过了间隔时间,调度器不会立刻补跑,而是等当前任务结束再排下一次。对于UDP发送这种微秒级操作来说,100毫秒的最小间隔已经非常可靠。现场调试时我一般用200毫秒间隔连续发几十次报文来测试设备响应,效果很好。
3.5 目标地址与广播/组播支持
UDPSend还有一个隐藏能力:支持广播地址255.255.255.255和组播地址。发送到广播地址不需要额外权限,但很多路由器对定向广播会做隔离,比如192.168.1.255这种子网定向广播在跨VLAN时会失效。组播发送则需要先joinGroup,而且WLAN环境组播包经常被AP过滤,这部分我在调试智能灯时遇到过,最后解决方式是改用单播到目标设备IP。
4. 常见问题与排查技巧实录
4.1 手机能发送但设备收不到
这个问题的排查顺序固定为:先确认手机和终端在同一个局域网段;再确认目标IP和端口是否填对;然后用PC端工具做交叉验证,从PC发同样的包给设备,如果PC能通而手机不通,问题基本出在AP隔离或防火墙。不少商业Wi-Fi(酒店、办公网络)开启了AP隔离,设备之间二层通信被阻断,手机和终端虽然连同一个路由,但互相不可见,这种情况换一台手机开热点最直接。
4.2 手机无法接收回包
做UDP联调时,除了发还希望收。安卓收UDP有两个坑:一是模拟器环境对UDP支持不好,用Genymotion或Android Studio自带模拟器经常收不到物理设备回包,建议直接真机测试;二是接收Socket要绑定端口,但设备端回包的目标IP是手机当前Wi-Fi的IP,如果你的代码里用DatagramSocket不指定本地端口发送,回包会发到一个随机端口,抓包能看到回包到了但应用层收不到。解决方式简单粗暴——发送Socket绑定固定端口,设备端回包前也做同样的正确配置。
4.3 息屏后定时发送中断
用手机做长时间UDP压力发送时,另一个高频坑是息屏后发送任务被系统挂起。安卓的省电策略会在屏幕熄灭后冻结后台应用CPU,Wi-Fi也会进入低功耗模式,导致ScheduledExecutorService任务迟迟不执行。解决办法有两个:一是测试期间把屏幕常亮打开(FLAG_KEEP_SCREEN_ON);二是申请WAKE_LOCK唤醒锁。做短时调试用屏幕常亮就够了,长时间打流还是建议回到PC端用iPerf3或自写脚本,毕竟手机在这种场景下不是最优工具。
4.4 混淆规则与64位环境适配
如果要把UDPSend打包发布,ProGuard混淆时要注意保留网络相关的类成员。虽然Java反射在UDP这个工具里用得少,但Gson、Log等反射类库对混淆很敏感。加上keepattributes Signature和keep class规则可避免运行时崩溃。另外现在安卓市场对上架App要求64位支持,纯Java代码不需要额外处理,但如果项目里放了JNI的so库,这个要特别注意。
常见问题整理成速查表:
| 现象 | 排查顺序 | 解决建议 |
|---|---|---|
| 能发送但对端收不到 | 网段 → IP/端口 → AP隔离 | 确认真机在同一网段;换热点复测;用PC交叉验证 |
| 发送就崩溃 | 主线程网络操作 → 权限 | 放到子线程;确认manifest声明INTERNET权限 |
| 定时发送不规律 | 息屏 → 省电策略 → 任务卡顿 | 屏幕常亮;缩短任务耗时;必要时持唤醒锁 |
| 收不到回包 | 本地端口 → 防火墙 → AP隔离 | 使用固定本地端口;关闭手机网络安全扫描类App |
| 中文内容乱码 | 字节编码 | 确认编码方式,设备端按UTF-8或GBK解码与发送端一致 |
5. 扩展玩法:从调试工具到自动化测试的进阶之路
5.1 与PC端工具、脚本的配合
UDPSend不一定要孤立使用。我经常把它和PC端工具配合:让手机上的UDPSend定时发触发信号,PC上用Wireshark抓包,观察设备是否正确应答。这在验证Linux下C++写的UDP服务以及Ubuntu与Windows之间UDP互通测试时非常有用。另一种玩法是让PC上用Python脚本模拟设备端的核心逻辑,手机作为测试终端,这样可以快速调试通信协议,不用每次改硬件固件。
5.2 从发送到回环测试
给UDPSend增加接收和回显功能后,它就变成了一个更完整的UDP探测工具。设备发一个包给手机,手机自动回显相同内容——这种“回声测试”在测UDP连通性和评估网络延迟时非常实用。延迟可以直接看到时间差,丢包也能通过连续发送计数感知到。市面上不少UDP工具都有这个能力,但自己手机上装一个总是不亏的。
5.3 与iPerf3的配合分工
再强调一次工具选型:UDP通信是否正常用UDPSend这类交互式工具测,测出来能通再考虑性能;性能怎么评价,用iPerf3这类打流工具测。两者分工不同,不冲突。iptables防火墙、路由器NAT、ACL配置完之后,我都习惯先用UDPSend发个包测试连通,通了再走性能压测。这个习惯救了我好几次,因为性能上不去的时候,很多人都先怀疑带宽,其实问题往往是UDP包根本就没到达。
6. 实操心得与小细节补充
最后分享几个在开发调试中积累的小细节。
端口的校验一定要做。安卓上端口号是0到65535的整数,但实际能用的范围是1到65535,端口0在发送时会被系统视为“随机选择”,如果用户填了0,UDP包会发到随机端口,非常容易踩坑。
数据长度显示别只看字节数。十六进制模式下,同一条报文ASCII模式可能显示12字节,HEX模式可能显示6字节,原因是一个ASCII字符对应一个字节,而两个十六进制字符才对应一个字节。日志区域里把两种模式的字节数分开显示,调试时就一目了然。
文本输入框的回车处理也要注意。多行输入时用户可能不小心带了换行,发送前如果没做trim(),报文尾部会多出\r\n两个字节,对端做严格校验时会直接抛异常。这个问题在跟串口服务器对接时最容易出现,因为很多工控设备的协议长度字段是固定的,多两个字节整个报文就解析失败了。
我在实际调试过程中最大的体会是:工具越简单,越好用。UDPSend没有花哨的界面,没有复杂的功能,但它覆盖了“手边测试”这个最频繁的场景。不管是和智能硬件联调、给LabVIEW程序做数据源、还是验证C# FINS通信流程,它都能在关键时刻顶上。做这类工具时,把核心场景打通,把边缘情况加好防护,比堆功能更重要。
本文还有配套的精品资源,点击获取