什么是 mDNS(多播 DNS) mDNS(多播 DNS)是一种基于 UDP 协议的域名系统(DNS),它允许设备在局域网内发现和通信,而无需配置静态 IP 地址,省去了手动配置 IP 地址的步骤,让你可以直接连接到局域网内的智能设备,所以它还有个别名叫做 Zero Configuration Networking(Zeroconf),零配置网络,即无需手动配置,自动连接。 它的经典使用场景就是打印机,你和打印机同处于一个局域网内,可是你打印的时候从来不需要输入打印机的 IP 地址,但你的手机和电脑往往能自动发现并连接到打印机,这就是 mDNS 的魔力所在。
苹果的 Bonjour 协议基于 mDNS 实现,标准 mDNS 让你可以在局域网内互相发现其他设备,可是 Bonjour 让你可以跳出局域网,只要你登录了同一个 Apple ID,它也可以互相识别到。
mDNS 能用来做什么 基于 mDNS,你可以探索局域网内的其他设备,得到对方的基本信息,拿到对方的 IP 地址和端口,你也可以把自己广播出去,它就像一块公告板 ,你可以在上面找到其他人的信息,也可以把自己挂上去。 它帮你敲开了对方的大门,接下来到底是怎么进行交互,比如直接调用某 HTTP 服务,或者用 Socket 连接到对方某端口,进行流媒体传输,全看你想怎么做。
实现局域网设备发现 很多概念还是结合代码实现来理解比较好,接下来我会从 iOS 和安卓的配置部分,以及代码实现部分,分别介绍每一个步骤和配置项的意义。
配置项 安卓 安卓需要配置网络权限部分,在AndroidManifest.xml中添加以下权限:
1 2 3 4 5 <uses-permission android:name ="android.permission.INTERNET" /> <uses-permission android:name ="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name ="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> <uses-permission android:name ="android.permission.NEARBY_WIFI_DEVICES" /> <uses-permission android:name ="android.permission.ACCESS_WIFI_STATE" />
iOS iOS 需要在Info.plist中声明局域网络权限,以及 Bonjour 服务权限。
1 2 3 4 5 6 7 <key > NSLocalNetworkUsageDescription</key > <string > 我需要使用您的本地网络来发现其他设备,这一般会用在打印机,投屏等场景下。</string > <key > NSBonjourServices</key > <array > <string > _http._tcp</string > <string > _https._tcp</string > </array >
NSLocalNetworkUsageDescription是权限描述文案,这个一般会在调用相关 API 时弹出,询问用户是否同意使用本地网络。NSBonjourServices是声明你要发现的服务类型,这里声明了_http._tcp和_https._tcp,即 HTTP 和 HTTPS 服务。_http和_https代表了目标设备所能提供的服务类型,这两个值代表了 HTTP 服务,说明在探索到目标设备后,目标设备将会提供 HTTP 接口服务,你可以使用 HTTP 协议访问对方的某个端口来与其进行交互。 后者_tcp描述了设备发现阶段的传输协议,可选值共有_tcp和_udp,分别代表了 TCP 和 UDP 协议。注意是设备发现阶段,不是后期的通信阶段。
除了_http和_https,还有很多常见的服务类型,比如_ssh._tcp,SSH 服务,用于远程登录,_printer._tcp,打印机服务,你也可以为你的设备自定义服务类型,比如_myapp._tcp。
有两个注意点:
服务类型和协议类型必须以_开头,这是 mDNS 的规范。 在 iOS 上,你必须事先声明你要发现的服务类型,如果你在NSBonjourServices内只写了_http._tcp,但是尝试扫描设备的时候去扫描_ftp._tcp,是不会有任何效果的。 代码实现 发现其他设备 iOS 和安卓的代码逻辑基本相同,大致都是先初始化 mDNS 服务,并指定服务和协议类型,添加监听器,拿到实时的扫描结果,然后开始扫描。
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 34 35 36 37 38 39 40 41 42 import Networkvar browser: NWBrowser ?func startDiscoverServices (serviceType : String = "_https" , protocolType : String = "_tcp" ) { let type = "\(serviceType) .\(protocolType) " browser = NWBrowser (for: .bonjourWithTXTRecord(type: type, domain: nil )) browser? .stateUpdateHandler = { [weak self ] state in switch state { case .ready: print ("mDNS扫描器已准备就绪" ) case .failed(let error): print ("mDNS扫描器启动失败:\(error.localizedDescription) " ) default : break } } browser? .browseResultsChangedHandler = { [weak self ] (services, changes) in for change in changes { switch change { case .added(let service): print ("发现新设备:\(service) " ) case .removed(let service): print ("设备移除:\(service) " ) default : break } } } browser? .start(queue: .global()) } func stopDiscoverServices () { browser? .cancel() }
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 34 35 36 37 38 39 private var nsdManager: NsdManager? = null private var discoveryListener: NsdManager.DiscoveryListener? = null fun startDiscoverServices (serviceType: String = "_https" , protocolType: String = "_tcp" ) { nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager try { val type = "$serviceType .$protocolType " discoveryListener = object : NsdManager.DiscoveryListener { override fun onServiceFound (service: NsdServiceInfo ) { } override fun onServiceLost (service: NsdServiceInfo ) { } override fun onDiscoveryStarted (regType: String ) { } override fun onDiscoveryStopped (serviceType: String ) { } } nsdManager?.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, discoveryListener) } catch (e: Exception) { throw e } } fun stopDiscoverServices () { discoveryListener?.let { try { nsdManager?.stopServiceDiscovery(it) } catch (e: Exception) { } } }
需要注意的是,到目前为止,的确可以拿到局域网中的设备列表了,但是这些设备信息中,并不包含对方的 IP 地址和端口! 相当于普通的上网场景中,你目前知道了对方的域名,但是不知道对方的 IP 地址,还需要通过 DNS 解析才能拿到 IP 和端口。
解析 IP 地址 对于iOS设备,你要通过NWConnection连接到目标设备,才能获取到目标设备的 IP 地址,对于安卓设备,你需要调用NsdManager.resolveService方法来解析 IP 地址。
1 2 3 4 5 6 7 8 9 10 11 12 import Networkfunc resolveIPAddress (for service : NWBrowser .Service ) { let connection = NWConnection (host: service.endpoint.hostname, port: service.endpoint.port, using: .tcp) connection.stateUpdateHandler = { state in if case .ready = state, let endpoint = connection.currentPath? .remoteEndpoint, case .hostPort(let host, let port) = endpoint { print ("目标设备的IP地址为:\(host) " ) } } connection.start(queue: .global()) }
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 fun resolveIPAddress (service: NsdServiceInfo ) { val resolveListener = object : NsdManager.ResolveListener { override fun onResolveFailed (service: NsdServiceInfo , errorCode: Int ) { print("IP地址解析失败:\(errorCode)" ) } override fun onServiceResolved (service: NsdServiceInfo ) { val ipAddress = service.host.hostAddress print("目标设备的IP地址为:\(ipAddress)" ) } } try { nsdManager?.resolveService(service, resolveListener) } catch (e: Exception) { throw e } } <!-- endtab --> {% endtabs %} ### 发布服务 上面说了,mDNS就好像一个公告板,现在我们能够通过公告板上的其他告示,来获取发布者的IP地址了,那么我们也可以把自己的信息发布到公告板上,供其他设备获取。 {% tabs publish-service %} <!-- tab iOS代码 --> ```swift let parameters = NWParameters.tcp parameters.allowLocalEndpointReuse = true listener = try NWListener(using: parameters, on: NWEndpoint.Port(rawValue: 8080 )!) let txtRecord = NetService.data (fromTXTRecord: ["你可以在txt中添加各种自定义的字段,这有助于你的其他设备快速找到你" : "xxx" .data (using: .utf8)!]) listener?.service = NWListener.Service(name: options.name, type: options.service, domain: nil, txtRecord: txtRecord) listener?.stateUpdateHandler = { [weak self] state in switch state { case .failed(let error): let payload: [String: Any] switch error { case .posix(let errorCode): payload = ["errorCode" : errorCode.rawValue, "type" : "POSIX ERROR" ] case .dns(let type): payload = ["errorCode" : type, "type" : "DNS ERROR" ] case .tls(let status): payload = ["errorCode" : status, "type" : "TLS ERROR" ] @unknown default: payload = [:] } self?.sendEvent("onPublishingError" , payload) self?.listener?.cancel() default: break } } listener?.newConnectionHandler = { [weak self] connection in switch connection.endpoint { case let .hostPort(host, port): switch host { case .ipv4(let address): self?.sendEvent("onNewConnectionInComing" , ["host" : address.rawValue.toIPv4String(), "port" : "\(port.rawValue)" ]) case .ipv6(let address): self?.sendEvent("onNewConnectionInComing" , ["host" : address.rawValue.toIPv6String(), "port" : "\(port.rawValue)" ]) default: break } default: break } } listener?.start(queue: .global())
实际运用 mDNS的实际运用场景有什么呢?
对于网络打印机,你可以直接连接到打印机,而无需手动配网。 对于网络摄像头,你可以通过mDNS与设备建立局域网长连接,建立实时的摄像头预览。 对于智能家居场景,你可以实现实时监控,远程控制,智能语音交互等功能。
感谢阅读!