Android.Mac.Windows | WI-FI 直连/P2P 实现大文件高速传输

2,210 阅读4分钟

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.gif

感兴趣的可以看看源码实现: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 查询附近设备

通过前面准备工作初始化的 WifiManagerWifiChannel 实例请求查询附近设备,通常可以通过用户触发查询,也可以开启循环任务定时查询,取决于自己的需求。


 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)