通过 Wi-Fi 直连创建点对点连接

借助 Wi-Fi 直连(也称为点对点或点对点连接),您的应用可以在超出蓝牙功能的范围内快速查找附近的设备并与之互动。

借助 Wi-Fi 直连(点对点)API,应用无需连接到网络或热点即可连接到附近的设备。如果您的应用旨在成为安全的近距离网络的一部分,则 Wi-Fi 直连比传统 Wi-Fi 临时网络更合适,原因如下:

  • Wi-Fi 直连支持 WPA2 加密。(某些临时网络仅支持 WEP 加密。)
  • 设备可以广播其提供的服务,这有助于其他设备更轻松地发现合适的对等设备。
  • 在确定哪台设备应为网络的群组所有者时,WLAN 直连会检查每个设备的电源管理、界面和服务功能,并使用这些信息选择能够最有效地处理服务器职责的设备。
  • Android 不支持 Wi-Fi 临时模式。

本课程介绍了如何使用 Wi-Fi 点对点查找和连接到附近的设备。

设置应用权限

如需使用 Wi-Fi 直连,请将 ACCESS_FINE_LOCATIONCHANGE_WIFI_STATEACCESS_WIFI_STATEINTERNET 权限添加到您的清单中。如果您的应用以 Android 13(API 级别 33)或更高版本为目标平台,请同时向清单中添加 NEARBY_WIFI_DEVICES 权限。Wi-Fi 直连不需要互联网连接,但使用标准 Java 套接字,这需要 INTERNET 权限。因此,您需要拥有以下权限才能使用 WLAN 直连:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.nsdchat"
    ...
    <!-- If your app targets Android 13 (API level 33)
         or higher, you must declare the NEARBY_WIFI_DEVICES permission. -->
        <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
        <!-- If your app derives location information from Wi-Fi APIs,
             don't include the "usesPermissionFlags" attribute. -->
        android:usesPermissionFlags="neverForLocation" />
        
    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_FINE_LOCATION"
        <!-- If any feature in your app relies on precise location information,
             don't include the "maxSdkVersion" attribute. -->
        android:maxSdkVersion="32" />
    <uses-permission
        android:required="true"
        android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.CHANGE_WIFI_STATE"/>
    <uses-permission
        android:required="true"
        android:name="android.permission.INTERNET"/>
    ...

除上述权限外,您还需要启用位置信息模式才能使用以下 API:

设置广播接收器和对等连接管理器

如需使用 Wi-Fi 直连,您需要监听广播 intent,它会告知您的应用某些事件何时发生。在您的应用中,实例化 IntentFilter 并将其设置为监听以下内容:

WIFI_P2P_STATE_CHANGED_ACTION
指示是否启用 Wi-Fi 直连
WIFI_P2P_PEERS_CHANGED_ACTION
表示可用的对等设备列表已更改。
WIFI_P2P_CONNECTION_CHANGED_ACTION
用于指示 Wi-Fi 直连连接的状态已更改。从 Android 10 开始,这不是固定的。如果您的应用依赖于在注册时接收这些广播(因为它们一直具有粘性),请在初始化时使用适当的 get 方法获取信息。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
指示此设备的配置详细信息已更改。从 Android 10 开始,这不是固定的。如果您的应用依赖于在注册时接收这些广播(因为它们一直具有粘性),请在初始化时使用适当的 get 方法获取信息。

Kotlin

private val intentFilter = IntentFilter()
...
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main)

    // Indicates a change in the Wi-Fi Direct status.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)

    // Indicates a change in the list of available peers.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)

    // Indicates the state of Wi-Fi Direct connectivity has changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)

    // Indicates this device's details have changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
    ...
}

Java

private final IntentFilter intentFilter = new IntentFilter();
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Indicates a change in the Wi-Fi Direct status.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

    // Indicates a change in the list of available peers.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

    // Indicates the state of Wi-Fi Direct connectivity has changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

    // Indicates this device's details have changed.
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    ...
}

onCreate() 方法的末尾,获取 WifiP2pManager 的实例,并调用其 initialize() 方法。此方法会返回一个 WifiP2pManager.Channel 对象,稍后您将使用该对象将应用连接到 Wi-Fi 直连框架。

Kotlin

private lateinit var channel: WifiP2pManager.Channel
private lateinit var manager: WifiP2pManager

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
    channel = manager.initialize(this, mainLooper, null)
}

Java

Channel channel;
WifiP2pManager manager;

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    channel = manager.initialize(this, getMainLooper(), null);
}

现在创建一个新的 BroadcastReceiver 类,您将使用该类监听对系统的 Wi-Fi 状态所做的更改。在 onReceive() 方法中,添加一个条件来处理上面列出的每个状态更改。

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    when(intent.action) {
        WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
            // Determine if Wi-Fi Direct mode is enabled or not, alert
            // the Activity.
            val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
            activity.isWifiP2pEnabled = state == WifiP2pManager.WIFI_P2P_STATE_ENABLED
        }
        WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {

            // The peer list has changed! We should probably do something about
            // that.

        }
        WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

            // Connection state changed! We should probably do something about
            // that.

        }
        WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
            (activity.supportFragmentManager.findFragmentById(R.id.frag_list) as DeviceListFragment)
                    .apply {
                        updateThisDevice(
                                intent.getParcelableExtra(
                                        WifiP2pManager.EXTRA_WIFI_P2P_DEVICE) as WifiP2pDevice
                        )
                    }
        }
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
        // Determine if Wi-Fi Direct mode is enabled or not, alert
        // the Activity.
        int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
        if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
            activity.setIsWifiP2pEnabled(true);
        } else {
            activity.setIsWifiP2pEnabled(false);
        }
    } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

        // The peer list has changed! We should probably do something about
        // that.

    } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

        // Connection state changed! We should probably do something about
        // that.

    } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
        DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
                .findFragmentById(R.id.frag_list);
        fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
                WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));

    }
}

最后,添加代码,以便在主 activity 处于活动状态时注册 intent 过滤器和广播接收器,并在 activity 暂停时取消注册 intent 过滤器和广播接收器。最好使用 onResume()onPause() 方法。

Kotlin

/** register the BroadcastReceiver with the intent values to be matched  */
public override fun onResume() {
    super.onResume()
    receiver = WiFiDirectBroadcastReceiver(manager, channel, this)
    registerReceiver(receiver, intentFilter)
}

public override fun onPause() {
    super.onPause()
    unregisterReceiver(receiver)
}

Java

/** register the BroadcastReceiver with the intent values to be matched */
@Override
public void onResume() {
    super.onResume();
    receiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
    registerReceiver(receiver, intentFilter);
}

@Override
public void onPause() {
    super.onPause();
    unregisterReceiver(receiver);
}

启动对等设备发现

如需使用 Wi-Fi 点对点连接开始搜索附近的设备,请调用 discoverPeers()。此方法采用以下参数:

Kotlin

manager.discoverPeers(channel, object : WifiP2pManager.ActionListener {

    override fun onSuccess() {
        // Code for when the discovery initiation is successful goes here.
        // No services have actually been discovered yet, so this method
        // can often be left blank. Code for peer discovery goes in the
        // onReceive method, detailed below.
    }

    override fun onFailure(reasonCode: Int) {
        // Code for when the discovery initiation fails goes here.
        // Alert the user that something went wrong.
    }
})

Java

manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {

    @Override
    public void onSuccess() {
        // Code for when the discovery initiation is successful goes here.
        // No services have actually been discovered yet, so this method
        // can often be left blank. Code for peer discovery goes in the
        // onReceive method, detailed below.
    }

    @Override
    public void onFailure(int reasonCode) {
        // Code for when the discovery initiation fails goes here.
        // Alert the user that something went wrong.
    }
});

请记住,这仅启动对等设备发现。discoverPeers() 方法启动发现过程,然后立即返回。系统会通过在提供的操作监听器中调用方法通知您是否已成功启动对等设备发现过程。此外,在启动连接或形成点对点连接群组之前,发现会一直保持活跃状态。

获取对等设备列表

现在编写获取并处理对等设备列表的代码。首先实现 WifiP2pManager.PeerListListener 接口,该接口提供有关 Wi-Fi 直连检测到的对等设备的信息。借助此信息,您的应用还可以确定对等设备何时加入或离开网络。以下代码段展示了这些与对等方相关的操作:

Kotlin

private val peers = mutableListOf<WifiP2pDevice>()
...

private val peerListListener = WifiP2pManager.PeerListListener { peerList ->
    val refreshedPeers = peerList.deviceList
    if (refreshedPeers != peers) {
        peers.clear()
        peers.addAll(refreshedPeers)

        // If an AdapterView is backed by this data, notify it
        // of the change. For instance, if you have a ListView of
        // available peers, trigger an update.
        (listAdapter as WiFiPeerListAdapter).notifyDataSetChanged()

        // Perform any other updates needed based on the new list of
        // peers connected to the Wi-Fi P2P network.
    }

    if (peers.isEmpty()) {
        Log.d(TAG, "No devices found")
        return@PeerListListener
    }
}

Java

private List<WifiP2pDevice> peers = new ArrayList<WifiP2pDevice>();
...

private PeerListListener peerListListener = new PeerListListener() {
    @Override
    public void onPeersAvailable(WifiP2pDeviceList peerList) {

        List<WifiP2pDevice> refreshedPeers = peerList.getDeviceList();
        if (!refreshedPeers.equals(peers)) {
            peers.clear();
            peers.addAll(refreshedPeers);

            // If an AdapterView is backed by this data, notify it
            // of the change. For instance, if you have a ListView of
            // available peers, trigger an update.
            ((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();

            // Perform any other updates needed based on the new list of
            // peers connected to the Wi-Fi P2P network.
        }

        if (peers.size() == 0) {
            Log.d(WiFiDirectActivity.TAG, "No devices found");
            return;
        }
    }
}

现在,修改广播接收器的 onReceive() 方法,以便在收到具有 WIFI_P2P_PEERS_CHANGED_ACTION 操作的 intent 时调用 requestPeers()。您需要以某种方式将此监听器传递给接收器。一种方式是将其作为参数发送给广播接收器的构造函数。

Kotlin

fun onReceive(context: Context, intent: Intent) {
    when (intent.action) {
        ...
        WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {

            // Request available peers from the wifi p2p manager. This is an
            // asynchronous call and the calling activity is notified with a
            // callback on PeerListListener.onPeersAvailable()
            mManager?.requestPeers(channel, peerListListener)
            Log.d(TAG, "P2P peers changed")


        }
        ...
    }
}

Java

public void onReceive(Context context, Intent intent) {
    ...
    else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {

        // Request available peers from the wifi p2p manager. This is an
        // asynchronous call and the calling activity is notified with a
        // callback on PeerListListener.onPeersAvailable()
        if (mManager != null) {
            mManager.requestPeers(channel, peerListListener);
        }
        Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
    }...
}

现在,具有操作 WIFI_P2P_PEERS_CHANGED_ACTION intent 的 intent 触发对更新后的对等设备列表的请求。

连接到对等设备

为了连接到对等设备,请创建一个新的 WifiP2pConfig 对象,然后从表示您要连接的设备的 WifiP2pDevice 中将数据复制到该对象中。然后调用 connect() 方法。

Kotlin

override fun connect() {
    // Picking the first device found on the network.
    val device = peers[0]

    val config = WifiP2pConfig().apply {
        deviceAddress = device.deviceAddress
        wps.setup = WpsInfo.PBC
    }

    manager.connect(channel, config, object : WifiP2pManager.ActionListener {

        override fun onSuccess() {
            // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
        }

        override fun onFailure(reason: Int) {
            Toast.makeText(
                    this@WiFiDirectActivity,
                    "Connect failed. Retry.",
                    Toast.LENGTH_SHORT
            ).show()
        }
    })
}

Java

@Override
public void connect() {
    // Picking the first device found on the network.
    WifiP2pDevice device = peers.get(0);

    WifiP2pConfig config = new WifiP2pConfig();
    config.deviceAddress = device.deviceAddress;
    config.wps.setup = WpsInfo.PBC;

    manager.connect(channel, config, new ActionListener() {

        @Override
        public void onSuccess() {
            // WiFiDirectBroadcastReceiver notifies us. Ignore for now.
        }

        @Override
        public void onFailure(int reason) {
            Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
                    Toast.LENGTH_SHORT).show();
        }
    });
}

如果组中的每个设备都支持 Wi-Fi 直连,则在连接时不需要明确要求输入组的密码。不过,如需允许不支持 Wi-Fi 直连的设备加入群组,您需要通过调用 requestGroupInfo() 来检索此密码,如以下代码段所示:

Kotlin

manager.requestGroupInfo(channel) { group ->
    val groupPassword = group.passphrase
}

Java

manager.requestGroupInfo(channel, new GroupInfoListener() {
  @Override
  public void onGroupInfoAvailable(WifiP2pGroup group) {
      String groupPassword = group.getPassphrase();
  }
});

请注意,在 connect() 方法中实现的 WifiP2pManager.ActionListener 仅在启动成功或失败时通知您。如需监听连接状态的变化,请实现 WifiP2pManager.ConnectionInfoListener 接口。onConnectionInfoAvailable() 回调会在连接状态发生变化时通知您。如果多个设备要连接到单个设备(例如有三个或更多玩家的游戏,或聊天应用),其中一个设备被指定为“群组所有者”。您可以按照创建群组部分中的步骤,将特定设备指定为网络的群组所有者。

Kotlin

private val connectionListener = WifiP2pManager.ConnectionInfoListener { info ->

    // String from WifiP2pInfo struct
    val groupOwnerAddress: String = info.groupOwnerAddress.hostAddress

    // After the group negotiation, we can determine the group owner
    // (server).
    if (info.groupFormed && info.isGroupOwner) {
        // Do whatever tasks are specific to the group owner.
        // One common case is creating a group owner thread and accepting
        // incoming connections.
    } else if (info.groupFormed) {
        // The other device acts as the peer (client). In this case,
        // you'll want to create a peer thread that connects
        // to the group owner.
    }
}

Java

@Override
public void onConnectionInfoAvailable(final WifiP2pInfo info) {

    // String from WifiP2pInfo struct
    String groupOwnerAddress = info.groupOwnerAddress.getHostAddress();

    // After the group negotiation, we can determine the group owner
    // (server).
    if (info.groupFormed && info.isGroupOwner) {
        // Do whatever tasks are specific to the group owner.
        // One common case is creating a group owner thread and accepting
        // incoming connections.
    } else if (info.groupFormed) {
        // The other device acts as the peer (client). In this case,
        // you'll want to create a peer thread that connects
        // to the group owner.
    }
}

现在,返回到广播接收器的 onReceive() 方法,并修改用于监听 WIFI_P2P_CONNECTION_CHANGED_ACTION intent 的部分。收到此 intent 时,调用 requestConnectionInfo()。这是一个异步调用,因此,您作为参数提供的连接信息监听器会收到结果。

Kotlin

when (intent.action) {
    ...
    WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {

        // Connection state changed! We should probably do something about
        // that.

        mManager?.let { manager ->

            val networkInfo: NetworkInfo? = intent
                    .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) as NetworkInfo

            if (networkInfo?.isConnected == true) {

                // We are connected with the other device, request connection
                // info to find group owner IP

                manager.requestConnectionInfo(channel, connectionListener)
            }
        }
    }
    ...
}

Java

    ...
    } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {

        if (manager == null) {
            return;
        }

        NetworkInfo networkInfo = (NetworkInfo) intent
                .getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

        if (networkInfo.isConnected()) {

            // We are connected with the other device, request connection
            // info to find group owner IP

            manager.requestConnectionInfo(channel, connectionListener);
        }
        ...

创建群组

如果您希望运行您的应用的设备充当包含旧版设备(即不支持 Wi-Fi 直连的设备)的网络的群组所有者,请遵循连接到对等设备部分中的相同步骤顺序,只是使用 createGroup() 而不是 connect() 创建新的 WifiP2pManager.ActionListenerWifiP2pManager.ActionListener 中的回调处理方式相同,如以下代码段所示:

Kotlin

manager.createGroup(channel, object : WifiP2pManager.ActionListener {
    override fun onSuccess() {
        // Device is ready to accept incoming connections from peers.
    }

    override fun onFailure(reason: Int) {
        Toast.makeText(
                this@WiFiDirectActivity,
                "P2P group creation failed. Retry.",
                Toast.LENGTH_SHORT
        ).show()
    }
})

Java

manager.createGroup(channel, new WifiP2pManager.ActionListener() {
    @Override
    public void onSuccess() {
        // Device is ready to accept incoming connections from peers.
    }

    @Override
    public void onFailure(int reason) {
        Toast.makeText(WiFiDirectActivity.this, "P2P group creation failed. Retry.",
                Toast.LENGTH_SHORT).show();
    }
});

注意 :如果网络中的所有设备都支持 Wi-Fi 直连,您可以在每个设备上使用 connect() 方法,因为该方法随后会创建群组并选择群组所有者。

创建群组后,您可以调用 requestGroupInfo() 来检索有关网络上对等设备的详细信息,包括设备名称和连接状态。