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

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

<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有输出就带边可以使用,一般来说都可以使用。

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)的地址不是同一个,在代码中需要使用下面方法获取。

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初始化。

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连接的状态变化。

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)
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方式,一种是代码获取。

接下来要发现设备:

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地址一致的,确保我们被连接机器已经就绪。

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

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

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地址不能用于这里。

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