Android 中使用 WI-FI 直连/P2P 实现高速数据传输
原作者:Tans5
一个大文件极速传输应用,支持android端,以及mac,windows。大大解决了android手机和mac之间传输文件的繁琐问题。
0 简单介绍
就算做过好几年的 Android 开发老油条,有很多人都不知道 WI-FI 直连。简单来说就是可以将两台支持 WI-FI 直连设备建立一条高速的通信连接,连接建立后我们就可以通过 TCP/UDP 协议来传输我们想要的数据。相对于蓝牙的数据传输,WI-FI 直连的传输速度、距离、延迟和稳定性都大幅度优于蓝牙;相对于普通的局域网内的通信速度和稳定性也是有优势,只是在某些比较好的局域网内才能够与 WI-FI 直连 媲美。
WI-FI 直连通常用在需要传递大量数据的场景,比如大文件的分享、无人机图传等等场景。Android 从 4.0 版本就已经支持 WI-FI 直连,基本就是有 WI-FI 的 Android 设备都支持。条件要求特别低只需要两台手机打开 WI-FI 就能创建连接。(不需要手机接入 WLAN 哦)
给一个我自己开源的基于 WI-FI 直连的文件传输应用截图:
感兴趣的可以看看源码实现:tFileTransfer WI-FI 直连相关代码, 其中代码我用协程封装了一下。
Tips: 需要达到上面的传输速度,还得通过多线程创建多条连接传输数据,才能达到网络带宽的上限。
1 权限添加
1.1 普通权限
// ...
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
// ...
1.2 敏感权限
敏感权限在 Android 6 以后需在运行时动态再申请一次,动态申请权限的代码就不再贴了。
在 Android 13 及其以后的版本需要 NEARBY_WIFI_DEVICES 权限:
// ...
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="tiramisu" />
// ...
在 Android 12 及其以前的版本需要 ACCESS_FINE_LOCATION 权限:
// ...
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32"/>
// ...
1.3 注意
在 Android 上要让 WI-FI 直连正常工作,一定得打开 WI-FI 和 GPS 开关。
2 构建 WI-FI 直连连接
2.1 准备工作
获取 WifiP2pManager 和 初始化 WifiP2pChannel:
val wifiP2pManager = context?.getSystemService(Context.WIFI_P2P_SERVICE) as? WifiP2pManager // 获取 WiFiP2pManager
val wifiChannel = wifiP2pManager?.initialize(requireActivity(), requireActivity().mainLooper, null) // 初始化 WifiChannel
动态注册 BroadcastReceiver 来监听后续操作中 WI-FI 直连中需要用到的各种状态:
val intentFilter = IntentFilter().apply {
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) // Wifi 直连可用状态改变
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) // Wifi 直连发现的设备改变
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) // Wifi 直连的连接状态改变
}
requireActivity().registerReceiver(wifiReceiver, intentFilter)
private val wifiReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
if (state == WifiP2pManager.WIFI_P2P_STATE_DISABLED) {
AndroidLog.e(TAG, "Wifi p2p disabled.")
} else {
AndroidLog.d(TAG, "Wifi p2p enabled.")
}
}
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
val wifiDevicesList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST, WifiP2pDeviceList::class.java)
} else {
intent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
}
AndroidLog.d(TAG, "WIFI p2p devices: ${wifiDevicesList?.deviceList?.joinToString { "${it.deviceName} -> ${it.deviceAddress}" }}")
}
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
AndroidLog.d(TAG, "Connection state change.")
}
}
}
}
其中 WIFI_P2P_STATE_CHANGED_ACTION 表示 WI-FI 直连可以状态改变,比如说这时开关 WI-FI。
WIFI_P2P_PEERS_CHANGED_ACTION 表示发现的设备发生改变,后续会再讲到。
WIFI_P2P_CONNECTION_CHANGED_ACTION 表示连接状态改变,后续会再讲到。
2.2 查询附近设备
通过前面准备工作初始化的 WifiManager
和 WifiChannel
实例请求查询附近设备,通常可以通过用户触发查询,也可以开启循环任务定时查询,取决于自己的需求。
wifiP2pManager.discoverPeers(wifiChannel, object : ActionListener {
override fun onSuccess() {
TODO("Not yet implemented")
}
override fun onFailure(p0: Int) {
TODO("Not yet implemented")
}
})
如果查询成功会触发能够触发准备工作中注册的广播监听,通过以下方式能够拿到对应的设备信息。
// ...
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
val wifiDevicesList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST, WifiP2pDeviceList::class.java)
} else {
intent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST)
}
// ...
}
// ...
查询设备成功后可以根据返回的结果,通过以下方式获取发现的设备的名字和硬件地址。
for (d in wifiDevicesList?.deviceList ?: emptyList()) {
val deviceName = d.deviceName
val macAddress = d.deviceAddress
}
Tips: 从 Android 10 开始,Google 慢慢禁掉了可以定位用户设备的方法,类似于这里的硬件地址,获取到的地址是假的,不过可以获取到其他设备的硬件地址。
查询成功后也可以手动触发查询设备。
wifiP2pManager.requestPeers(wifiChannel, object : PeerListListener {
override fun onPeersAvailable(p0: WifiP2pDeviceList?) {
TODO("Not yet implemented")
}
})
2.3 请求与目标设备连接
val config = WifiP2pConfig()
config.deviceAddress = data.macAddress
wifiP2pManager.connect(wifiChannel, config, object : ActionListener {
override fun onSuccess() {
TODO("Not yet implemented")
}
override fun onFailure(p0: Int) {
TODO("Not yet implemented")
}
})
其中的 MAC 地址,是在发现阶段获取到的。
同样的查询成功后也会触发上面动态注册的广播接收器。
// ....
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
// ....
}
// ...
通过以下方法查询连接信息。
wifiP2pManager.requestConnectionInfo(wifiChannel) {
val ownerIpAddress = it.groupOwnerAddress
val isOwner = it.isGroupOwner
}
这个时候连接就可以使用了,如果当前设备是 owner 那么,owner ip address 就是当前设置的 IP 地址,通常这时 owner 就可以开启一个 TCP / UDP 服务,非 owner 的设备就可以连接这个开启的服务,这样一个网络连接就建立完成了。可以传输你需要的数据了。
2.4 回收资源
如果不再使用,需要回收。
断开连接,如果需要重新连接其他设备,也可以调用这个方法把当前的连接关闭。
wifiP2pManager.cancelConnect(wifiChannel, null)
wifiP2pManager.removeGroup(wifiChannel, null)
取消注册广播。
requireActivity().unregisterReceiver(wifiReceiver)