资讯动态

Android Wi-Fi网络测试小工具:300行代码实现HTTP延迟与DNS检测

发布时间:2026/10/15 1:13:24 来源:尧图企业网站定制
简介这是一款面向Android开发初学者与网络调试人员的WiFi网络测试工具源码围绕网络连接质量评估这一实际需求将上传速度、下载速度与ping延迟三类测试整合到同一应用中适合用于学习Android网络编程、HttpClient使用及简单命令行调用。资源包共45个文件包含12个java源码文件、18个xml布局与配置、9个png图标资源以及jar依赖、properties配置和工程描述文件压缩包约3.51MB结构完整可直接导入开发环境。目前已有1294人学习下载。源码借助Apache HttpClient实现文件上传与下载测速并对ping命令做了简单封装测试过程通过直观界面实时展示结果可保存到文件便于后续对比分析。对于想了解Android端网络测速实现思路、界面与逻辑分层组织方式的开发者这份工程具备较好的参考与二次开发价值。1. 一个只有 300 行的 Android 小工具为什么我留了三年没删做 Android 网络这块的兄弟大概都有过这种经历App 在办公室 Wi-Fi 下跑得好好的一到客户现场就各种超时、DNS 解析失败、图片加载转圈抓包工具又来不及装只能靠猜。三年前我在一个模拟项目里被这种问题折腾了整整两天后来干脆自己写了个 Wi-Fi 网络测试小工具塞进工程里结果这个不到 300 行的小东西一直留到现在。它干的事很朴素拿到当前 Wi-Fi 的连接信息、跑一遍 HTTP 请求测延迟和状态码、把结果直接怼在界面上。技术栈就是原生 Android HttpClient 那一套没有第三方网络库没有花哨架构适合两类人一是刚接触 Android 网络编程、想找个能跑起来的最小闭环练手的二是手上有现成工程、想快速加个网络自检入口的老手。下面我按「这东西怎么搭起来 → 每个模块怎么落地 → 哪里容易翻车」的顺序拆一遍代码可以直接抄。2. 从零搭出可运行的测试界面Activity、布局与权限2.1 先想清楚这个工具要采集哪些数据动手写代码之前得先明确「网络测试」到底测什么。很多人一上来就ping但 Android 上ping命令受权限和厂商 ROM 限制结果不稳定。我一般会采集四类信息当前 Wi-Fi 的 SSID 和 BSSID确认连的是哪个热点、IP 地址和信号强度判断链路质量、一次 HTTP 请求的耗时和响应码判断端到端通不通、以及 DNS 解析是否正常。这四类数据覆盖了从链路层到应用层的主要故障点出问题时能快速定位是「没连上」「连上了但信号差」还是「网络通但服务端有问题」。采集方式上Wi-Fi 信息走WifiManagerHTTP 请求走HttpClient。这里有个选型理由要讲清楚为什么不用HttpURLConnection或 OkHttp因为这个小工具的定位是「零依赖、能塞进任何工程」HttpClientAndroid 内置的org.apache.http虽然在新版本被标记废弃但在 API 22 以下兼容性最好而且代码量最少。如果你的工程minSdk在 23 以上直接用HttpURLConnection更省事逻辑几乎一样。2.2 布局文件与权限声明先看布局一个垂直排列的LinearLayout就够上面放几个TextView显示结果下面放一个按钮触发测试!-- res/layout/activity_network_test.xml -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp TextView android:idid/tv_wifi_info android:layout_widthmatch_parent android:layout_heightwrap_content android:textSize14sp android:textWi-Fi 信息待检测 / TextView android:idid/tv_http_result android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop12dp android:textSize14sp android:textHTTP 结果待检测 / Button android:idid/btn_start_test android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop24dp android:text开始测试 / /LinearLayout布局本身没什么好说的关键是权限。Wi-Fi 相关操作在 Android 6.0 之后需要动态申请定位权限因为 SSID 和 BSSID 被归为位置信息。AndroidManifest.xml里要声明这几项!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.INTERNET / !-- Android 6.0 获取 SSID 需要定位权限 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /ACCESS_WIFI_STATE读 Wi-Fi 状态ACCESS_NETWORK_STATE判断网络连通性INTERNET发请求后两个定位权限是为了拿 SSID。这里有个坑先埋个伏笔只声明不动态申请在 Android 6.0 以上拿到的 SSID 会是unknown ssid后面避坑章节会细说。2.3 Activity 骨架与动态权限申请Activity 的结构很直白onCreate里绑定控件和按钮事件按钮点击时先检查权限再依次执行 Wi-Fi 信息采集和 HTTP 测试。为了不阻塞主线程网络操作必须放到子线程结果通过runOnUiThread回传// NetworkTestActivity.java public class NetworkTestActivity extends AppCompatActivity { private static final int REQ_LOCATION 1001; private TextView tvWifiInfo, tvHttpResult; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_network_test); tvWifiInfo findViewById(R.id.tv_wifi_info); tvHttpResult findViewById(R.id.tv_http_result); findViewById(R.id.btn_start_test).setOnClickListener(v - { // 先申请定位权限拿到 SSID 的前提 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQ_LOCATION); } else { startTest(); } }); } Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQ_LOCATION grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { startTest(); } else { tvWifiInfo.setText(定位权限被拒绝无法读取 SSID); } } private void startTest() { // 网络操作放子线程避免 NetworkOnMainThreadException new Thread(() - { final String wifiResult WifiHelper.getWifiInfo(this); final String httpResult HttpHelper.testHttp(http://www.example.com); runOnUiThread(() - { tvWifiInfo.setText(wifiResult); tvHttpResult.setText(httpResult); }); }).start(); } }这段代码有三个关键点。第一权限申请用ActivityCompat兼容低版本onRequestPermissionsResult里判断结果拒绝后给用户明确提示而不是静默失败。第二startTest里开子线程Android 3.0 之后主线程禁止网络操作直接在主线程发请求会抛NetworkOnMainThreadException这是新手最常见的翻车点。第三runOnUiThread回主线程更新 UI子线程碰控件会崩。参数上REQ_LOCATION是自定义请求码只要保证唯一即可一般用 1001 这种不容易冲突的值。3. Wi-Fi 信息采集与 HttpClient 请求的落地实现3.1 用 WifiManager 读取 SSID、IP 和信号强度Wi-Fi 信息的采集封装成一个工具类核心是WifiManager和WifiInfo。注意getConnectionInfo()在新版本被标记废弃但为了兼容老工程我还是用它新工程可以换成ConnectivityManager配合NetworkCallback// WifiHelper.java public class WifiHelper { public static String getWifiInfo(Context context) { StringBuilder sb new StringBuilder(); WifiManager wifiManager (WifiManager) context.getApplicationContext() .getSystemService(Context.WIFI_SERVICE); if (wifiManager null || !wifiManager.isWifiEnabled()) { return Wi-Fi 未开启; } WifiInfo info wifiManager.getConnectionInfo(); if (info null) { return 未连接到任何 Wi-Fi; } // SSID 在 6.0 需要定位权限否则返回 unknown String ssid info.getSSID(); String bssid info.getBSSID(); int ip info.getIpAddress(); int rssi info.getRssi(); sb.append(SSID: ).append(ssid).append(\n); sb.append(BSSID: ).append(bssid).append(\n); sb.append(IP: ).append(intToIp(ip)).append(\n); sb.append(信号强度: ).append(rssi).append( dBm\n); sb.append(链路速度: ).append(info.getLinkSpeed()).append( Mbps); return sb.toString(); } // 把 int 形式的 IP 转成点分十进制 private static String intToIp(int ip) { return (ip 0xFF) . ((ip 8) 0xFF) . ((ip 16) 0xFF) . ((ip 24) 0xFF); } }getSSID()返回的字符串带双引号比如MyWiFi展示时可以replace(\, )去掉。getIpAddress()返回的是小端序的 int必须按低位到高位拼直接Integer.toHexString会得到反的地址这是很多人第一次写会踩的坑。getRssi()单位是 dBm正常范围 -30 到 -90越接近 0 信号越好低于 -80 基本就没法用了。getLinkSpeed()是协商速率注意它和实际吞吐不是一回事别拿它当测速结果。3.2 HttpClient 发请求并统计耗时HTTP 测试的核心是记录请求前后的时间戳拿到状态码和响应体长度。用HttpClient的DefaultHttpClient或AndroidHttpClient都行我这里用前者// HttpHelper.java public class HttpHelper { public static String testHttp(String url) { StringBuilder sb new StringBuilder(); HttpClient client new DefaultHttpClient(); // 设置超时避免一直卡住 HttpParams params client.getParams(); HttpConnectionParams.setConnectionTimeout(params, 5000); HttpConnectionParams.setSoTimeout(params, 5000); HttpGet get new HttpGet(url); long start System.currentTimeMillis(); try { HttpResponse response client.execute(get); long cost System.currentTimeMillis() - start; int code response.getStatusLine().getStatusCode(); sb.append(URL: ).append(url).append(\n); sb.append(状态码: ).append(code).append(\n); sb.append(耗时: ).append(cost).append( ms\n); // 读取响应体长度判断是否真的拿到数据 if (response.getEntity() ! null) { long len response.getEntity().getContentLength(); sb.append(响应长度: ).append(len).append( 字节); } } catch (Exception e) { long cost System.currentTimeMillis() - start; sb.append(请求失败: ).append(e.getClass().getSimpleName()) .append(\n耗时: ).append(cost).append( ms\n) .append(原因: ).append(e.getMessage()); } finally { client.getConnectionManager().shutdown(); } return sb.toString(); } }超时设置是这段代码的灵魂。setConnectionTimeout管的是建立 TCP 连接的时间setSoTimeout管的是连接建立后等待数据的时间两个都设 5000 毫秒。不设的话默认是无限等待用户点了测试按钮界面就卡死体验极差。耗时统计用System.currentTimeMillis()精度到毫秒够用了别用nanoTime除非你要测微秒级。finally里shutdown连接管理器防止连接泄漏——这个工具是反复点击测试的不释放连接跑几十次就会出问题。异常分支里把异常类型和消息都打出来UnknownHostException说明 DNS 有问题SocketTimeoutException说明网络慢或服务端不响应ConnectException说明端口不通这比只显示「失败」有用得多。3.3 把两个模块串起来并处理线程切换回到 ActivitystartTest里已经调用了这两个工具类。这里补充一个细节如果测试逻辑比较重建议用ExecutorService管理线程池而不是每次new Thread。小工具场景下new Thread够用但连续快速点击按钮会创建多个线程加个标志位防抖更稳妥private volatile boolean testing false; private void startTest() { if (testing) return; // 防抖避免重复点击 testing true; new Thread(() - { try { final String wifiResult WifiHelper.getWifiInfo(this); final String httpResult HttpHelper.testHttp(http://www.example.com); runOnUiThread(() - { tvWifiInfo.setText(wifiResult); tvHttpResult.setText(httpResult); }); } finally { testing false; } }).start(); }volatile保证testing变量在多线程间的可见性finally里复位标志位即使中途抛异常也能恢复。这个防抖逻辑看着简单但少了它用户手快连点五下就是五个线程同时发请求结果乱序回填界面显示的是哪次的结果都说不清。4. 避坑与排查那些让我加班到凌晨的细节4.1 SSID 显示为 unknown ssid现象代码逻辑没问题但getSSID()返回unknown ssidBSSID 返回02:00:00:00:00:00。原因Android 6.0 之后SSID 和 BSSID 被归类为位置信息没有定位权限时系统直接返回占位符。解决在AndroidManifest.xml声明ACCESS_FINE_LOCATION并在运行时动态申请用户授权后再调用getConnectionInfo()。注意部分厂商 ROM 还要求打开系统定位开关光有权限不够这个只能引导用户去设置里开。4.2 主线程网络异常 NetworkOnMainThreadException现象点击按钮直接崩溃日志里是android.os.NetworkOnMainThreadException。原因Android 3.0 起禁止在主线程做网络操作HttpClient.execute是阻塞调用放主线程必崩。解决所有网络请求包在子线程里用new Thread或ExecutorService都行结果通过runOnUiThread或Handler回主线程。别用AsyncTask它在 API 30 已废弃新代码不建议再用。4.3 请求一直卡住不返回现象点了测试按钮界面一直转圈几分钟都没结果。原因没设超时HttpClient默认无限等待遇到服务端不响应或网络黑洞就一直挂着。解决HttpConnectionParams.setConnectionTimeout和setSoTimeout都设上一般 5000 毫秒。另外finally里必须shutdown连接管理器否则连接池耗尽后后续请求全部阻塞。4.4 高版本 Android 上 HttpClient 找不到类现象在 API 28 以上的设备运行报NoClassDefFoundError: org.apache.http.client.HttpClient。原因Android 9.0 起移除了 Apache HTTP 客户端org.apache.http包默认不可用。解决在AndroidManifest.xml的application标签里加uses-library android:nameorg.apache.http.legacy android:requiredfalse /或者干脆改用HttpURLConnection。我一般推荐后者代码改动不大兼容性更好。4.5 信号强度读出来是 0 或负数异常现象getRssi()返回 0 或者 -127 这种极端值。原因Wi-Fi 刚连接上时WifiInfo还没刷新或者设备处于息屏状态导致数据不更新。解决加个短暂延迟再读或者注册BroadcastReceiver监听WIFI_STATE_CHANGED_ACTION等状态稳定后再采集。另外 -127 通常表示无效值展示时判断一下小于 -100 就显示「信号极弱」而不是直接甩数字。5. 进阶玩法把测试结果落盘和批量对比小工具跑通之后我一般会加两个进阶功能让它从「看一眼」变成「能对比」。第一个是结果落盘把每次测试的 SSID、耗时、状态码写进本地文件方便事后翻查第二个是批量测试对同一 URL 连续请求 N 次算平均耗时和成功率排除单次抖动。落盘用最简单的FileOutputStream追加写就行路径放getExternalFilesDir下不需要额外存储权限// 追加一条测试记录到本地文件 public static void appendLog(Context context, String line) { File dir context.getExternalFilesDir(null); if (dir null) return; File file new File(dir, net_test_log.txt); try (FileOutputStream fos new FileOutputStream(file, true)) { // 每行带时间戳方便排序 String record System.currentTimeMillis() | line \n; fos.write(record.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { Log.e(NetTest, 写日志失败, e); } }FileOutputStream第二个参数true表示追加模式不传或传false会覆盖原文件。getExternalFilesDir(null)返回应用专属外部目录卸载时自动清理不用申请WRITE_EXTERNAL_STORAGE。每行前面加时间戳后面用文本工具排序就能看出不同时间段的网络质量变化。批量测试则是在HttpHelper外面套一层循环统计成功次数和总耗时// 连续请求 count 次返回平均耗时和成功率 public static String batchTest(String url, int count) { long totalCost 0; int success 0; for (int i 0; i count; i) { long start System.currentTimeMillis(); boolean ok HttpHelper.pingOnce(url); // 内部只判断状态码是否 200 totalCost System.currentTimeMillis() - start; if (ok) success; } long avg totalCost / count; int rate success * 100 / count; return 平均耗时: avg ms\n成功率: rate %; }count一般取 5 到 10太多用户等不及太少没统计意义。平均耗时能过滤掉单次网络抖动成功率低于 80% 就说明这条链路有问题值得进一步查。这里有个经验批量测试之间最好加 200 毫秒间隔连续高频请求可能触发服务端限流测出来的成功率偏低是假象。最后说个我自己的习惯。这个工具我每次改完网络相关代码都会跑一遍而且固定用同一个测试 URL这样不同版本之间的数据才有可比性。有一次我图省事换了个 URL结果耗时从 80 毫秒跳到 400 毫秒排查半天才发现是新 URL 的服务器在海外跟代码改动毫无关系。从那以后我每次做网络对比测试都强制固定 URL 和测试时段避免自己给自己挖坑。希望这个小工具和这些踩坑记录能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑