在你的app中添加网络服务发现功能可以让你的app发现局域网当中的其他设备,来支持你的app所需要的功能。这个功能对于很多的app都很有用,比如说,多人的游戏。Android的NSD API大大简化了你使用这些功能的麻烦。
这节课教你怎样在局域网中广播你的名字和功能以及发现局域网中其他设备的广播。最后我们将实现连接到其他设备中的同样的app。
在你的网络中注册一个服务
提示:这个步骤是可选的。如果你关注你的app是否在局域网中发送广播,你可以跳过到下一节,Discover Service on the Network。
要在局域网中创建你的服务,首先要定义一个NsdServiceInfo对象。这个对象中包含着用来判断是否要连接你的 服务的信息。
public void registerService(int port) { // Create the NsdServiceInfo object, and populate it.创建一个NsdServcie对象,并填充对象 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("_http._tcp."); serviceInfo.setPort(port); .... }这个代码片吧服务的名字起做“NsdChat”。这个名字对于所有在局域网中使用NSD来寻找服务的设备都是可见的。这个名字对于局域网中的所有服务都应该是唯一的,并且android会主动进行冲突处理。如果局域网中两个服务都初始化了“NsdChat”的app,其中的一个就会自动修改名字,比如或“NsdChat(1)”。
下一个蚕食设定了服务的类型,也就是通信协议,和app使用的传输层。语法是“_<protocal>._<transportlayer>”。在这个代码片中,服务使用的是运行在TCP协议上的Http协议。例如一个提供打印服务的app(比如说一个网络打印机)将会设定这个服务类型为“_ipp._tcp”。
提示:互联网数字分配机构(IANA International Assigned Numbers Authority)集中的,权威的被服务发现协议如 NSD和Bonjour可以使用 列表。你可以冲 IANA的服务名和端口号网站上面下载到这个列表。如果你想要使用一个新的服务的类型,你需要通过填写NANA的端口和服务注册表来预约。
当为你的服务设定端口号,不要把它写死,以防止和其他的应用冲突。例如,如果设定你的app使用1337端口,那么你的app就始终处于和其他app冲突的潜在危险中。避免冲突你可以使用下一个端口号。因为是通过广播来让其他的设备知道你的服务的,所以没有必要再编译的时候就指定端口。避免这种情况,这里其他的设备在连接你的 设备之前就可以从你的广播中获得你的服务的接口。
如果你使用socket,那么这里你可以仅仅把它设置为0,含义是可以使用任何可用的接口。
public void initializeServerSocket() { // Initialize a server socket on the next available port.使用下一个可用的端口来初始化socket。 mServerSocket = new ServerSocket(0); // Store the chosen port. 存储这个选择了的端口。 mLocalPort = mServerSocket.getLocalPort(); ... }
现在你定义了一个NsdServiceInfo对象,你需要实现RegistrationListener接口。这个接口包含了被Android回调的函数 ,在服务注册和注销成功或者失败的时候来通知你。
public void initializeRegistrationListener() { mRegistrationListener = new NsdManager.RegistrationListener() { @Override public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) { // Save the service name. Android may have changed it in order to保存服务名,android可能为了解决解决冲突已经改变了他,使用android真正使用名字来更新你的名字。 // resolve a conflict, so update the name you initially requested // with the name Android actually used. mServiceName = NsdServiceInfo.getServiceName(); } @Override public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { // Registration failed! Put debugging code here to determine why. 在这里防止debug的代码来寻找原因。 } @Override public void onServiceUnregistered(NsdServiceInfo arg0) { // Service has been unregistered. This only happens when you call 服务注销成功。只有在你调用NsdManager.unregisterSevice()并传入listener的时候才会调用。 // 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()方法中。
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需要监听网络服务的广播,并且它们会过滤掉你的应用不能使用的广播。
服务发现类似于服务注册,有两个步骤:1.使用相关的回调建立发现listener 2.进行单独的异步API discoverService()调用。
1.实例化一个匿名类,实现MsdManager.DiscoveryListener接口。下面的代码片就是一个简单的例子。
public void initializeDiscoveryListener() { // Instantiate a new DiscoveryListener实例化一个发现Listener mDiscoveryListener = 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 //servcie 的type是一个包含协议和传输层的字符串 // transport layer for this service. Log.d(TAG, "Unknown Service Type: " + service.getServiceType()); } else if (service.getServiceName().equals(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); } else if (service.getServiceName().contains("NsdChat")){ mNsdManager.resolveService(service, mResolveListener); } } @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); 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。注意到这个代码片在服务发现的时候进行了很多的判断。
1.找到的服务的名字和本地的服务相互比较,来判断时候一个设备时候获取了她自己的广播(意思是有效的)。
2.检查服务的类型,来认证你这个服务的类型是你的app可以连接的。
3.检查服务的名字,类认证你连接了正确的application。
检查服务的名字并不总是必要的,如果你只想连接到一个特定的app,它才有关。例如,你的app想要连接到在其他设备上运行的同样的app。但是如果你的app只想连接到一个网络打印机,那么只要判断服务协议是“_ipp._tcp”就可以了。
在设置listener以后,调用discoverServices(),传入你的app要寻找的服务类型,要发现的协议,以及你刚刚创建的listener。
mNsdManager.discoverServices( SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
连接网络上的服务
当你的app在网络上发现了要连接的服务的时候,它必须要首先判断这个服务的网络的连接,使用resolveService()方法。实现一个NsdManager.ResolverListener并把它传递到这个方法中,并且用它来获取一个NsdServiceInfo对象,其中包含了连接的信息。
public void initializeResolveListener() { mResolveListener = 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(mServiceName)) { Log.d(TAG, "Same IP."); return; } mService = serviceInfo; int port = mService.getPort(); InetAddress host = mService.getHost(); } }; }一旦一个服务被解决,你的app就可以从中获取详细的服务信息,包括 ip地址和端口号。这是你要连接这个服务所需要的全部的信息。
在应用关闭的时候注销你的Service
在app的适当的生命周期的时候开启NSD和关闭NSD功能是十分重要的。在app关闭的时候注销服务可以方式其他的设备认为我们的服务仍然可用并仍然尝试连接。并且,设备发现是一个很耗费资源的的活动,应该在父Activity pause的时候停用,并在其resume的时候重新启用。重写你的main Activity的声明周期方法,并合理的开启和停止service广播和发现。
//In your application's 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();//这里注意onDestroy的超类方法要被后调用,不然会被析构掉,功能方法无法调用了。 mConnection.tearDown(); super.onDestroy(); } // NsdHelper's tearDown method public void tearDown() { mNsdManager.unregisterService(mRegistrationListener); mNsdManager.stopServiceDiscovery(mDiscoveryListener); }