说到在 Android 手机上搞网络调试,很多人第一反应就是:“没 Root 是不是就废了?” 其实完全不是。从 Android 10(API 29)开始,系统对 /proc/net 这种底层文件的访问权限收紧了,但这并不代表我们束手无策。相反,它逼迫我们换了一种更“Android 原生”且更优雅的方式来思考问题。今天我们就来聊聊,怎么在不 Root 的情况下,搞定本机端口扫描,以及为什么扫描其他设备依然是一场硬仗。
本机端口扫描:从“暴力读取”到“优雅调用”
在 Android 9 及之前的版本,开发者们喜欢直接 FileInputStream 去读 /proc/net/tcp 或者 /proc/net/udp 文件。那里面是一堆十六进制的原始数据,解析起来既麻烦又不安全。而且,一旦设备升级或系统策略变动,这些路径往往直接失效。
现在,我们有两个非常稳妥的方案:使用系统级的 NetworkStatsManager 配合 NetStat 工具(通过 ADB),或者在代码内部利用 Inet 系列类进行探测。
方案一:使用 NetStat 工具类(推荐用于调试场景)
如果你是在通过 ADB 连接电脑进行调试,最简单的办法其实是直接使用 Linux 原生的 netstat 或 ss 命令。虽然你不能直接在 App 代码里随便调,但你可以在开发工具里轻松拿到结果。
不过,如果你希望 App 内部就能显示“我现在开了哪些端口”,或者用于网络诊断功能,我们可以写一个基于 InetAddress 的端口探测逻辑。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class PortScanner {
private static final int START_PORT = 1;
private static final int END_PORT = 65535;
private static final int TIMEOUT = 500; // 毫秒,超时时间
public List<Integer> scanLocalPorts() {
List<Integer> openPorts = new ArrayList<>();
// 注意:这里只是演示逻辑,扫描全端口非常耗时,生产环境建议只扫常用端口
int[] commonPorts = {80, 443, 8080, 8443, 3000, 5000, 22, 21, 25, 53};
for (int port : commonPorts) {
if (isPortOpen(port)) {
openPorts.add(port);
}
}
return openPorts;
}
private boolean isPortOpen(int port) {
try (Socket socket = new Socket()) {
// 连接本机 localhost
socket.connect(new InetSocketAddress("127.0.0.1", port), TIMEOUT);
return true;
} catch (IOException e) {
return false;
}
}
}
为什么这个代码能工作?
因为 Socket.connect 尝试建立 TCP 连接。如果端口处于 LISTEN 状态,系统会返回成功;如果端口未打开或防火墙拦截,则会抛出 IOException。这种方式不依赖 Root,也不依赖 /proc/net/tcp 的解析,完全符合 Android 10+ 的安全规范。
方案二:利用 NetworkStatsManager 获取连接统计
如果你更关心的是“有哪些进程在用网络”,而不是单纯的端口监听,NetworkStatsManager 是更好的选择。它能告诉你某个 UID 在某个时间段内发送或接收了多少数据,从而间接推断出活跃的网络连接。
import android.app.usage.NetworkStats;
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
public class NetworkStatsHelper {
private final Context context;
private final NetworkStatsManager statsManager;
public NetworkStatsHelper(Context context) {
this.context = context;
statsManager = (NetworkStatsManager) context.getSystemService(Context.NETWORK_STATS_SERVICE);
}
/**
* 获取当前活跃的网络接口信息
*/
public boolean isNetworkActive() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
return capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR));
}
}
关键点: Android 10+ 要求 PACKAGE_USAGE_STATS 权限才能读取详细的流量统计,但检查活跃网络状态只需要普通的 ACCESS_NETWORK_STATE 权限。
扫描其他设备:为什么这么难?
接下来是更有趣的部分。题目提到“扫描其他设备需 ADB 或第三方工具如 NetCut”。这背后其实涉及两个核心难点:网络隔离和权限沙箱。
1. 局域网扫描的权限限制
在 Android 10+ 中,即使你有 INTERNET 和 ACCESS_WIFI_STATE 权限,你也无法随意扫描同 WiFi 下的其他设备。因为:
- Android 10 的本地网络权限限制:从 Android 10 开始,
ACCESS_FINE_LOCATION权限成为获取局域网设备列表(如 WiFi MAC 地址、ARP 表)的前提。这是为了保护用户隐私,防止 App 通过扫描设备指纹来追踪用户位置。 - ARP 表不可直接读取:在 Linux 底层,我们可以通过
arp -a或读取/proc/net/arp来获取同网段设备的 IP 和 MAC。但在 Android 上,普通 App 没有权限读取/proc/net/arp,除非 Root。
2. 使用 ADB 进行外部设备扫描
如果你是在开发阶段,或者是一个内网调试工具,ADB 是最强的武器。你可以让手机通过 USB 或 TCP/IP 连接到电脑,然后利用电脑上的工具扫描局域网。
# 1. 连接手机到电脑(无线)
adb connect 192.168.1.100:5555
# 2. 在电脑上扫描同网段设备
nmap -sn 192.168.1.0/24
# 3. 如果必须通过手机执行扫描,可以使用 NetCat 或简单的 TCP 探针
# 注意:这需要手机已 Root 或有特殊权限,否则无法发送原始 ICMP 包
3. NetCut 类工具的原理与局限
NetCut 这类工具之所以能“踢人”或“扫描”,是因为它们通常:
- 需要 Root 权限:直接操作内核的 netfilter 模块。
- 或者利用 APK 的特权签名:如果是系统级 App,可以绕过应用沙箱。
- 或者使用 ADB 提权:通过 ADB shell 执行 root 命令。
对于普通开发者,我们能做什么?
如果我们想实现一个“发现局域网内其他 Android 设备”的功能(比如投屏、文件传输),正确的方法是使用 mDNS/Bonjour 或 UDP 广播,而不是端口扫描。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DeviceDiscovery {
private static final int BROADCAST_PORT = 5000;
private static final String BROADCAST_ADDR = "255.255.255.255";
public void sendDiscoveryBroadcast() {
try (DatagramSocket socket = new DatagramSocket()) {
String message = "HELLO_ANDROID";
InetAddress broadcastAddress = InetAddress.getByName(BROADCAST_ADDR);
DatagramPacket packet = new DatagramPacket(
message.getBytes(), message.length(), broadcastAddress, BROADCAST_PORT
);
socket.send(packet);
System.out.println("广播发送成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
为什么这比扫描端口好?
- 无需 Root:UDP 广播是标准网络操作,普通权限即可。
- 跨设备友好:只要目标设备上的 App 监听这个端口并回应,就能完成“发现”。
- 符合现代 Android 安全策略:不依赖漏洞或特权。
总结:选择正确的工具
| 场景 | 推荐方案 | 是否需要 Root |
|---|---|---|
| 扫描本机端口 | Socket.connect 探测常用端口 |
否 |
| 查看本机网络统计 | NetworkStatsManager |
否(需权限) |
| 扫描同 WiFi 其他设备 IP | ADB + nmap(从电脑端) | 否 |
| 局域网设备发现 | UDP 广播 / mDNS | 否 |
| ARP 表读取 / 精确 MAC 扫描 | /proc/net/arp |
是 |
给小朋友的话: 想象你的手机是一个小房子,端口就是房子里的窗户。你想看看自己家开了哪些窗户(本机扫描),你只需要走到每个窗户前看看有没有亮灯(尝试连接)。但如果你想看看邻居家的窗户(扫描其他设备),你不能直接飞过去看,因为你没有超能力(Root 权限)。这时,你需要请警察叔叔帮忙(ADB 工具),或者在小区里大喊一声“有人吗?”(UDP 广播),让邻居自己回应你。这就是为什么有些工具需要 Root,而有些则不需要的原因啦!
