WLAN 直连(对等连接或 P2P)概览

Wi-Fi 直连 (P2P) 允许具有相应硬件的设备通过 Wi-Fi 直接互联,而无需使用中间接入点。使用这些 API,您可以实现支持 WLAN P2P 的设备间相互发现和连接,从而获得比蓝牙连接更远距离的高速连接通信效果。对于多人游戏或照片共享等需要在用户之间共享数据的应用而言,这一技术非常有用。

WLAN P2P API 包含以下主要部分:

  • 支持您发现、请求,以及连接到对等设备的方法(在 WifiP2pManager 类中定义)。
  • 支持您获知 WifiP2pManager 方法调用成功与否的监听器。调用 WifiP2pManager 方法时,每个方法均可收到作为参数传入的特定监听器。
  • 通知您 WLAN P2P 框架检测到的特定事件(例如连接断开或新发现对等设备)的 intent。

通常,您可以一起使用 API 的这三个主要组件。例如,您可以为针对 discoverPeers() 的调用提供 WifiP2pManager.ActionListener,以便 ActionListener.onSuccess()ActionListener.onFailure() 方法可以通知您。如果 discoverPeers() 方法发现对等设备列表已经更改,则还将广播 WIFI_P2P_PEERS_CHANGED_ACTION intent。

API 概览

WifiP2pManager 类提供了一些方法,可让您与设备上的 Wi-Fi 硬件交互,以执行发现和连接对等设备等操作。可执行的操作如下:

表 1. Wi-Fi P2P 方法

方法 说明
initialize() 通过 WLAN 框架注册应用。请先调用此方法,然后再调用任何其他 WLAN P2P 方法。
connect() 启动与具有指定配置的设备的对等连接。
cancelConnect() 取消任何正在进行的对等群组协商。
requestConnectInfo() 请求设备连接信息。
createGroup() 以群组所有者的身份,使用当前设备创建对等群组。
removeGroup() 移除当前对等群组。
requestGroupInfo() 请求对等群组信息。
discoverPeers() 启动对等设备发现。
requestPeers() 请求已发现对等设备的当前列表。

WifiP2pManager 方法使您可以在侦听器中进行传递,以便 WLAN P2P 框架可以向您的 activity 通知通话状态。表 2 介绍了可用的监听器接口和使用监听器的相应 WifiP2pManager 方法调用。

表 2. Wi-Fi P2P 监听器

监听器接口 关联的操作
WifiP2pManager.ActionListener connect()cancelConnect()createGroup()removeGroup()discoverPeers()
WifiP2pManager.ChannelListener initialize()
WifiP2pManager.ConnectionInfoListener requestConnectInfo()
WifiP2pManager.GroupInfoListener requestGroupInfo()
WifiP2pManager.PeerListListener requestPeers()

WLAN P2P API 定义当发生特定 WLAN P2P 事件时会广播的 intent,例如发现新的对等设备时,或设备的 WLAN 状态更改时。您可以通过创建广播接收器来处理这些 intent,从而在应用中注册接收这些 intent:

表 3. Wi-Fi P2P intent

intent 说明
WIFI_P2P_CONNECTION_CHANGED_ACTION 当设备的 WLAN 连接状态更改时广播。
WIFI_P2P_PEERS_CHANGED_ACTION 在调用 discoverPeers() 时广播。如果您在应用中处理此 intent,则通常需要调用 requestPeers() 以获取对等设备的更新列表。
WIFI_P2P_STATE_CHANGED_ACTION 当 WLAN P2P 在设备上启用或停用时广播。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION 当设备的详细信息(例如设备名称)更改时广播。

为 WLAN P2P Intent 创建广播接收器

广播接收器允许您通过 Android 系统接收 intent 广播,以便您的应用对您感兴趣的事件作出响应。创建广播接收器以处理 Wi-Fi P2P intent 的基本步骤如下:

  1. 创建一个扩展 BroadcastReceiver 类的类。对于类的构造函数,您将使用 WifiP2pManagerWifiP2pManager.Channel 以及此广播接收器将在其中注册的 activity 的参数。这样一来,广播接收器就可以向 activity 发送更新,访问 WLAN 硬件并获得通信通道(如果需要)。

  2. 在广播接收器中,在 onReceive() 方法中查看您感兴趣的 intent。根据接收到的 intent,执行任何必要操作。例如,如果广播接收器接收到 WIFI_P2P_PEERS_CHANGED_ACTION intent,则您可以调用 requestPeers() 方法,以获得当前所发现对等设备的列表。

以下代码展示了如何创建典型的广播接收器。广播接收器以 WifiP2pManager 对象和 activity 作为参数,并在接收到 intent 时,使用这两个类恰当地执行所需操作:

Kotlin

/**
* A BroadcastReceiver that notifies of important Wi-Fi p2p events.
*/
class WiFiDirectBroadcastReceiver(
       private val manager: WifiP2pManager,
       private val channel: WifiP2pManager.Channel,
       private val activity: MyWifiActivity
) : BroadcastReceiver() {

   override fun onReceive(context: Context, intent: Intent) {
       val action: String = intent.action
       when (action) {
           WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
               // Check to see if Wi-Fi is enabled and notify appropriate activity
           }
           WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
               // Call WifiP2pManager.requestPeers() to get a list of current peers
           }
           WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
               // Respond to new connection or disconnections
           }
           WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
               // Respond to this device's wifi state changing
           }
       }
   }
}

Java

/**
* A BroadcastReceiver that notifies of important Wi-Fi p2p events.
*/
public class WiFiDirectBroadcastReceiver extends BroadcastReceiver {

   private WifiP2pManager manager;
   private Channel channel;
   private MyWiFiActivity activity;

   public WiFiDirectBroadcastReceiver(WifiP2pManager manager, Channel channel,
           MyWifiActivity activity) {
       super();
       this.manager = manager;
       this.channel = channel;
       this.activity = activity;
   }

   @Override
   public void onReceive(Context context, Intent intent) {
       String action = intent.getAction();

       if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
           // Check to see if Wi-Fi is enabled and notify appropriate activity
       } else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
           // Call WifiP2pManager.requestPeers() to get a list of current peers
       } else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
           // Respond to new connection or disconnections
       } else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
           // Respond to this device's wifi state changing
       }
   }
}

在搭载 Android 10 及更高版本的设备上,以下广播 intent 是非粘性 intent:

WIFI_P2P_CONNECTION_CHANGED_ACTION
应用可以使用 requestConnectionInfo()requestNetworkInfo()requestGroupInfo() 来检索当前连接信息。
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
应用可以使用 requestDeviceInfo() 检索当前连接信息。

创建 WLAN P2P 应用

创建 WLAN P2P 应用涉及为应用创建并注册广播接收器、发现对等设备、连接到对等设备,以及将数据传输到对等设备。以下部分将介绍如何完成此操作。

初始设置

在使用 WLAN P2P API 之前,您必须确保您的应用可以访问硬件,并且设备支持 WLAN P2P API 协议。如果设备支持 WLAN P2P,您可以获取 WifiP2pManager 的实例,创建并注册广播接收器,然后开始使用 WLAN P2P API。

  1. 请求在设备上使用 WLAN 硬件的权限,并在 Android 清单中声明您的应用具有正确的最低 SDK 版本:

    <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.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- 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: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" />
    

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

  2. 检查 WLAN P2P 是否开启并受支持。您可以在广播接收器收到 WIFI_P2P_STATE_CHANGED_ACTION intent 时,在接收器中检查此项。向您的 activity 通知 WLAN P2P 的状态,并作出相应回应:

    Kotlin

    override fun onReceive(context: Context, intent: Intent) {
    ...
    val action: String = intent.action
    when (action) {
       WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
           val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)
           when (state) {
               WifiP2pManager.WIFI_P2P_STATE_ENABLED -> {
                   // Wifi P2P is enabled
               }
               else -> {
                   // Wi-Fi P2P is not enabled
               }
           }
       }
    }
    ...
    }

    Java

    @Override
    public void onReceive(Context context, Intent intent) {
    ...
    String action = intent.getAction();
    if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
       int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
       if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
           // Wifi P2P is enabled
       } else {
           // Wi-Fi P2P is not enabled
       }
    }
    ...
    }
  3. 在 activity 的 onCreate() 方法中,获取 WifiP2pManager 的实例,并通过调用 initialize() 将应用注册到 Wi-Fi P2P 框架。此方法会返回 WifiP2pManager.Channel,用于将您的应用连接到 Wi-Fi 点对点框架。此外,您还应通过 WifiP2pManagerWifiP2pManager.Channel 对象以及对 activity 的引用,创建广播接收器实例。这样广播接收器便可通知 activity 感兴趣的事件并进行相应更新。此外,您还可以操纵设备的 WLAN 状态(如有必要):

    Kotlin

    val manager: WifiP2pManager? by lazy(LazyThreadSafetyMode.NONE) {
       getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager?
    }
    
    var channel: WifiP2pManager.Channel? = null
    var receiver: BroadcastReceiver? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
       ...
    
       channel = manager?.initialize(this, mainLooper, null)
       channel?.also { channel ->
           receiver = WiFiDirectBroadcastReceiver(manager, channel, this)
       }
    }

    Java

    WifiP2pManager manager;
    Channel channel;
    BroadcastReceiver receiver;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState){
       ...
       manager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
       channel = manager.initialize(this, getMainLooper(), null);
       receiver = new WiFiDirectBroadcastReceiver(manager, channel, this);
       ...
    }
  4. 创建 intent 过滤器,然后添加与广播接收器检查内容相同的 intent:

    Kotlin

    val intentFilter = 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)
    }

    Java

    IntentFilter intentFilter;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState){
       ...
       intentFilter = new IntentFilter();
       intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
       intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
       intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
       intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
       ...
    }
  5. 在 activity 的 onResume() 方法中注册广播接收器,然后在 activity 的 onPause() 方法中取消注册该接收器:

    Kotlin

    /* register the broadcast receiver with the intent values to be matched */
    override fun onResume() {
       super.onResume()
       receiver?.also { receiver ->
           registerReceiver(receiver, intentFilter)
       }
    }
    
    /* unregister the broadcast receiver */
    override fun onPause() {
       super.onPause()
       receiver?.also { receiver ->
           unregisterReceiver(receiver)
       }
    }

    Java

    /* register the broadcast receiver with the intent values to be matched */
    @Override
    protected void onResume() {
       super.onResume();
       registerReceiver(receiver, intentFilter);
    }
    /* unregister the broadcast receiver */
    @Override
    protected void onPause() {
       super.onPause();
       unregisterReceiver(receiver);
    }
  6. 获取 WifiP2pManager.Channel 并设置广播接收器后,应用便可调用 WLAN P2P 方法并收到 WLAN P2P intent。

  7. 实现应用,然后通过调用 WifiP2pManager 中的方法,使用 Wi-Fi 点对点功能。

下一部分介绍如何执行常见操作,例如发现和连接到对等设备。

发现对等设备

调用 discoverPeers() 以检测范围内且可连接的可用对等设备。对此函数的调用为异步操作,如果您已创建 WifiP2pManager.ActionListener,则系统会通过 onSuccess()onFailure() 告知应用成功与否。onSuccess() 方法仅会通知您发现进程已成功,但不会提供有关其发现的实际对等设备(如有)的任何信息。以下代码示例展示了如何进行此设置。

Kotlin

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

   override fun onSuccess() {
       ...
   }

   override fun onFailure(reasonCode: Int) {
       ...
   }
})

Java

manager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
   @Override
   public void onSuccess() {
       ...
   }

   @Override
   public void onFailure(int reasonCode) {
       ...
   }
});

如果发现进程成功并检测到对等设备,则系统会广播 WIFI_P2P_PEERS_CHANGED_ACTION intent,您可以在广播接收器中监听该 intent,以获取对等设备列表。当应用接收到 WIFI_P2P_PEERS_CHANGED_ACTION intent 时,您可以使用 requestPeers() 请求已发现对等设备的列表。以下代码展示了如何进行此设置。

Kotlin

override fun onReceive(context: Context, intent: Intent) {
   val action: String = intent.action
   when (action) {
       ...
       WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
           manager?.requestPeers(channel) { peers: WifiP2pDeviceList? ->
               // Handle peers list
           }
       }
       ...
   }
}

Java

PeerListListener myPeerListListener;
...
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 (manager != null) {
       manager.requestPeers(channel, myPeerListListener);
   }
}

requestPeers() 方法也为异步操作,并可在对等设备列表可用时通过 onPeersAvailable()(定义见 WifiP2pManager.PeerListListener 接口)通知您的 activity。onPeersAvailable() 方法为您提供 WifiP2pDeviceList,您可对其进行迭代以查找要连接的对等设备。

连接到对等设备

获取可能对等设备的列表并选择要连接的设备后,调用 connect() 方法即可连接到相应设备。调用此方法需要使用 WifiP2pConfig 对象,其中包含要连接的设备的信息。WifiP2pManager.ActionListener 可以向您发送通知,以报告连接成功或失败。以下代码展示了如何创建与设备的连接。

Kotlin

val device: WifiP2pDevice = ...
val config = WifiP2pConfig()
config.deviceAddress = device.deviceAddress
channel?.also { channel ->
   manager?.connect(channel, config, object : WifiP2pManager.ActionListener {

       override fun onSuccess() {
           //success logic
       }

       override fun onFailure(reason: Int) {
           //failure logic
       }
   }
)}

Java

//obtain a peer from the WifiP2pDeviceList
WifiP2pDevice device;
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
manager.connect(channel, config, new ActionListener() {

   @Override
   public void onSuccess() {
       //success logic
   }

   @Override
   public void onFailure(int reason) {
       //failure logic
   }
});

转移数据

建立连接后,您可以通过套接字在设备之间传输数据。数据传输的基本步骤如下:

  1. 创建 ServerSocket。此套接字会在指定端口等待来自客户端的连接,然后加以屏蔽直到连接发生,因此请在后台线程中也执行此操作。
  2. 创建客户端 Socket。客户端使用 IP 地址和服务器套接字端口连接到服务器设备。
  3. 将数据从客户端发送到服务器。客户端套接字成功连接到服务器套接字后,您可以通过字节流将数据从客户端发送到服务器。
  4. 服务器套接字等待客户端连接(通过 accept() 方法)。在客户端连接前,此调用会屏蔽连接,因此请在另一个线程中调用此方法。发生连接时,服务器设备可接收到客户端数据。

以下示例(修改自 Wi-Fi P2P 演示)展示了如何创建此客户端-服务器套接字通信,以及如何通过服务将 JPEG 图像从客户端传输到服务器。如需完整工作示例,请编译并运行演示。

Kotlin

class FileServerAsyncTask(
       private val context: Context,
       private var statusText: TextView
) : AsyncTask<Void, Void, String?>() {

   override fun doInBackground(vararg params: Void): String? {
       /**
        * Create a server socket.
        */
       val serverSocket = ServerSocket(8888)
       return serverSocket.use {
           /**
            * Wait for client connections. This call blocks until a
            * connection is accepted from a client.
            */
           val client = serverSocket.accept()
           /**
            * If this code is reached, a client has connected and transferred data
            * Save the input stream from the client as a JPEG file
            */
           val f = File(Environment.getExternalStorageDirectory().absolutePath +
                   "/${context.packageName}/wifip2pshared-${System.currentTimeMillis()}.jpg")
           val dirs = File(f.parent)

           dirs.takeIf { it.doesNotExist() }?.apply {
               mkdirs()
           }
           f.createNewFile()
           val inputstream = client.getInputStream()
           copyFile(inputstream, FileOutputStream(f))
           serverSocket.close()
           f.absolutePath
       }
   }

   private fun File.doesNotExist(): Boolean = !exists()

   /**
    * Start activity that can handle the JPEG image
    */
   override fun onPostExecute(result: String?) {
       result?.run {
           statusText.text = "File copied - $result"
           val intent = Intent(android.content.Intent.ACTION_VIEW).apply {
               setDataAndType(Uri.parse("file://$result"), "image/*")
           }
           context.startActivity(intent)
       }
   }
}

Java

public static class FileServerAsyncTask extends AsyncTask {

   private Context context;
   private TextView statusText;

   public FileServerAsyncTask(Context context, View statusText) {
       this.context = context;
       this.statusText = (TextView) statusText;
   }

   @Override
   protected String doInBackground(Void... params) {
       try {

           /**
            * Create a server socket and wait for client connections. This
            * call blocks until a connection is accepted from a client
            */
           ServerSocket serverSocket = new ServerSocket(8888);
           Socket client = serverSocket.accept();

           /**
            * If this code is reached, a client has connected and transferred data
            * Save the input stream from the client as a JPEG file
            */
           final File f = new File(Environment.getExternalStorageDirectory() + "/"
                   + context.getPackageName() + "/wifip2pshared-" + System.currentTimeMillis()
                   + ".jpg");

           File dirs = new File(f.getParent());
           if (!dirs.exists())
               dirs.mkdirs();
           f.createNewFile();
           InputStream inputstream = client.getInputStream();
           copyFile(inputstream, new FileOutputStream(f));
           serverSocket.close();
           return f.getAbsolutePath();
       } catch (IOException e) {
           Log.e(WiFiDirectActivity.TAG, e.getMessage());
           return null;
       }
   }

   /**
    * Start activity that can handle the JPEG image
    */
   @Override
   protected void onPostExecute(String result) {
       if (result != null) {
           statusText.setText("File copied - " + result);
           Intent intent = new Intent();
           intent.setAction(android.content.Intent.ACTION_VIEW);
           intent.setDataAndType(Uri.parse("file://" + result), "image/*");
           context.startActivity(intent);
       }
   }
}

在客户端上,通过客户端套接字连接到服务器套接字,然后传输数据。本示例传输的是客户端设备文件系统中的 JPEG 文件。

Kotlin

val context = applicationContext
val host: String
val port: Int
val len: Int
val socket = Socket()
val buf = ByteArray(1024)
...
try {
   /**
    * Create a client socket with the host,
    * port, and timeout information.
    */
   socket.bind(null)
   socket.connect((InetSocketAddress(host, port)), 500)

   /**
    * Create a byte stream from a JPEG file and pipe it to the output stream
    * of the socket. This data is retrieved by the server device.
    */
   val outputStream = socket.getOutputStream()
   val cr = context.contentResolver
   val inputStream: InputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg"))
   while (inputStream.read(buf).also { len = it } != -1) {
       outputStream.write(buf, 0, len)
   }
   outputStream.close()
   inputStream.close()
} catch (e: FileNotFoundException) {
   //catch logic
} catch (e: IOException) {
   //catch logic
} finally {
   /**
    * Clean up any open sockets when done
    * transferring or if an exception occurred.
    */
   socket.takeIf { it.isConnected }?.apply {
       close()
   }
}

Java

Context context = this.getApplicationContext();
String host;
int port;
int len;
Socket socket = new Socket();
byte buf[]  = new byte[1024];
...
try {
   /**
    * Create a client socket with the host,
    * port, and timeout information.
    */
   socket.bind(null);
   socket.connect((new InetSocketAddress(host, port)), 500);

   /**
    * Create a byte stream from a JPEG file and pipe it to the output stream
    * of the socket. This data is retrieved by the server device.
    */
   OutputStream outputStream = socket.getOutputStream();
   ContentResolver cr = context.getContentResolver();
   InputStream inputStream = null;
   inputStream = cr.openInputStream(Uri.parse("path/to/picture.jpg"));
   while ((len = inputStream.read(buf)) != -1) {
       outputStream.write(buf, 0, len);
   }
   outputStream.close();
   inputStream.close();
} catch (FileNotFoundException e) {
   //catch logic
} catch (IOException e) {
   //catch logic
}

/**
* Clean up any open sockets when done
* transferring or if an exception occurred.
*/
finally {
   if (socket != null) {
       if (socket.isConnected()) {
           try {
               socket.close();
           } catch (IOException e) {
               //catch logic
           }
       }
   }
}