WLAN 直连 (P2P) 允许具有相应硬件的设备通过 WLAN 直接相互连接,而无需使用中间接入点。使用这些 API,您可以在每个设备都支持 Wi-Fi P2P 时发现和连接到其他设备,然后通过高速连接进行通信,其通信距离比蓝牙连接长得多。这对于在用户之间共享数据的应用(例如多人游戏或照片共享应用)非常有用。
Wi-Fi P2P API 包含以下主要部分:
- 可让您发现、请求和连接到对等设备的方法,这些方法在
WifiP2pManager类中定义。 - 监听器,用于在
WifiP2pManager方法调用成功或失败时通知您。调用WifiP2pManager方法时,每个方法都可以接收作为参数传入的特定监听器。 - 用于在 Wi-Fi P2P 框架检测到特定事件(例如连接中断或新发现的对等设备)时通知您的 intent。
您通常会同时使用 API 的这三个主要组件。例如,您可以向 discoverPeers() 调用提供 WifiP2pManager.ActionListener,以便 ActionListener.onSuccess() 和 ActionListener.onFailure() 方法可以通知您。如果 discoverPeers() 方法发现对等设备列表已发生变化,系统还会广播 WIFI_P2P_PEERS_CHANGED_ACTION intent。
API 概览
WifiP2pManager 类提供了一些方法,可让您与设备上的 WLAN 硬件进行交互,从而执行发现和连接到对等设备等操作。可执行的操作如下:
表 1. WLAN 点对点方法
| 方法 | 说明 |
initialize()
|
向 Wi-Fi 框架注册应用。请在调用任何其他 WLAN P2P 方法之前调用此方法。 |
connect()
|
与具有指定配置的设备建立点对点连接。 |
cancelConnect()
|
取消任何正在进行的点对点群组协商。 |
requestConnectInfo()
|
请求设备的连接信息。 |
createGroup()
|
创建一个点对点群组,并将当前设备设为群组所有者。 |
removeGroup()
|
移除当前对等互连组。 |
requestGroupInfo()
|
请求对等互连组信息。 |
discoverPeers()
|
启动对等发现。 |
requestPeers()
|
请求当前发现的对等设备列表。 |
WifiP2pManager 方法可让您传入监听器,以便 Wi-Fi 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()
|
Wi-Fi P2P API 定义了在发生特定 Wi-Fi P2P 事件(例如发现新对等设备或设备的 Wi-Fi 状态发生变化)时广播的 intent。您可以通过创建广播接收器来处理这些 intent,从而在应用中注册以接收这些 intent:
表 3. WLAN P2P intent
| Intent | 说明 |
WIFI_P2P_CONNECTION_CHANGED_ACTION
|
在设备的 Wi-Fi 连接状态发生变化时广播。 |
WIFI_P2P_PEERS_CHANGED_ACTION
|
在您调用 discoverPeers() 时广播。如果您在应用中处理此 intent,通常会调用 requestPeers() 来获取更新后的对等设备列表。
|
WIFI_P2P_STATE_CHANGED_ACTION
|
在设备上启用或停用 Wi-Fi P2P 时广播。 |
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
|
在设备的详细信息(例如设备名称)发生更改时广播。 |
为 WLAN 点对点 intent 创建广播接收器
借助广播接收器,您可以接收 Android 系统广播的 intent,以便您的应用可以响应您感兴趣的事件。创建广播接收器以处理 WLAN P2P intent 的基本步骤如下:
创建一个扩展
BroadcastReceiver类的类。对于该类的构造函数,您将使用WifiP2pManager、WifiP2pManager.Channel以及将注册此广播接收器的 activity 的参数。这样,广播接收器就可以向 activity 发送更新,并根据需要访问 Wi-Fi 硬件和通信通道。在广播接收器中,在
onReceive()方法中检查您感兴趣的 intent。根据收到的 intent 执行任何必要的操作。例如,如果广播接收器收到WIFI_P2P_PEERS_CHANGED_ACTIONintent,您可以调用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 Wi-Fi 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 Wi-Fi state changing
}
}
}
在搭载 Android 10 及更高版本的设备上,以下广播 intent 是非粘性的:
WIFI_P2P_CONNECTION_CHANGED_ACTION- 应用可以使用
requestConnectionInfo()、requestNetworkInfo()或requestGroupInfo()检索当前连接信息。 WIFI_P2P_THIS_DEVICE_CHANGED_ACTION- 应用可以使用
requestDeviceInfo()检索当前连接信息。
创建 WLAN P2P 应用
创建 Wi-Fi P2P 应用涉及为应用创建和注册广播接收器、发现对等设备、连接到对等设备以及向对等设备传输数据。以下各部分介绍了如何执行此操作。
初始设置
在使用 Wi-Fi P2P API 之前,您必须确保应用可以访问硬件,并且设备支持 Wi-Fi P2P 协议。如果支持 Wi-Fi 点对点,您可以获取 WifiP2pManager 的实例,创建并注册广播接收器,然后开始使用 Wi-Fi 点对点 API。
请求使用设备上的 Wi-Fi 硬件的权限,并在 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 还需要启用位置信息模式:
检查 WLAN P2P 是否已开启且受支持。检查此项的一个好位置是在广播接收器收到
WIFI_P2P_STATE_CHANGED_ACTIONintent 时。向您的 activity 通知 Wi-Fi 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 -> { // Wi-Fi 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) { // Wi-Fi P2P is enabled } else { // Wi-Fi P2P is not enabled } } ... }在 activity 的
onCreate()方法中,获取WifiP2pManager的实例,并通过调用initialize()向 Wi-Fi P2P 框架注册您的应用。此方法会返回一个WifiP2pManager.Channel,用于将您的应用连接到 Wi-Fi P2P 框架。您还应使用WifiP2pManager和WifiP2pManager.Channel对象以及对 activity 的引用来创建广播接收器的实例。通过此配置,您的广播接收器可以向 activity 通知感兴趣的事件,并相应地更新 activity。它还允许您在必要时操纵设备的 Wi-Fi 状态: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); ... }创建一个 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); ... }在 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); }获取
WifiP2pManager.Channel并设置广播接收器后,您的应用便可以进行 Wi-Fi P2P 方法调用并接收 Wi-Fi P2P intent。通过调用
WifiP2pManager中的方法,使用 Wi-Fi P2P 功能实现您的应用。
以下各部分介绍了如何执行常见操作,例如发现和连接到对等设备。
发现对等节点
调用 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 Wi-Fi 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
}
});
转移数据
建立连接后,您可以使用套接字在设备之间传输数据。转移数据的基本步骤如下:
- 创建
ServerSocket。此套接字会等待客户端在指定端口上进行连接,并会一直阻塞,直到连接发生,因此请在后台线程中执行此操作。 - 创建客户端
Socket。客户端使用服务器端套接字的 IP 地址和端口连接到服务器设备。 - 将数据从客户端发送到服务器。当客户端套接字成功连接到服务器端套接字后,您可以使用字节流将数据从客户端发送到服务器。
- 服务器端套接字等待客户端连接(使用
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
}
}
}
}