iOS Bonjour与MDNS协议

背景

在VibeOne的早期版本中,是通过后端接口获得跟手机同一网络下的白板设备的。而后端判断手机和设备是否在同一网络的逻辑也相对比较简单,只是通过::判断手机和白板的IP地址是否相同::来判断两者是否处于同一网络。这种判断方式在相对简单的网络环境下,有一定的效果。但是对于复杂的,::多IP出口的网络::,哪怕手机跟白板都连接在同一WI-FI下,也无法正确判断。这个时候,我们就需::要采用局域网服务(设备)发现技术,在iOS端VibeOne采用了Bonjour协议,其中最核心的就是Multicast DNS(多播DNS)协议。::

DNS协议

既然MDNS是多播DNS协议,那么首先我们需要大致了解一下,什么是DNS协议

::DNS 的全称是 Domain Name System,DNS:: ,是一个由分层的 DNS 服务器(DNS server)实现的分布式数据库,还是一个使得::主机能够查询分布式数据库的应用层协议。::最主要的作用,就是将我们平时访问网站的网址,例如www.baidu.com ,解析成路由器使用的IP地址,例如121.17.106.8

Image.tiff
当我们在浏览器中输入www.xxxx.com/xxxxx.html 这个地址时,为了我们的主机能正确的将HTTP请求报文发送到对应的服务器主机,会经历以下的操作:

  • 主机上运行着DNS应用的客户端
  • 浏览器解析地址,获得主机名www.xxxx.com ,并将主机名传递给DNS应用
  • DNS应用向DNS服务器发送包含主机名的查询请求
  • DNS服务查询后,返回包含主机名对应IP地址的回答报文
  • DNS应用客户端获得IP地址后,提供给浏览器,浏览器向位于该 IP 地址 80 端口的 HTTP 服务器进程发起一个 TCP 连接

这就是DNS协议的基本作用和工作原理,详细的DNS协议内容可以阅读这篇文章万字长文爆肝 DNS 协议

单播、广播、多播(组播)

既然MDNS是多播DNS协议,那么什么是多播呢

::单播、广播和多播(又称组播)是基本的计算机网络概念,表示在网络中报文的不同发送形式。::

单播

当主机A中的某个程序想要将一段报文数据发送给计算机网络中的主机B的某个程序(及两个程序进行::目标明确且唯一的数据传输::)时,就可以采用单播的形式。通常的网络协议采用的就是单播的形式,例如TCP协议,需要建立点多点的连接。

Image.tiff

但是单播有个前提,就是主机A必::须明确的知道主机B的IP地址和主机B上某个程序所运行的端口::

广播

如果主机A不知道主机B的IP地址,那么它还能用什么方式来找到主机B,答案就是采取广播的形式。

如果主机A中的某个程序采用广播的形式,那么::报文信息将被发送到整个网络中的所有设备。::

Image.tiff

如果主机A在报文信息中携带了自己的IP地址和端口,那么主机B可以采用单播的形式应答主机A

Image.tiff

如果没有,那么主机B也只能通过广播的形式应答主机A

组播

从单播和组播的局限性来看,单播必须明确知道通信方的IP地址,而广播会导致不想收到信息的网络中设备(主机C)也收到报文信息。因此还有另外一种方式,就是组播。

组播的形式就是将不同的主机::按照服务类型分组::,当请求报文到达组播路由器后,路由器就可以根据服务类型,发送到对应组内的主机。那么一台主机如何加入某个组中呢?

主机主动加入组:

主机主动告知网络中的组播路由器要加入某个组,这个时候组播路由器就知道网络内有224.0.0.251组的成员,会建立组的转发表

Image.tiff

当有其他主机发送广播的目的地址为224.0.0.251时,组播路由器会将这个数据包转发给组内的其他成员,所以源主机根本不关注目的主机是谁。

组播路由器发现组:

组播路由器会周期的向局域网发送广播查询有哪些组存在

Image.tiff

当组形成以后,主机就可以向组播路由器发送组播信息,组播信息就会被发送到组内所有主机

Image.tiff

Bonjour

Bonjour协议是由三种协议实现的::Link-Local Address协议、Multicast Dns协议和DNS-SD(dns-base service discovery)协议。::

Bonjour将底层的具体实现屏蔽,提供给上层应用简单的API,使用者从三种基础通信变成了简单的函数调用:

  • 通告服务API:用来宣告自己提供哪些服务
  • 发现服务API:用来查询局域网内是否有它所需要的服务
  • 解析服务API:通过发现服务API获取服务实例,进一步解析服务提供的信息

我们知道,可用于网络通信的设备(计算机、智能手机、路由器等)都有唯一的MAC地址。但是在IP网络中,设备通常使用IP地址进行通信。所以需要将MAC地址和IP私有地址进行绑定。

IPv4的私有地址范围指的是在局域网内部使用的地址,用于在私有网络中提供内部通信功能,不在公网上使用。

::Link-Local Address协议就是将设备的MAC地址和IP私有地址进行绑定::。整个基本流程如下。

  1. 主机选择一个私有地址,使用ARP协议查询选中IP对应的MAC地址,查询三次

Image.tiff

  1. 如果没有查到,则发送组播,宣告自己使用这个IP,同时把自己的MAC地址带上

Image.tiff

  1. 假设查到并得到使用这个IP地址的设备的单播回复,则重新选择IP,并重复流程。

这样的IP分配算法效率相对低下,但是在局域网内是够用的。

Multicast DNS

该协议用于在局域网内建立不依赖DNS服务器的解析服务,将设备的IP地址和设备提供的服务的服务名建立联系。

MDNS的顶级域名是.local ,具体流程如下:

  1. 提供服务的主机将自己提供服务的域名xxx.local 与IP等信息通过组播发送出去查询,发送的是查询报文,重复3次。过程如下:

Image.tiff

Multicast DNS的组播地址为224.0.0.251,端口5353

  1. 如果没有其他主机回复,则会再发一次组播,宣告自己提供服务,IP是XXXX,端口是XXXX,发送的是响应报文,其他机器收到后会更新本地的DNS缓存。

Image.tiff

  1. 如果有人回复该域名已经有映射关系,服务主机会更新域名,并重复。

DNS-SD (dns-based serveice discovery)协议

DNS- SD协议使用MDNS的资源记录功能中的三种类型存储去建立服务类型、服务实例、服务实例IP+PORT的映射关系,以达到快速发现服务的目的。

MDNS 为什么不直接实现服务发现功能?

MDNS的功能定义是明确的,用于实现无DNS服务的域名解析。服务发现不属于它该有的功能,这种功能属于具体业务场景。所以Bonjour通过DNS- SD与MDNS结合起来,实现服务发现。

简而言之:该协议定义了三种存储类型的数据格式,相当于建立通讯录,以及通讯录里面的数据格式,以便于发现服务。

DNS- SD对MDNS数据段的使用方式如下:

  • PTR记录:将服务名称与服务类型建立映射关系
  • SRV记录:将服务于IP地址和端口号建立映射关系
  • TXT记录:服务所附带的其他数据

下面就是DNS- SD的工作过程:

查询及宣告服务:

  1. 查询服务

    发送的是查询报文,查询三次,DNS- SD基于MDNS将服务域名于IP和PORT的信息放在SRV区域:

Image.tiff

  1. 宣告服务

    如果没有其他主机回复则会再次使用组播,宣告自己提供服务,发送的是PTR响应报文:

Image.tiff

  1. 服务冲突

    如果有收到回复存在冲突,将服务域名更改并重复第1、2步

发现服务:

主机定期通过组播查询某个类型的服务列表,发送的是PTR查询报文,因为一开始查的都是某个服务类型,进而获得某个服务类型的服务列表

解析服务:

发现服务发送的是PTR查询,IP和服务域名的映射关系存放在SRV数据段中,所以想获得具体IP需要进一步对服务进行解析

在MAC中,可以是使用dns-sd命令进行查询,例如,我们使用dns-sd -Z _vibecast._tcp 命令查询当前网络中所有提供_vibecast._tcp 服务的信息

Image.png

其中SRV信息需要通过dns-sd -q 命令进一步解析

Image.png

具体代码

1
2
3
4
5
6
7
- (void)createServiceBrowser {
[self.netDeviceArray removeAllObjects];
self.vibeBrowser = [[NSNetServiceBrowser alloc] init];
self.vibeBrowser.delegate = self;
[self.vibeBrowser scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.vibeBrowser searchForServicesOfType:@"_vibecast._tcp" inDomain:@"local."];
}

这部分主要就是创建NSNetServiceBrowser 实例,并设置需要查询的服务域名和顶级域名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
didFindService:(NSNetService *)service
moreComing:(BOOL)moreComing {
NSLog(@"NetServiceBrowser:: Find service: %@", service);
for (NetDevice *nd in self.netDeviceArray) {
if ([nd.service.hostName isEqualToString:service.hostName]) {
DDLogError(@"The service already exists in the cache! Please check!");
return;
}
}
service.delegate = self;
[service startMonitoring];
[service resolveWithTimeout:15];
NetDevice *netDevice = [[NetDevice alloc] initWithService:service];
[self.netDeviceArray addObject:netDevice];
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
didRemoveService:(NSNetService *)service
moreComing:(BOOL)moreComing {
NSLog(@"NetServiceBrowser:: Remove service: %@", service);
for (NetDevice *nd in self.netDeviceArray) {
// delegate will give a new service instance, so just can find the same device by name
if ([nd.service.name isEqualToString:service.name]) {
if (nd.info[@"deviceUserId"]) {
[[FindDeviceModule share] deviceRemove:nd.info[@"deviceUserId"]];
}
[self.netDeviceArray removeObject:nd];
return;
}
}
DDLogError(@"In general, code never run into here, please check!");
}

这部分通过NSNetServiceBrowser 实例的两个生命周期函数,来记录搜寻到的和移除的服务记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data {
NSDictionary *recordDict = [NSNetService dictionaryFromTXTRecordData:data];
for (NetDevice *nd in self.netDeviceArray) {
if (nd.service == sender) {
for (NSString *str in [recordDict allKeys]) {
nd.info[str] = [[NSString alloc] initWithData:recordDict[str] encoding:NSUTF8StringEncoding];
}
if (self.isExecuted) {
[[FindDeviceModule share] deviceChange];
}
return;
}
}
DDLogError(@"In general, code never run into here, please check!");
}

当一台设备的TXTRecord信息改变时,就会更新存储的设备列表里的信息