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)來分析
通過使用抓包工具來分析,可以最直接的分析到設備間的網絡通訊情況。熟練使用工具能夠跟快跟蹤到問題所在。