Android NDK开发详解连接性之使用网络服务[NSD]发现
无线连接设备
Android 的无线 API 除了支持与云通信,还支持与同一本地网络上的其他设备通信,甚至包括不在网络上但物理上紧邻的设备。添加网络服务发现 (NSD) 进一步增强了这一功能,它允许应用查找附近运行服务的设备,以便与之通信。将此功能集成到您的应用中,可帮助您提供广泛的功能,比如和在同一房间中的用户一起玩游戏,从支持 NSD 的网络摄像头中提取图片,或远程登录到同一网络中的其他计算机。
本课介绍了用于从您的应用查找和连接其他设备的关键 API。具体来说,介绍了用于发现可用服务的 NSD API 以及用于进行点对点无线连接的 WLAN 点对点 (P2P) API。本课程还介绍了如何结合使用 NSD 和 WLAN 点对点,在没有网络连接的情况下,检测设备提供的服务并连接到设备。
如果您正在为 Android 应用寻找更高级别的 API,以便结合使用 WLAN 和蓝牙在设备之间安全可靠地传输数据,那么可以考虑使用 Nearby Connections API。
课程
使用网络服务发现
了解如何广播您的应用提供的服务,发现本地网络上提供的服务,以及使用 NSD 确定您要连接到的服务的连接细节。
通过 WLAN 建立点对点连接
了解如何获取附近对等设备的列表,为旧版设备创建接入点,以及连接到其他支持 WLAN 点对点连接的设备。
将 WLAN 点对点用于服务发现
了解如何利用 WLAN 点对点来发现不在同一网络上的邻近设备所发布的服务。
另请阅读
WLAN 点对点
Nearby Connections API
使用网络服务发现
网络服务发现 (NSD) 可让您的应用访问其他设备在本地网络上提供的服务。支持 NSD 的设备包括打印机、网络摄像头、HTTPS 服务器以及其他移动设备。
NSD 实现了基于 DNS 的服务发现 (DNS-SD) 机制,该机制允许您的应用通过指定服务类型和提供所需类型服务的设备实例的名称来请求服务。Android 和其他移动平台均支持 DNS-SD。
将 NSD 添加到应用中,可让您的用户识别本地网络上是否有其他设备支持您的应用所请求的服务。这对于各种点对点应用非常有用,例如文件共享或多人游戏。Android 的 NSD API 简化了实现此类功能所需的工作。
本节课向您介绍如何构建应用,使其可向本地网络广播其名称和连接信息并扫描功能相同的其他应用中的信息。最后,本节课将介绍如何连接到在其他设备上运行的相同应用。
在网络上注册您的服务
注意:此步骤是可选的。如果您不想在本地网络上广播应用的服务,可以直接跳至下一部分发现网络上的服务。
如需在本地网络上注册服务,请先创建一个 NsdServiceInfo 对象。此对象提供网络上其他设备在决定是否连接到您的服务时使用的信息。
Kotlin
fun registerService(port: Int) {
// Create the NsdServiceInfo object, and populate it.
val serviceInfo = NsdServiceInfo().apply {
// The name is subject to change based on conflicts
// with other services advertised on the same network.
serviceName = "NsdChat"
serviceType = "_nsdchat._tcp"
setPort(port)
...
}
}
Java
public void registerService(int port) {
// Create the NsdServiceInfo object, and populate it.
NsdServiceInfo serviceInfo = new NsdServiceInfo();
// The name is subject to change based on conflicts
// with other services advertised on the same network.
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_nsdchat._tcp");
serviceInfo.setPort(port);
...
}
此代码段将服务名称设置为“NsdChat”。此服务名称是实例名称:它是对网络上其他设备可见的名称。网络上使用 NSD 查找本地服务的任何设备都可以看到该名称。请记住,网络上任何服务的名称都必须是唯一的,并且 Android 会自动处理冲突解决。如果网络中的两台设备都安装了 NsdChat 应用,则其中一台设备会自动更改服务名称,如更改为“NsdChat (1)”之类的。
第二个参数会设置服务类型,指定应用使用的协议和传输层。语法为“.”。在代码段中,该服务使用通过 TCP 运行的 HTTP 协议。提供打印机服务(例如网络打印机)的应用会将服务类型设置为“_ipp._tcp”。
注意:国际编号分配机构 (IANA) 负责管理服务发现协议(如 NSD 和 Bonjour)所使用的服务类型的集中式权威列表。您可以从 IANA 服务名称和端口号列表中下载该列表。如果您打算使用新的服务类型,则应填写 IANA 端口和服务注册表预订该服务。
为服务设置端口时,请避免对其进行硬编码,因为这会与其他应用冲突。例如,假设您的应用始终使用端口 1337,这可能会与使用相同端口的其他已安装应用发生冲突。请改用设备的下一个可用端口。由于此信息通过服务广播提供给其他应用,因此在编译时无需让其他应用知道您的应用所使用的端口。应用可以在即将连接到您的服务之前通过您的服务广播获取此信息。
如果您使用套接字,只需将套接字设置为 0 即可将套接字初始化为任何可用端口,如下所示。
Kotlin
fun initializeServerSocket() {
// Initialize a server socket on the next available port.
serverSocket = ServerSocket(0).also { socket ->
// Store the chosen port.
mLocalPort = socket.localPort
...
}
}
Java
public void initializeServerSocket() {
// Initialize a server socket on the next available port.
serverSocket = new ServerSocket(0);
// Store the chosen port.
localPort = serverSocket.getLocalPort();
...
}
您已定义 NsdServiceInfo 对象,现在需要实现 RegistrationListener 接口。此接口包含 Android 用于向您的应用提醒服务注册和取消注册是否成功的回调。
Kotlin
private val registrationListener = object : NsdManager.RegistrationListener {
override fun onServiceRegistered(NsdServiceInfo: NsdServiceInfo) {
// Save the service name. Android may have changed it in order to
// resolve a conflict, so update the name you initially requested
// with the name Android actually used.
mServiceName = NsdServiceInfo.serviceName
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Registration failed! Put debugging code here to determine why.
}
override fun onServiceUnregistered(arg0: NsdServiceInfo) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Unregistration failed. Put debugging code here to determine why.
}
}
Java
public void initializeRegistrationListener() {
registrationListener = new NsdManager.RegistrationListener() {
@Override
public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
// Save the service name. Android may have changed it in order to
// resolve a conflict, so update the name you initially requested
// with the name Android actually used.
serviceName = NsdServiceInfo.getServiceName();
}
@Override
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Registration failed! Put debugging code here to determine why.
}
@Override
public void onServiceUnregistered(NsdServiceInfo arg0) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
}
@Override
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Unregistration failed. Put debugging code here to determine why.
}
};
}
现在,您已具备注册服务的所有条件。调用 registerService() 方法。
请注意,此方法是异步的,因此任何需要在服务注册后运行的代码都必须包含在 onServiceRegistered() 方法中。
Kotlin
fun registerService(port: Int) {
// Create the NsdServiceInfo object, and populate it.
val serviceInfo = NsdServiceInfo().apply {
// The name is subject to change based on conflicts
// with other services advertised on the same network.
serviceName = "NsdChat"
serviceType = "_nsdchat._tcp"
setPort(port)
}
nsdManager = (getSystemService(Context.NSD_SERVICE) as NsdManager).apply {
registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
}
Java
public void registerService(int port) {
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_http._tcp.");
serviceInfo.setPort(port);
nsdManager = Context.getSystemService(Context.NSD_SERVICE);
nsdManager.registerService(
serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
}
发现网络上的服务
网络充满生机,既有凶猛的网络打印机,也有温顺的网络摄像头,还有附近井字棋游戏玩家残酷而激烈的战斗。服务发现是让应用看到这个充满活力的功能生态系统的关键所在。您的应用需要监听网络上的服务广播以查看可用的服务,并过滤掉应用无法使用的任何服务。
与服务注册一样,服务发现包括两个步骤:设置具有相关回调的发现监听器,以及对 discoverServices() 进行单个异步 API 调用。
首先,实例化一个实现 NsdManager.DiscoveryListener 的匿名类。以下代码段展示了一个简单的示例:
Kotlin
// Instantiate a new DiscoveryListener
private val discoveryListener = object : NsdManager.DiscoveryListener {
// Called as soon as service discovery begins.
override fun onDiscoveryStarted(regType: String) {
Log.d(TAG, "Service discovery started")
}
override fun onServiceFound(service: NsdServiceInfo) {
// A service was found! Do something with it.
Log.d(TAG, "Service discovery success$service")
when {
service.serviceType != SERVICE_TYPE -> // Service type is the string containing the protocol and
// transport layer for this service.
Log.d(TAG, "Unknown Service Type: ${service.serviceType}")
service.serviceName == mServiceName -> // The name of the service tells the user what they'd be
// connecting to. It could be "Bob's Chat App".
Log.d(TAG, "Same machine: $mServiceName")
service.serviceName.contains("NsdChat") -> nsdManager.resolveService(service, resolveListener)
}
}
override fun onServiceLost(service: NsdServiceInfo) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "service lost: $service")
}
override fun onDiscoveryStopped(serviceType: String) {
Log.i(TAG, "Discovery stopped: $serviceType")
}
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "Discovery failed: Error code:$errorCode")
nsdManager.stopServiceDiscovery(this)
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "Discovery failed: Error code:$errorCode")
nsdManager.stopServiceDiscovery(this)
}
}
Java
public void initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
discoveryListener = new NsdManager.DiscoveryListener() {
// Called as soon as service discovery begins.
@Override
public void onDiscoveryStarted(String regType) {
Log.d(TAG, "Service discovery started");
}
@Override
public void onServiceFound(NsdServiceInfo service) {
// A service was found! Do something with it.
Log.d(TAG, "Service discovery success" + service);
if (!service.getServiceType().equals(SERVICE_TYPE)) {
// Service type is the string containing the protocol and
// transport layer for this service.
Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
} else if (service.getServiceName().equals(serviceName)) {
// The name of the service tells the user what they'd be
// connecting to. It could be "Bob's Chat App".
Log.d(TAG, "Same machine: " + serviceName);
} else if (service.getServiceName().contains("NsdChat")){
nsdManager.resolveService(service, resolveListener);
}
}
@Override
public void onServiceLost(NsdServiceInfo service) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "service lost: " + service);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "Discovery stopped: " + serviceType);
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
nsdManager.stopServiceDiscovery(this);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
nsdManager.stopServiceDiscovery(this);
}
};
}
NSD API 使用此接口中的方法在发现启动、发现失败以及服务被发现和丢失时(丢失意味着“不再可用”)通知您的应用。请注意,此代码段会在发现服务时进行多次检查。
将发现的服务的服务名称与本地服务的服务名称进行比较,以确定设备是否只选择了自己的广播(有效广播)。
检查服务类型,以确认它是应用可以连接的服务类型。
检查服务名称,以确认是否与正确的应用连接。
检查服务名称并不总是必要的,并且仅在需要连接到特定应用时才有意义。例如,应用可能只希望连接到在其他设备上运行的其自身的实例。但是,如果应用要连接到网络打印机,则看到服务类型为“_ipp._tcp”就已足够。
设置监听器后,调用 discoverServices(),传入应用应查找的服务类型、要使用的发现协议以及您刚创建的监听器。
Kotlin
nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener)
Java
nsdManager.discoverServices(
SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
连接到网络上的服务
当应用在网络上找到要连接的服务时,它必须首先使用 resolveService() 方法确定该服务的连接信息。实现 NsdManager.ResolveListener 以传入此方法,并使用它获取包含连接信息的 NsdServiceInfo。
Kotlin
private val resolveListener = object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Called when the resolve fails. Use the error code to debug.
Log.e(TAG, "Resolve failed: $errorCode")
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
Log.e(TAG, "Resolve Succeeded. $serviceInfo")
if (serviceInfo.serviceName == mServiceName) {
Log.d(TAG, "Same IP.")
return
}
mService = serviceInfo
val port: Int = serviceInfo.port
val host: InetAddress = serviceInfo.host
}
}
Java
public void initializeResolveListener() {
resolveListener = new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Called when the resolve fails. Use the error code to debug.
Log.e(TAG, "Resolve failed: " + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.e(TAG, "Resolve Succeeded. " + serviceInfo);
if (serviceInfo.getServiceName().equals(serviceName)) {
Log.d(TAG, "Same IP.");
return;
}
mService = serviceInfo;
int port = mService.getPort();
InetAddress host = mService.getHost();
}
};
}
解析服务后,您的应用会收到详细的服务信息,包括 IP 地址和端口号。这是您建立与该服务的网络连接所需的所有信息。
在应用关闭时取消注册您的服务
在应用的生命周期中,酌情启用和停用 NSD 功能非常重要。当应用关闭时将其取消注册,有助于防止其他应用认为它仍处于活动状态并尝试连接到该应用。此外,服务发现是一项开销很大的操作,应在父级 Activity 暂停时停止,并在 Activity 恢复后重新启用。替换主 Activity 的生命周期方法,并插入代码以酌情启动和停止服务广播及发现。
Kotlin
//In your application's Activity
override fun onPause() {
nsdHelper?.tearDown()
super.onPause()
}
override fun onResume() {
super.onResume()
nsdHelper?.apply {
registerService(connection.localPort)
discoverServices()
}
}
override fun onDestroy() {
nsdHelper?.tearDown()
connection.tearDown()
super.onDestroy()
}
// NsdHelper's tearDown method
fun tearDown() {
nsdManager.apply {
unregisterService(registrationListener)
stopServiceDiscovery(discoveryListener)
}
}
Java
//In your application's Activity
@Override
protected void onPause() {
if (nsdHelper != null) {
nsdHelper.tearDown();
}
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
if (nsdHelper != null) {
nsdHelper.registerService(connection.getLocalPort());
nsdHelper.discoverServices();
}
}
@Override
protected void onDestroy() {
nsdHelper.tearDown();
connection.tearDown();
super.onDestroy();
}
// NsdHelper's tearDown method
public void tearDown() {
nsdManager.unregisterService(registrationListener);
nsdManager.stopServiceDiscovery(discoveryListener);
}
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2020-06-26。