把网络服务探索NSD(Network Service Discovery)添加到开发者的app中可以让用户识别局域网上支持你的app所请求服务的设备。对于很多点对点peer-to-peer应用这是非常有用的,例如文件分享或是多玩家游戏。Android的NSD API简化了开发者实现上述功能特性的工作。
本节课讲解如何构建一个app应用,实现把应用名称和连网信息广博到局域网的功能,并且可以扫描来自于其他应用的类似信息。最后,本节课讲解app如何连接到另一设备的同一app。
在网络上注册服务
注意:此步骤是可选项。T如果开发者不在乎将其app的服务广播到局域网之外,那么可以跳过该步骤到下一章节。
为了将服务注册到局域网,首先创建一个NsdServiceInfo对象。当局域网上其他设备决定是否连接到该服务时它们会用到该对象所提供的信息。
public void registerService(int port) { // 创建NsdServiceInfo 对象, 添加相应信息. NsdServiceInfo serviceInfo = new NsdServiceInfo(); // 当所注册服务与同一网络其他服务冲突时会改变这里的服务名称 serviceInfo.setServiceName("NsdChat"); serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(port); .... }
上面代码片段设定了一个名为NsdChat的服务。对于局域网内用NSD查找本地服务的所有设备,这个名字都是可见的。在同一局域网上必须保证该服务名称是唯一的,Android会自动的处理名字冲突。如果局域网上两台设备都有安装NsdChat应用,那么其中的一个会自动把服务名称设置为NsdChat(1)。
第二个参数设置服务类型,指定app使用哪个协议和传输层协议。参数语法为"_<protocol>._<transportlayer>". 在这段代码片段中,该服务在TCP层上使用HTTP协议。一个提供打印服务的应用应该设置服务类型为"_ipp._tcp".
注意: 互联网号码分配局 (IANA) 管理着一个集中的权威的服务类型清单,可用于像NSD和Bonjour这类服务发现协议。你可以去下载该清单,地址:the IANA list of service names and port numbers. 如果你的目的在于使用一个新的service type,那么需要向IANA提交表单申请:IANA Ports and Service registration form.
当设置服务的端口时,避免对端口的硬编码,因为其有可能与其他应用冲突。假如,你的app一直使用端口1337,与同用1337端口的其他应用就存在潜在的冲突。因此,取所在设备的下一个可用端口。由于信息是通过服务广播的形式传给其他app的,因此其他的app不需要在你的app的编译阶段就确定了你的app所用的端口。因此,app能够在连接到你的服务之前就通过你的服务广播获取信息。
如果你用socket进行通信,那么下面是如何初始化一个socket到任意可用的端口,做法就是设置端口为0。
public void initializeServerSocket() { // 初始化一个server socket到下一个可用端口 mServerSocket = new ServerSocket(0); // 保存所选择的端口 mLocalPort = mServerSocket.getLocalPort(); ... }
既然已经定义了NsdServiceInfo对象,那么就需要实现RegistrationListener接口。该接口包含了一个回调方法,由Android来通知你的app是否成功的完成服务注册或移除注册。
public void initializeRegistrationListener() { mRegistrationListener = new NsdManager.RegistrationListener() { @Override public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // 保存服务名称。Android有可能会改变该服务名称,因为潜在的名称冲突,所以要用Androi实际分配的服务名称去更新一下最开始请求的服务名称。 mServiceName = NsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { // 注册失败!可以在这里写调试代码来确定为何失败 } @Override public void onServiceUnregistered(NsdServiceInfo arg0) { // 服务已经被移除注册。这仅发生在调用NsdManager.unregisterService()时 } @Override public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { // 移除注册失败。可以在这里写调试代码以确定为何失败。 } }; }
现在都已经准备好了各个代码片段,下面调用方法registerService()
.
注意该方法是异步方法,所以任何需要在服务注册完毕后执行的代码,都应该放到 onServiceRegistered()
方法中。
public void registerService(int port) { NsdServiceInfo serviceInfo = new NsdServiceInfo(); serviceInfo.setServiceName("NsdChat"); serviceInfo.setServiceType("_http._tcp."); serviceInfo.setPort(port); mNsdManager = Context.getSystemService(Context.NSD_SERVICE); mNsdManager.registerService( serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener); }
搜索网络上的服务
网络上有各种有趣的服务。 让你的app能够看到网络上各种服务功能的即是服务探索。你的app需要监听网络上的服务广播以查看什么服务是可用的,过滤掉你的app不能使用的服务。
服务探索Service discovery,类似于服务注册,有两个步骤:设置一个discovery 监听器中的相关回调方法,执行一个异步的API调用discoverService()。
首先实例化一个匿名的类,这个类实现接口NsdManager.DiscoveryListener。下面代码片段给出了一个简单的例子:
public void initializeDiscoveryListener() { // 实例化一个新的DiscoveryListener mDiscoveryListener = new NsdManager.DiscoveryListener() { // 在服务探索开始时被调用 @Override public void onDiscoveryStarted(String regType) { Log.d(TAG, "Service discovery started"); } @Override public void onServiceFound(NsdServiceInfo service) { // 找到了一个服务!做某些处理 Log.d(TAG, "Service discovery success" + service); if (!service.getServiceType().equals(SERVICE_TYPE)) { // Service type 即一个包含了应用层协议和运输层协议的字符串 Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(mServiceName)) { // 服务名称 Log.d(TAG, "Same machine: " + mServiceName); } else if (service.getServiceName().contains("NsdChat")){ mNsdManager.resolveService(service, mResolveListener); } } @Override public void onServiceLost(NsdServiceInfo service) { // 在网络服务不再可用时被调用 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); mNsdManager.stopServiceDiscovery(this); } @Override public void onStopDiscoveryFailed(String serviceType, int errorCode) { Log.e(TAG, "Discovery failed: Error code:" + errorCode); mNsdManager.stopServiceDiscovery(this); } }; }
NSD API使用该接口内的方法在以下几个情况下通知你的app:当探索开始,当探索失败,当服务找到,当服务不可用。注意本段代码在找到服务后会做几个检查:
- 对比探索到服务名称与本机服务的名称,确定设备是不是探索到了设备自身的服务(这是不合法的)。
- 检查服务类型,检查服务类型是否能被你的app所连接到。The service type is checked, to verify it's a type of service your application can connect to.
- 检查服务名称以确定连接到正确的应用上。
检查服务名称并非总是需要,如果想连接到特定的服务,那么该检查是重要的。例如,想让app仅仅连接到其本身在另一个设备上的实例。如果app想连接到网络打印机,那么查看服务类型为“_ipp._tcp"就可以了。
在建立了监听器以后,调用discoverServices()
, 传递你的app欲查找的服务类型,所用的探索协议,以及刚刚创建的监听器。
mNsdManager.discoverServices( SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
连接到网络上的服务
当app在网络上发现了可以连接的服务时,首先应判定该服务的连接信息,这可以用 resolveService()
方法来做到。实现一个NsdManager.ResolveListener
侦听器然后将其传参给resolveService()方法,返回一个NsdServiceInfo对象,其中包含连接信息。
public void initializeResolveListener() { mResolveListener = new NsdManager.ResolveListener() { @Override public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { // 失败时调用,errorCode用于调试 Log.e(TAG, "Resolve failed" + errorCode); } @Override public void onServiceResolved(NsdServiceInfo serviceInfo) { Log.e(TAG, "Resolve Succeeded. " + serviceInfo); if (serviceInfo.getServiceName().equals(mServiceName)) { Log.d(TAG, "Same IP."); return; } mService = serviceInfo; int port = mService.getPort(); InetAddress host = mService.getHost(); } }; }
执行 resolveService()
成功后,你的app收到了包括ip地址和端口在内的详细的服务信息,可以满足app连接到该服务的全部需要。
在应用关闭时移除服务注册
在应用的生命周期内恰当的启用和停用NSD功能非常重要。当应用关闭时就将服务移除注册,以免其他应用认为其仍然可用而去尝试连接。另外,服务探索是个代价高昂的操作,当父activity暂停时其也应停止,当父aitivity恢复运转时其也应重新启用。所以,重写你的主activity的生命周期方法,插入相关代码来启动和停止服务广播,以及服务探索。
//在你的应用的activity @Override protected void onPause() { if (mNsdHelper != null) { mNsdHelper.tearDown(); } super.onPause(); } @Override protected void onResume() { super.onResume(); if (mNsdHelper != null) { mNsdHelper.registerService(mConnection.getLocalPort()); mNsdHelper.discoverServices(); } } @Override protected void onDestroy() { mNsdHelper.tearDown(); mConnection.tearDown(); super.onDestroy(); } // NsdHelper's tearDown method public void tearDown() { mNsdManager.unregisterService(mRegistrationListener); mNsdManager.stopServiceDiscovery(mDiscoveryListener); }