返回介绍

Android Says Bonjour

发布于 2022-03-27 22:42:38 字数 11759 浏览 966 评论 0 收藏 0

很高兴能在农历蛇年刚开始的这期《程序员》杂志上继续为读者奉上Android的故事。初来咋到,首先要向大家说声”你好“。有意思的是,Android也很通人情,从4.1开始,它会说”Bonjour“了。不过它说得是不是原汁原味的法语腔呢?来看下文。

一背景知识介绍

Bonjour是法语中的Hello之意。它是Apple公司为基于组播域名服务(multicast DNS)的开放性零配置网络标准所起的名字。使用Bonjour的设备在网络中自动组播它们自己的服务信息并监听其它设备的服务信息。设备之间就像在打招呼,这也是该技术命名为Bonjour的原因。Bonjour使得局域网中的系统和服务即使在没有网络管理员的情况下也很容易被找到。

举一个简单的例子:在局域网中,如果要进行打印服务,必须先知道打印服务器的IP地址。此IP地址一般由IT部门的人负责分配,然后他还得全员发邮件以公示此地址。有了Bonjour以后,打印服务器自己会依据零配置网络标准在局域网内部找到一个可用的IP并注册一个打印服务,名为“print service”之类的。当客户端需要打印服务时,会先搜索网络内部的打印服务器。由于不知道打印服务器的IP地址,客户端只能根据诸如"print service"的名字去查找打印机。在Bonjour的帮助下,客户端最终能找到这台注册了“print service”名字的打印机,并获得它的IP地址以及端口号。

从Bonjour角度来看,该技术主要解决了三个问题:

  • Addressing:即为主机分配IP。Bonjour的Addressing处理比较简单,即每个主机在网络内部的地址可选范围内找一个IP,然后查看网络内部是否有其他主机再用。如果该IP没有被分配的话,它将使用此IP。
  • Naming:Naming解决的是host名和IP地址的对应关系。Bonjour采用的是Multiple DNS技术,即DNS查询消息将通过UDP组播方式发送。一旦网络内部某个机器发现查询的机器名和自己设置的一样,就回复这条请求。此外,Bonjour还拓展了MDNS的用途,即除了能查找host外,还支持对service的查找。不过,Bonjour的Naming有一个限制,即网络内部不能有重名的host或service。
  • Service Discovery:SD基于上面的Naming工作,它使得应用程序能查找到网络内部的服务,并解析该服务对应的IP地址和端口号。应用程序一旦得到服务的IP地址和端口号,就可以直接和该服务建立交互关系。

Bonjour技术在Mac OS以及Itunes、Iphone上都得到了广泛应用。为了进一步推广,Apple通过开源工程mdnsresponder将其开源出来。在Windows平台上,它将生成一个后台程序mdnsresponder。在Android平台上(或者说支持POSIX的Linux平台)它是一个名为mdnsd的程序。不过,不论是mdnsresponder还是mdnsd,应用开发者要做的仅仅是利用Bonjour的API向它们发起服务注册、服务查询和服务解析等请求并接收来自它们的处理结果。

下面我们将介绍Bonjour API中使用最多的三个函数,它们分别是服务注册、服务查询和服务解析。理解这三个函数的功能也是理解MDnsSdListener的基础。

使用Bonjour API必须包含如下的头文件和动态库,并连接到:

#include <dns_sd.h>  //必须包含此头文件

libmdnssd.so  //链接到此so

Bonjour中,服务注册的API为DNSServiceRegister,原型如图1所示:

图1  DNSServiceRegister原型

该函数的解释如下:

  • sdRef:代表一个未初始化的DNSService实体。其类型DNSServiceRef是指针。该参数最终由DNSServiceRegister函数分配内存并初始化。
  • flags:表示当网络内部有重名服务时的冲突处理。默认是按顺序修改服务名。例如要注册的服务名为“printer”,当检测到重名冲突时,就可改名为“printer(1)”。
  • interfaceIndex:表示该服务输出到主机的哪些网络接口上。值-1表示仅对本机支持,也就是该服务的用在loop接口上。
  • name:表示服务名,为空的话就取机器名。
  • regtype:服务类型,用字符串表达。Bonjour要求格式为"_服务名._传输协议",例如"_ftp._tcp"。目前传输协议仅支持TCP和UDP。
  • domian和host一般都为空。
  • port表示该服务的端口。如果为0的话,Bonjour会自动分配一个。
  • txtLen以及txtRecord字符串用来描述该服务。一般都设置为空。
  • callBack:设置回调函数。该服注册的请求结果都会通过它回调给客户端。
  • context:上下文指针,由应用程序设置。

当客户端需要搜索网络内部特定服务时,需要使用DNSServiceBrowser API,其原型如图2所示:

图2  DNSServiceBrowser原型

其中:

  • sdref、interfaceIndex、regtype、domain以及context含义与DNSServiceRegister一样。
  • flags:在本函数中没有作用。
  • callBack:为DNSServiceBrowser处理结果的回调通知接口。

当客户端想获得指定服务的IP和端口号时,需要使用DNSServiceResolve API,其原型如图3所示:

图3  DNSServiceResolve原型

其中:

  • name、regtype和domain都从DNSServiceBrowse函数的处理结果中获得。
  • callBack用于通知DNSServiceResolve的处理结果。该回调函数将返回服务的IP地址和端口号。

以上介绍的三个API是Bonjure的核心API。不过Android中的Bonjour会是怎么个说法呢?

二Android Says Bonjour

几乎能肯定的是,Bonjour想跑在Android平台上,还需要一番定制。不过这套定制不是针对mdnsd本身,而是针对Bonjour API的使用。Android平台的Bonjour架构可由图4表达:

图4  Android Bonjour架构

由图4可知,Android拓展了原有的Bonjour架构,改变如下:

  • 在Netd中增加了MDnsSdListener对象,它一方面通过socket和上层对象通信,另一方面通过Bonjour API和mdnsd通信(也是基于Socket的跨进程通信)。从mdnsd角度来看,它是最懂Bonjour API的”人“了。
  • System_process进程新增NsdService。Nsd是Network Service Discovery的缩写。NsdService通过socket和位于Netd中的MDnsSdListener通信。
  • App借用NsdManager API通过Binder技术和System_process的NsdService通信。

总之,在Android平台中,应用程序要借助其他三个进程(System_process、Netd、mdnsd)才能享受到Bonjour好处。不过,这么繁杂的进程间通信会不会影响效率呢?

答案是肯定的。但就如Bonjour的本意一样,它仅是通过打一声招呼以了解网络内服务是否存在以及一些简单信息。一旦客户端通过Bonjour获取到服务的IP地址和端口后,后续客户端和服务的交互就属于私密范畴(即客户端通过服务的IP地址直接和其建立连接)了。从这个角度来看,Android上的这点效率损失实属无伤大雅。

另外,Android上Bonjour架构的设计对读者们来说还有一个启示:如果手机厂商想定制一些功能,最好先对现有Android的架构有充分了解。这样才能结合自己的需求,将功能模块合理得集成到Android架构中以更有效得发挥其功用。

下面来看看Android中Bonjour架构中的几位重要成员。

2.1  MDnsSdListener介绍

MDnsSdListener在Android Bonjour架构中扮演着转换器的角色:

  • 一方面它处理来自NsdService的请求,并通过Bonjour API将其转换成mdnsd能懂的“语言”以驱动其工作。
  • 另一方面它接收来自mdnsd的信息,并把它们通报给NsdService。

图5所示为MDnsSdListener的家族成员示意图。

图5  MDnsSdListener家族成员

由图5可知:

  • MDnsSdListener的内部类Monitor用于和mdnsd进程通信,它将调用前面提到的Bonjour API。
  • Monitor内部针对每个DNSService都会建立一个Element对象,该对象通过Monitor的mHead指针保存在一个list中。
  • Handler是MDnsSdListener注册的Command。

下面将简单介绍MDnsSdListener的运行过程,其主要工作可分成三步:

  • Netd创建MDnsSdListener对象,其内部会创建Monitor对象,而Monitor对象将启动一个线程用于和mdnsd通信,并接收来自Handler的请求。
  • NsdService启动完毕后将向MDnsSdListener发送"start-service"命令。
  • NsdService响应应用程序的请求,向MDnsSdListener发送其他命令,例如"discovery"等。Monitor将最终处理这些请求。

先来看第一步,当MDnsSdListener构造时,会创建一个Monitor对象,代码如图6所示:

图6  Monitor的构造

由图6可知:

  • Monitor的threadStart线程将调用其run函数,该函数通过poll方式侦听包括mCtrlSocketPair在内的socket信息。这部分代码属于基本的Linux socket编程。对大部分读者来说,难度应该不大。
  • 当NsdService发送"start-service"命令后,Handler的runCommand将执行Monitor的startService函数。

starService将启动mdnsd,其所使用的方法颇具Android特色,如图7所示:

图7  startService代码示意

图7中,MDS_SERVICE_NAME宏代表字符串"mdnsd"。了解Android的读者,看完图7,您能很快知道Android启动mdnsd的方法吗?

当NsdService发送注册服务请求时,Handler的serviceRegister函数将被调用,代码如图8所示:

图8  serviceRegister示意图

DNSServiceRegister内部将把请求发送给mdnsd去处理,处理的结果通过MDnsSdListenerRegisterCallback返回,该函数最终会通过socket把信息传递给NsdService去处理。

MDnsSdListener介绍暂且到此,感兴趣的读者不妨亲自看看代码以加深对Bonjour API用法的理解。

2.2  NsdService介绍

对所有Android App来说,NsdService才是背后的Boss,其用法(当然,是NsdService客户端API的封装类NsdManager的用法)也是在SDK文档中白纸黑字列出来的。NsdService的内部结构可由图9表示:

图9  NsdService内部结构示意图

图9列出了NsdService中的几个重要成员,其中:

  • NsdService从INsdManager.stub派生。这个类也是Android的特色产品,由INsdManager.aidl文件生成。
  • NsdService内部工作将通过NsdStateMachine及内部的三个状态对象(DefaultState、EnableState、DisableState)驱动。让笔者颇为惊讶的是,整个NsdService的代码只有800来行。而且从理论上说,NSD不存在什么状态转换。状态机的出现使得代码理解会相对困难。还好NsdStateMachine只有三个状态。读者不妨以它为契机,了解一下Android中StateMachine的用法。因为它在系统很多地方都被用到。在那些代码中,状态就不止三个了。
  • NsdService通过NativeDaemonConnector和Netd中的MDnsSdListener建立socket通信。
  • 类NativeCallbackReceiver用来通知NsdService来自Netd的消息。
  • 当然,NsdService费劲心力得到的最重要的产出物就是NsdServiceInfo了。它就是Network Service在Android Bonjour中的代表。其包含的内容有服务名、服务类型、IP地址和端口号等。

由于篇幅原因,本文不拟对NsdService展开详细讨论了。接下来,本文将介绍Android SDK中一个关于Nsd API使用的小例子NsdChat。

2.3  NsdChat案例介绍

Android SDK新增了一个NsdChat例子用于向开发者介绍Android平台中Nsd的使用方法。相关文档位于http://developer.android.com/training/connect-devices-wirelessly/nsd.html。案例的源码位于Android4.1源码根目录/development/samples/training/NsdChat下。

该例描述了一个简单的聊天程序,故其命名为NsdChat。Nsd在此例中的作用就是注册并搜索网络内的聊天服务。所以,在本例中有一个NsdChat进程将通过NsdService的registerService函数注册一个聊天服务。相关代码如图10所示:

图10  NsdChat注册聊天服务

由图10可知:

  • 应用程序要注册的Nsd服务将通过NsdServiceInfo类来表达。结合前文背景知识,在此NsdServiceInfo中,最重要就是服务的端口号、服务名(根据Bonjour的要求,网络内部不能有同名服务)以及服务的类型。
  • 接着,应用程序通过NsdManager的registerService函数注册此服务。注册的结果通过NsdManager的内部接口类RegistrationListener来通知。

两人聊天才有意义,所以另外一个运行着NsdChat的客户端进程将搜索网络内部的”NsdChat“服务,相关代码如图11所示:

图11  寻找“NsdChat”服务

由图11可知:

  • 应用进程只需调用NsdManager的discoveryServices函数并传递要找的服务类型即可。搜索的结果通过NsdManager的内部接口类DiscoveryListener返回。

注意,Nsd只能根据服务类型进行搜索。当网络中有多个同属于一种服务类型(本例中,服务类型是"_http._tcp.")的服务时,应用程序还需根据DiscoveryListener返回的信息进行筛选。这部分代码如图12所示:

图12  NsdChat的DiscoveryListener处理

由图12可知,discoveryServices的结果通过DiscoveryListener接口类提供的回调函数返回。注意其中onServiceFound函数对同类型服务的筛选处理(值得特别指出的是,Android SDK中并未对此处极易疏忽的地方做任何说明)。

当客户端成功找到NsdChat服务后,下一步工作就是解析该服务的IP地址和端口号。这是通过NsdManager的resolveService函数(注意图12中的红框)来完成的。这个函数的处理结果将通过NsdManager定义的另外一个接口类ResolveListener返回。

通过对NsdChat的研究,读者会发现:

  • 总体而言,NsdManager的使用并不复杂,相关类也比较简单。
  • 唯一特别之处是其主要API都被设计成异步调用的方式,这将增大应用程序开发的难度。请读取务必注意这点。

三总结

本文对Android中Bonjour的实现进行了一番介绍。Bonjour的原理知识还请读者阅读https://developer.apple.com/library/mac/。该网站是关于Bonjure基础知识的入口,包含《About Bonjour》、《Bonjour API Architecture》等文档。

另外,Android中的Bonjour主要是为了支持Network Service Discovery功能。与其类似的还有UPnP技术中使用的Simple Service Discovery Protocol(SSDP)。相比Bonjour而言,UPnP不仅实现了NSD,还在后续客户端和服务端交互方面支持标准SOAP协议,极大方便了客户端和服务端的代码逻辑实现。所以,笔者在此提醒开发者,如果想使用Bonjour技术,要特别注意Nsd只能简化服务注册及寻找这一步骤,后续还需重点考虑客户端和服务端交互的协议及实现。

关于DLNA,读者可参考笔者的博客 http://blog.csdn.net/innost/article/details/7078539

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文