Android NDK开发详解连接性之将 WLAN 直连(点对点)用于服务发现
本课程的第一课“使用网络服务发现”向您介绍了如何发现连接到本地网络的服务。然而,使用 WLAN 点对点 (P2P) 服务发现可让您在未连接到网络的情况下直接发现附近设备的服务。您还可以广播您的设备上运行的服务。使用这些功能,即使没有可用的本地网络或热点,也可以在应用之间进行通信。
虽然这组 API 在用途方面与上一课程中介绍的网络服务发现 API 相似,但在代码中实现它们的方式却截然不同。本课程向您介绍了如何使用 WLAN 点对点发现其他设备提供的服务,并假定您已熟悉 WLAN 点对点 API。
设置清单
如需使用 WLAN 点对点,请在清单中添加 CHANGE_WIFI_STATE、ACCESS_WIFI_STATE、ACCESS_FINE_LOCATION 和 INTERNET 权限。尽管 WLAN 点对点不需要互联网连接,但它使用标准 Java 套接字,而在 Android 中使用这些套接字需要获得所请求的权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.nsdchat"
...
<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.ACCESS_FINE_LOCATION"/>
<uses-permission
android:required="true"
android:name="android.permission.INTERNET"/>
...
除了上面的权限外,以下 API 还需要启用位置信息模式:
discoverPeers
discoverServices
requestPeers
添加本地服务
如果您要提供本地服务,则需要注册它以支持服务发现。注册本地服务后,框架会自动响应来自对等设备的服务发现请求。
如需创建本地服务,请执行以下步骤:
创建一个 WifiP2pServiceInfo 对象。
用您的服务相关信息填充该对象。
调用 addLocalService() 来注册本地服务以支持服务发现。
Kotlin
private fun startRegistration() {
// Create a string map containing information about your service.
val record: Map<String, String> = mapOf(
"listenport" to SERVER_PORT.toString(),
"buddyname" to "John Doe${(Math.random() * 1000).toInt()}",
"available" to "visible"
)
// Service information. Pass it an instance name, service type
// _protocol._transportlayer , and the map containing
// information other devices will want once they connect to this one.
val serviceInfo =
WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record)
// Add the local service, sending the service info, network channel,
// and listener that will be used to indicate success or failure of
// the request.
manager.addLocalService(channel, serviceInfo, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
// Command successful! Code isn't necessarily needed here,
// Unless you want to update the UI or add logging statements.
}
override fun onFailure(arg0: Int) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
})
}
Java
private void startRegistration() {
// Create a string map containing information about your service.
Map record = new HashMap();
record.put("listenport", String.valueOf(SERVER_PORT));
record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
record.put("available", "visible");
// Service information. Pass it an instance name, service type
// _protocol._transportlayer , and the map containing
// information other devices will want once they connect to this one.
WifiP2pDnsSdServiceInfo serviceInfo =
WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);
// Add the local service, sending the service info, network channel,
// and listener that will be used to indicate success or failure of
// the request.
manager.addLocalService(channel, serviceInfo, new ActionListener() {
@Override
public void onSuccess() {
// Command successful! Code isn't necessarily needed here,
// Unless you want to update the UI or add logging statements.
}
@Override
public void onFailure(int arg0) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
}
发现附近的服务
Android 使用回调方法向应用通知可用服务,因此首先要设置这些方法。创建 WifiP2pManager.DnsSdTxtRecordListener 以监听传入的记录。可以选择由其他设备对此记录进行广播。当有记录传入时,将您所需的设备地址和任何其他相关信息复制到当前方法外部的数据结构中,以便稍后访问。以下示例假定记录包含一个填充了用户身份的“buddyname”字段。
Kotlin
private val buddies = mutableMapOf<String, String>()
...
private fun discoverService() {
/* Callback includes:
* fullDomain: full domain name: e.g "printer._ipp._tcp.local."
* record: TXT record dta as a map of key/value pairs.
* device: The device running the advertised service.
*/
val txtListener = DnsSdTxtRecordListener { fullDomain, record, device ->
Log.d(TAG, "DnsSdTxtRecord available -$record")
record["buddyname"]?.also {
buddies[device.deviceAddress] = it
}
}
}
Java
final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
@Override
/* Callback includes:
* fullDomain: full domain name: e.g "printer._ipp._tcp.local."
* record: TXT record dta as a map of key/value pairs.
* device: The device running the advertised service.
*/
public void onDnsSdTxtRecordAvailable(
String fullDomain, Map record, WifiP2pDevice device) {
Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
buddies.put(device.deviceAddress, record.get("buddyname"));
}
};
}
如需获取服务信息,请创建一个 WifiP2pManager.DnsSdServiceResponseListener。该接口将接收实际的说明和连接信息。之前的代码段实现了一个 Map 对象,用于将设备地址与好友名称配对。服务响应监听器使用此对象将 DNS 记录与相应的服务信息关联起来。两个监听器都实现后,使用 setDnsSdResponseListeners() 方法将它们添加到 WifiP2pManager 中。
Kotlin
private fun discoverService() {
...
val servListener = DnsSdServiceResponseListener { instanceName, registrationType, resourceType ->
// Update the device name with the human-friendly version from
// the DnsTxtRecord, assuming one arrived.
resourceType.deviceName = buddies[resourceType.deviceAddress] ?: resourceType.deviceName
// Add to the custom adapter defined specifically for showing
// wifi devices.
val fragment = fragmentManager
.findFragmentById(R.id.frag_peerlist) as WiFiDirectServicesList
(fragment.listAdapter as WiFiDevicesAdapter).apply {
add(resourceType)
notifyDataSetChanged()
}
Log.d(TAG, "onBonjourServiceAvailable $instanceName")
}
manager.setDnsSdResponseListeners(channel, servListener, txtListener)
...
}
Java
private void discoverService() {
...
DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
@Override
public void onDnsSdServiceAvailable(String instanceName, String registrationType,
WifiP2pDevice resourceType) {
// Update the device name with the human-friendly version from
// the DnsTxtRecord, assuming one arrived.
resourceType.deviceName = buddies
.containsKey(resourceType.deviceAddress) ? buddies
.get(resourceType.deviceAddress) : resourceType.deviceName;
// Add to the custom adapter defined specifically for showing
// wifi devices.
WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
.findFragmentById(R.id.frag_peerlist);
WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
.getListAdapter());
adapter.add(resourceType);
adapter.notifyDataSetChanged();
Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
}
};
manager.setDnsSdResponseListeners(channel, servListener, txtListener);
...
}
现在,创建一个服务请求并调用 addServiceRequest()。此方法还会采用监听器报告成功或失败。
Kotlin
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance()
manager.addServiceRequest(
channel,
serviceRequest,
object : WifiP2pManager.ActionListener {
override fun onSuccess() {
// Success!
}
override fun onFailure(code: Int) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
}
)
Java
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
manager.addServiceRequest(channel,
serviceRequest,
new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
最后,调用 discoverServices()。
Kotlin
manager.discoverServices(
channel,
object : WifiP2pManager.ActionListener {
override fun onSuccess() {
// Success!
}
override fun onFailure(code: Int) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
when (code) {
WifiP2pManager.P2P_UNSUPPORTED -> {
Log.d(TAG, "P2P isn't supported on this device.")
}
}
}
}
)
Java
manager.discoverServices(channel, new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
if (code == WifiP2pManager.P2P_UNSUPPORTED) {
Log.d(TAG, "P2P isn't supported on this device.");
else if(...)
...
}
});
如果一切进展顺利,您就大功告成了!如果遇到问题,请记住,您所进行的异步调用采用 WifiP2pManager.ActionListener 作为参数,这为您提供了指示成功或失败的回调。如需诊断问题,请在 onFailure() 中加入调试代码。该方法提供的错误代码会提示问题。以下是可能的错误值及其含义
P2P_UNSUPPORTED
运行该应用的设备不支持 WLAN 点对点。
BUSY
系统太忙,无法处理请求。
ERROR
由于出现内部错误,操作失败。
WLAN Easy Connect
在 Android 10 (API 级别 29) 或更高版本 设备上,您可以使用 Easy Connect 为对等设备预配 WLAN 凭据,替代已在 Android 9 中弃用的 WPS。应用可使用 ACTION_PROCESS_WIFI_EASY_CONNECT_URI Intent,将 Easy Connect 集成到其设置和预配流程中。此 Intent 需要 URI。通话应用可通过多种方法检索 URI,包括扫描贴纸或显示器上的二维码,或者扫描蓝牙 LE 或 NFC 广告。
URI 可用后,您即可使用 ACTION_PROCESS_WIFI_EASY_CONNECT_URI Intent 来预配对等设备的 WLAN 凭据。这样用户便可选择 WLAN 网络以共享和安全传输凭据。
Easy Connect 无需位置或 WLAN 权限。
注意:在使用此 Intent 前,应用必须调用 WifiManager.isEasyConnectSupported()),以验证设备是否支持 Easy Connect。
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2020-06-11。