mDNS实现之mdnsresponder介绍
一、名词介绍
mdnsresponder:是Apple实现Benjour的一个开源工程。
Bonjour:Apple基于组播域名服务(multicast DNS)的开放性零配置网络标准所起的名字。Bonjour技术在Mac OS以及iTunes、iPhone上广泛应用(airplay)
zeroconf(Zero configuration networking):零配置网络服务规范,是一种用于自动生成可用IP地址的网络技术,不需要额外的手动配置和专属的配置服务器。Zeroconf规范的提出者是Apple公司。
mDNS:即组播域名服务(multicast DNS)。使用5353端口,在内网没有DNS服务器时,就会出现此组播信息。mNDSS是实现跟DNS相似服务,使得在没有NDS服务的情况下使局域网内的主机实现相互发现和通信。(The name "mDNS" was chosen because this protocol is designed to be,as much as possible, similar to conventional DNS)
二、实现机制
开源工程mDNSResponder实现了 Bonjour协议的服务名称与地址的转换以及服务的发现等 Bonjour部分协议的支持。Bonjour协议的服务名称与地址的转换以及服务的发现采用的流程和DNS流程近似包括:登记过程、服务发现过程、服务 地址解析过程以及建立连接等过程,服务发现采用的协议也和DNS协议相似,不过与DNS协议采用的单播方式不同的是采用了组播方式,因此被称为mDNS。
mdnsresponder是C代码实现,支持多种平台,在Windows平台上,它将生成一个后台程序mdnsresponder。在Android平台上(或者说支持POSIX的Linux平台)它是一个名为mdnsd的程序。不过,不论是mdnsresponder还是mdnsd,应用开发者要做的仅仅是利用工程中提供的API向它们发起服务注册、服务查询和服务解析等请求并接收来自它们的处理结果。mdnsd或者mdnsresponder作为守护进程,在开机启动时就开启,用户通过调用dns_sd.h里的API接口来实现服务注册、服务查询和服务解析等功能
三、主要的API接口
服务注册的API为DNSServiceRegister,原型如下。
DNSServiceErrorType DNSSD_API DNSServiceRegister
(
DNSServiceRef *sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
const char *name, /* may be NULL */
const char *regtype,
const char *domain, /* may be NULL */
const char *host, /* may be NULL */
uint16_t port, /* In network byte order */
uint16_t txtLen,
const void *txtRecord, /* may be NULL */
DNSServiceRegisterReply callBack, /* may be NULL */
void *context /* may be NULL */
);
该函数的解释如下。
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字符串用来描述该服务。
txtRecord格式为键值对(name/value pairs)例如:0x0A | name=value | 0x08 | paper=A4 | 0x12 | Rendezvous Is Cool |
callBack表示设置回调函数。该服务注册的请求结果都会通过它回调给客户端。
context表示上下文指针,由应用程序设置。
当客户端需要搜索网络内部特定服务时,需要使用DNSServiceBrowser API,其原型如下。
DNSServiceErrorType DNSSD_API DNSServiceBrowse
(
DNSServiceRef *sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
const char *regtype,
const char *domain, /* may be NULL */
DNSServiceBrowseReply callBack,
void *context /* may be NULL */
);
其中,sdref、interfaceIndex、regtype、domain以及context含义与DNSServiceRegister一样。flags在本函数中没有作用。callBack为DNSServiceBrowser处理结果的回调通知接口。
当客户端想获得指定服务的IP和端口号时,需要使用DNSServiceResolve API,其原型如下。
DNSServiceErrorType DNSSD_API DNSServiceResolve
(
DNSServiceRef *sdRef,
DNSServiceFlags flags,
uint32_t interfaceIndex,
const char *name,
const char *regtype,
const char *domain,
DNSServiceResolveReply callBack,
void *context /* may be NULL */
);
其中,name、regtype和domain都从DNSServiceBrowse函数的处理结果中获得。callBack用于通知DNSServiceResolve的处理结果。该回调函数将返回服务的IP地址和端口号
mdnsresponder在linux上的实现
一、 工程源码
http://www.opensource.apple.com/source/mDNSResponder/只能浏览,没有提供下载。
http://www.opensource.apple.com/tarballs/mDNSResponder/ 各个版本的打包文件,直接下载。
本文以选用mDNSResponder-320.10.80。
二、工程目录介绍
mDNSCore:主要核心协议引擎代码,纯C语言编写,各个平台都需要依赖该核心代码。
mDNSShared:多个平台共享的非核心引擎代码。
mDNSPosix:Posix平台相关代码。
Clients:包括如何使用后台服务提供的API的客户端例子代码等四个目录。
在linux下实现只需要以上几个目录代码。
使用mDNSPosix的Makefile编译(make os=linux)生成以下文件(/build/prod),可以修改Makefile的Debug=1项来生成有debug信息的文件(/build/debug下)
编译Clients生成一个dns-sd执行文件用于测试,用于跟mndsd服务通讯。
其中mdnsd是一个后台服务,这个服务应该设置随着系统启动时运行,libmdnssd是一个 MDns监视层(dns-sd)使用的库libmdnssd。
专用设备使用文件 (printer, network camera, etc.)
- mDNSClientPosix
- mDNSResponderPosix
- mDNSProxyResponderPosix
要把程序运行在嵌入式系统板上,需要修改Makefile来进行交叉编译
把
ifeq ($(findstring linux,$(os)),linux)
CFLAGS_OS = -D_GNU_SOURCE -DHAVE_IPV6 -DNOT_HAVE_SA_LEN -DUSES_NETLINK -DHAVE_LINUX -DTARGET_OS_LINUX -fno-strict-aliasing
LD = gcc –shared
改为
CC = /opt/mtk/gnu-toolchain_4.8.2_2.6.35_cortex-a9-neon/bin/armv7a-mediatek482_001_neon-linux-gnueabi-gcc
LD = /opt/mtk/gnu-toolchain_4.8.2_2.6.35_cortex-a9-neon/bin/armv7a-mediatek482_001_neon-linux-gnueabi-gcc –shared
/opt/mtk/gnu-toolchain_4.8.2_2.6.35_cortex-a9-neon/bin/armv7a-mediatek482_001_neon-linux-gnueabi-gcc是具体平台的编译工具链
生成mdnsd放到板子上运行,将生成的dns-sd运行起来,./dns-sd –h可以看到dns-sd测试程序的测试提示信息。接下来可以修改dns-sd.c里的代码来定制自己的测试项。
下面是一个注册airplay服务和raop服务的demo:
- Demo:
- {
- #define kRaopPort 50001
- #define kAirplayPort 50002
- static DNSServiceRef airplayRef = NULL;
- static DNSServiceRef raopRef = NULL;
- Opaque16 AirplayPort = { { kAirplayPort >> 8, kAirplayPort & 0xFF } };
- Opaque16 RaopPort = { { kRaopPort >> 8, kRaopPort & 0xFF } };
- static const char AirplayTXT[] =
- "\x1A" "deviceid=0c:54:a5:56:9d:80" \
- "\x0F" "features=0x3FFF"; \
- //"\x10" "model=AppleTV3,1";
- //"\x0E" "srcvers=150.33";
- static const char RaopTXT[] =
- "\x06" "tp=UDP" \
- "\x08" "sm=false" \
- "\x08" "sv=false" \
- "\x04" "ek=1" \
- "\x06" "et=0,1" \
- "\x06" "cn=0,1" \
- "\x04" "ch=2" \
- "\x05" "ss=16" \
- "\x08" "sr=44100" \
- "\x08" "pw=false" \
- "\x04" "vn=3" \
- "\x09" "txtvers=1";
- err = DNSServiceRegister(&airplayRef, 0, opinterface, "JieTools", "_airplay._tcp.", "", NULL, AirplayPort.NotAnInteger, 0, NULL, reg_reply, NULL);
- if (!err) err = DNSServiceUpdateRecord(airplayRef, NULL, 0, sizeof(AirplayTXT)-1, AirplayTXT, 0);
- err = DNSServiceRegister(&raopRef, 0, opinterface, "0C54A5569D80@JieTools", "_raop._tcp.", "", NULL, RaopPort.NotAnInteger, 0, NULL, reg_reply, NULL);
- if (!err) err = DNSServiceUpdateRecord(raopRef, NULL, 0, sizeof(RaopTXT)-1, RaopTXT, 0);
- while(1)getchar();
- return 0;
- }
- #endif
前两行定义指定服务端口,而后的AirplayTXT与RaopTXT分别两个服务的描述内容,下面对AirplayTXT做简单说明:
"\x1A"这样的写法,是为字符串前添加长度字值,为16进制,deviceid后面的值是本机网卡的物理地址,features这个参数不能少,它是airplay服务所支持的特性或能力描述,其它的参数可以忽略。
RaopTXT描述内容是我通过抓包COPY下来的,没有修改过;再接下来调用了两个mDNS SDK中的两个API,DNSServiceRegister用于注册,DNSServiceUpdateRecord用来更新服务的TXTRecord信息。
这里有两组调用服务注册,这里需要注意的是,如果你想实现_airplay服务,那么就必须将这两个服务一起注册,并且服务名称必须一致,如第四个参数是服务名称“JieTools”及“0C54A5569D80@JieTools”,注意命名规则。
OK,不出意外的话,运行它,打开你的手机,就能在airplay中发现自己注册的这个服务了
问题总结
1、 选择合适测试平台
测试应该选择合适平台,由于我的目的是要移植到linux arm平台,所以我选择linux环境来编译测试,本机在虚拟机上安装ubuntu,在ubuntu上进行编译测试,编译运行都没有问题,但是手机端怎么都发现不了设备,使用工程里ReadMe的测试方法:mDNSResponderPosix mDNSClientPosix测试也没起作用,最后是重新编译放到板子上运行,手机端能够发现到所注册的服务了。测试平台不能选择虚拟机
2、在测试过程中怎么看打印信息
默认情况下,打印信息都保存到/var/log/system.log里面,在运行mdnsd是带上debug参数(mdnsd -debug),或者把Makefile的编译项改成DEBUG=1就能在窗口上实视看到打印输出。调试过程中建议把debug信息打印出来,方便跟踪问题。
3、 调用函数DNSServiceRegister会返回-65549
返回-65549是错误代码,这种情况一版是参数不对造成,一般情况下,txtRecord参数容易出错,debug信息提示
Sep 15 16:06:13 localhost mDNSResponder[192]: Attempt to register record with invalid rdata: 17 Ice Cube._http._tcp.local. TXT ath=/index.html
TXT record的格式是:长度键值对长度键值对(length byte, data, length byte, data)
长度是16进制表示,键值对是=左边是字符串,右边是值,各个键值对之间没有间隔如:\011txtvers=1\020path=/index.html\025note=Bonjour Is Cool!
4、 要在ios端的airplay上发现服务,需要一些专有参数
对DNSServiceRegister函数的参数,regtype参数必须是_服务名._传输协议,并且只支持tcp和udp,其他的参数都没有做特殊要求。但是如果要让ios能够发现服务,_airplay._tcp 和_raop._tcp 两种服务都要注册,并且两个服务名name要一样,如:airplay的服务名称是hzzTools ,则raop的服务名称是0C54A5569D80@JieTools,其中0C54A5569D80是MAC地址。参数txtRecord里两个参数是必须的,deviceid本机网卡的物理地址和features
5、 注册服务后还需要更新服务信息
调用DNSServiceRegister注册服务后,还需要调用DNSServiceUpdateRecord来更新服务的TXTRecord信息。这样ios端才能发现到服务。
6、 在测试过程中除了打开debug信号来跟踪,还可以用抓包工具(如:Wireshark)来分析
通过使用抓包工具来分析,可以最直接的分析到设备间的网络通讯情况。熟练使用工具能够跟快跟踪到问题所在。