Android 设备通过WIFI传输数据 - 点对点传输

点对点传输(P2P)又是 WLAN 直连,他可以在没有中间接入点的情况下,通过 WLAN 进行直接互联。他有用户介入操作少,比蓝牙传输速度高等特点,对设备的要求仅仅为 14,同时他又不占用 wlan0 网卡。

WLAN P2P 需要使用到 WifiP2pManager ,同时需要以下权限,这里面有一些是运行时权限,需要用户同意后才能使用。

1
2
3
4
5
6
7
<uses-sdk android:minSdkVersion="14" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<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" />

点对点传输(P2P)至少有两个有 Wifi 的设备,其中一个是 Android,首先确定 Android 设备和另外一个设备是否支持 P2P 连接。把手机连接电脑运行 adb shell ip addr|grep p2p0 -A2有输出就带边可以使用,一般来说都可以使用。

1
2
29: p2p0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
link/ether 02:00:2d:63:a5:6b brd ff:ff:ff:ff:ff:ff

注意看上面的输出,link/ether 02:00:2d:63:a5:6 为 p2p 的 Mac 地址,不同的设备之间使用 Mac 地址连接,所以首先要知道 P2P(p2p0)的 Mac 地址,这个和 wifi(wlan0)的地址不是同一个,在代码中需要使用下面方法获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static String getLocalMacAddress() {
try {
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface ntwInterface : interfaces) {

if (ntwInterface.getName().equalsIgnoreCase("p2p0")) {
byte[] byteMac = ntwInterface.getHardwareAddress();
if (byteMac == null) {
return null;
}
StringBuilder strBuilder = new StringBuilder();
for (int i = 0; i < byteMac.length; i++) {
strBuilder.append(String.format("%02X:", byteMac[i]));
}

if (strBuilder.length() > 0) {
strBuilder.deleteCharAt(strBuilder.length() - 1);
}

return strBuilder.toString();
}

}
} catch (Exception e) {
Log.d("Lecon", e.getMessage());
}
return null;
}

接下来看一下如何主动连接到 p2p 设备。

使用方法

首先通过 WifiP2pManager 的 initialize 初始化。

1
2
3
4
5
6
7
val manager: WifiP2pManager? by lazy(LazyThreadSafetyMode.NONE) {
getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
}

override fun onCreate(savedInstanceState: Bundle?) {
mChannel = manager?.initialize(this, mainLooper, null)
}

同时使用广播来接受各种 P2P 连接的状态变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var mChannel: WifiP2pManager.Channel? = null
var mReceiver: WiFiDirectBroadcastReceiver? = null

private val mWifiP2pManager: WifiP2pManager by lazy(LazyThreadSafetyMode.NONE) {
getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
}

private val mIntentFilter: IntentFilter by lazy(LazyThreadSafetyMode.NONE) {
IntentFilter().apply {
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mChannel = mWifiP2pManager.initialize(this, mainLooper, null)
mReceiver = WiFiDirectBroadcastReceiver(mWifiP2pManager, mChannel, this)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class WiFiDirectBroadcastReceiver(
private val mManager: WifiP2pManager?,
private val mChannel: WifiP2pManager.Channel?,
private val mActivity: MainActivity
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION == action) {
// 当 WLAN P2P 在设备上启用或停用时广播
} else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION == action) {
// 调用 requestPeers() 方法,以获得当前所发现对等设备的列表。
// 同时 在这里调用 connect 方法连接对方机器
} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION == action) {
// 当设备的 WLAN 连接状态更改时广播。
// 连接对方机器成功或者失败都会在这里回调
} else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION == action) {
// 当设备的详细信息(例如设备名称)更改时广播
}
}

上边哪一些代码可以当成主动发起扫描之后的回调,使用广播进行回调说起来真的是挺复杂的,但是仔细想想,操作硬件设备本来就是跨进程的,而且这个还是长时间耗时操作,系统通过广播回调也是合理的。

到现在位置,可以把以上代码运行到 Android 上,他就可以最为 P2P 连接的被连接端。

连接到设备

连接到设备的时候,首先要确定对方设备的 mac 地址,上面的两种方法是针对于 Android 设备的。一种是 adb 方式,一种是代码获取。

接下来要发现设备:

1
2
3
4
5
6
7
8
9
mWifiP2pManager.discoverPeers(mChannel, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
Toast.makeText(this@MainActivity, "已发现设备,准备连接", Toast.LENGTH_SHORT).show()
}

override fun onFailure(reasonCode: Int) {

}
})

返现设备之后,通过广播回调的 WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION 事件获取设备列表。找到列表中的设备与被连接设备 mac 地址一致的,确保我们被连接机器已经就绪。

1
2
3
4
5
6
7
mManager?.requestPeers(mChannel) { peers ->
for (device in peers.deviceList) {
if (mac != null && mac.equals(device.deviceAddress, ignoreCase = true)) {
connectToDevice(device)
}
}
}

接下来就可以连接设备了!

1
2
3
4
5
6
7
8
9
val config = WifiP2pConfig()
config.deviceAddress = "被连接设备的Mac地址"
mManager?.connect(mChannel, config, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
Toast.makeText(mActivity, "连接成功", Toast.LENGTH_SHORT).show()
}

override fun onFailure(reason: Int) {}
})

如果被连接设备是 Android,你应该能看到一个连接提示,点击接受这样两台设备之间就连接成功了,通过 WIFI 两个设备可以实现近场通讯。

通信

通信之前需要知道被连接设备的 ip 地址。如果是 android 设备,在被连接设备执行adb shell ip addr|grep p2p0 -A4 就可以看到,这也是检测是否连接成功的方法。代码获取仍然要获取 p2p0 网卡的,wlan0 获取的 ip 地址不能用于这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static String getLocalIp() {
try {
List<NetworkInterface> interfaces = Collections
.list(NetworkInterface.getNetworkInterfaces());

for (NetworkInterface intf : interfaces) {
if (!intf.getName().contains("p2p0"))
continue;

List<InetAddress> addrs = Collections.list(intf
.getInetAddresses());
for (InetAddress addr : addrs) {
if (!addr.isLoopbackAddress()) {
return addr.getHostAddress().toUpperCase();
}
}
}

} catch (Exception ex) {
ex.printStackTrace();
}
return "";
}

如果被连接设备是一台服务器(在 Android 里面搭建一台服务器也是可以的),在这台设备上可以用 okhttp 等框架进行网络访问,或者使用 socket 进行传输。
现在两个手机(P2P)设备之间就可以同过 WLAN 直接通信了。

下面是源代码:https://github.com/leconio/WifiDircetP2PDemo


 Gitalk评论