Android局域网通信机制初探

简介

在网络上,两台主机要进行通信,就必须互相知晓对方的IP地址,在Internet上,一台主机的IP地址可能是经常发生变化的,并且IP地址非常难以记忆,因此产生了将主机名(域名)解析为IP地址的DNS服务。在局域网内,往往不存在DNS服务器,而在使用了DHCP的局域网内,各主机的IP往往也是不固定的,因此在主机之间进行IP解析是有一定困难的,为解决这个问题,mDNS应运而生。

在计算机网络中,多播域名系统(multicast Domain Name System, mDNS)被用于在一个不包含DNS服务器的局域网络中做域名解析服务。

RFC 6762定义了mDNS的规范,它使用UDP协议承载,在IPv4网络上,使用组播地址224.0.0.251,在IPv6网络上,使用组播地址ff02::fb, 5353端口来广播报文。Apple的Bonjour就是一个基于mDNS的服务。

工作原理

使用mDNS服务的主机主要执行两个动作:Register和Discover

Register

顾名思义,Register即为主机在网络上发布一个服务,一个最基本的服务包含以下信息:
– 服务类型:表明该主机提供服务的类型,例如,一台提供HTTP访问服务的主机发布的服务类型为_http._tcp
– 服务名称:主机名,类似web网站的域名
– 端口:表明该服务所在的端口

Discover

Discover在网络内查找提供相应服务的主机,查找操作只需提供服务名称即可。

工作流程

提供mDNS服务的主机在加入网络时,会向发送一个广播消息,表明自己的身份,在收到查询请求时,会回复自己的IP地址和端口信息;一个主机发起discover请求时,同样会发送一个广播消息,网络内所有能提供服务的主机都会回复此广播消息。

使用JmDNS库

JmDNS是一个使用Java实现的用于在局域网内进行服务注册和发现的mDNS服务,它完全兼容Apple Bonjour,可工作在JDK 1.6+。

引入

JmDNS在maven中的groupId为javax.jmdns, artifactId为jmdns

实例化

JmDNS jmdns = JmDNS.create();

注册服务

注册服务是可选的,如果只需要使用网络发现,而自己不提供服务的话可以跳过这一步。注册服务需要提供一个ServiceInfo对象,用于描述所提供服务的信息。

//创建ServiceInfo
String serviceType = "_test._tcp.local.";
String serverName = "JmDNS Test";
int port = 1234;
ServiceInfo serviceInfo = ServiceInfo.create(serviceType, serverName, port);  
//注册服务
jmdns.registerService(serviceInfo);

发现服务

发现服务需要创建一个ServiceListener对象,用来处理网络发现过程中各阶段的回调。

jmdns.addServiceListener("_http._tcp.local", listener = new ServiceListener() {
@Override
public void serviceResolved(ServiceEvent ev) {
String addr = "";
if (ev.getInfo().getInetAddresses() != null && ev.getInfo().getInetAddresses().length > 0) {
addr = ev.getInfo().getInetAddresses()[0].getHostAddress();
}
System.out.println("Service resolved: " + ev.getInfo().getName() +" ==> "+ addr + ":" + ev.getInfo().getPort() );
}
@Override
public void serviceRemoved(ServiceEvent ev) {
System.out.println("Service removed: " + ev.getName());
}
@Override
public void serviceAdded(ServiceEvent event) {
// Required to force serviceResolved to be called again (after the first search)
jmdns.requestServiceInfo(event.getType(), event.getName(), 1);
}});

在解析过程中,当一个新服务被发现时,serviceAdded()方法会被回调,在回调中我们可以使用requestServiceInfo()方法发起一个解析请求,当服务被成功解析后,serviceResolved()方法会被回调。当一个服务在网络中被移除时,serviceRemved()会被回调。

已知问题

JmDNS库在Android平台上效率欠佳

使用NsdManager

自Android 4.1开始实现了一个网络服务的发现服务NsdService,其基于苹果的Bonjour服务发现协议,支持远程服务的发现和零配置。Bonjour协议包括IP地址的自动分配、服务名称与地址的转换以及服务的发现三部分内容,Android 4.1借助第三方开源工程mDNSResponder实现了Bonjour协议的服务名称与地址的转换以及服务的发现等Bonjour部分协议的支持。
NsdManager的使用方法和JmDNS十分相似。

实例化

NsdManager nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE)

注册服务

在这里,注册服务同样是可选的

mNsdManager.registerService(serviceInfo,protocolType,listener);

发现服务

发现服务是需要指定服务类型、协议类型和一个Listener

mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, listener);

当发现一个新服务时,onServiceFound()方法会被回调,当一个服务丢失或停止提供时,onServiceLost()会被回调。

服务信息解析

当一个服务被发现时,服务的IP地址和端口号仍然是未知的,需要进一步做解析

nsdManager.resolveService(serviceInfo,listener)

如果解析成功,listener的onServiceResolved()会被回调,否则onResolvedFailed()会被回调,失败的原因主要有以下几种:
1. FAILURE_ALREADY_ACTIVE
出现这个错误表示指定的ServiceInfo已经处于解析状态,一般出现在尝试对一个ServiceInfo进行解析而对应的服务又已经关闭或丢失的情况下,出现此错误后除非重启APP,否则没有其他的恢复手段。
2. FAILURE_MAX_LIMIT
出现这个错误表示NsdManager能执行的解析任务已经到达上限,可能代表着你多次调用了nsdManager.discoverServices()方法添加了多个lisnenet,经过实验,在Android L上并行解析的上限是10个,要解决这一问题,需要在适当的时候调用nsdManager.stopServiceDiscovery()方法来释放listener。
3. FAILURE_INTERNAL_ERROR
表示NsdService出现了内部错误

通过服务的发现和解析过程,就可拿到服务对应的IP和Port.