受欢迎的博客标签

mDNS:使用mDNS进行局域网服务发现源代码(.NET )

Published

使用mDNS进行局域网服务发现源代码(.NET )

MDNS和DNS-SD是DNS协议的两个扩展。MDNS扩展了域名服务系统,在链路本地多播上运行。

DNS-SD添加了通过DNS发现网络服务的支持

场景

开发需求中,需要开发投影仪设备遥控功能。为了提升用户的体验需要实现手机与投影仪设备之间的近场发现.

智能设备,就是传统设备+wifi。常见的网络问题就是配网和设备发现。

 

mdns

首先MDNS和DNS-SD是DNS协议的两个扩展。

MDNS扩展了域名服务系统,在链路本地多播上运行。

DNS-SD添加了通过DNS发现网络服务的支持。

MDNS原理

在基于udp加入网络后向所有主机组播一个消息。

即多播dns(Multicast DNS),mDNS主要实现了在没有传统DNS服务器的情况下使局域网内的主机实现相互发现和通信,使用的端口为5353,遵从dns协议,使用现有的DNS信息结构、名语法和资源记录类型。

并且没有指定新的操作代码或响应代码。

在局域网中,设备和设备之前相互通信需要知道对方的ip地址的,大多数情况,设备的ip不是静态ip地址,而是通过dhcp协议动态分配的ip 地址,如何设备发现呢,就是要mdns大显身手,例如:现在物联网设备和app之间的通信,要么app通过广播,要么通过组播,发一些特定信息,感兴趣设备应答,实现局域网设备的发现,当然mdns 比这强大。

MDNS就是Multicast DNS,在内网没有DNS服务的时候,可以使用它来进行组播实现DNS。使用UDP协议的5353端口。

mDNS 在不同系统中的实现

avahi:Linux下实现(http://www.avahi.org/)
jmDNS:JAVA实现(http://jmdns.sourceforge.net/)
Bonjour:MAC OS实现(默认安装)和 Windows下实现(需要安装Bonjour Print Services)

Bonjour

HomeKit使用Bonjour作为其零配置与设备发现的服务,Bonjour底层使用了 mDNS(Multicast DNS) 与DNS-SD协议实现了零配置、服务发现的机制;

mDNS 最终成为一个正式标准(RFC 6762), 从Windows 10, Mac, Linux到RaspberryPi都支持这个协议.

这个协议比较著名的实现就是苹果的Bonjour,有一个非常有名的zeroconf,mDNS也是一个标准(RFC6762)。

Multicast DNS(mDNS)是一种在小型网络(例如单个子网)中无需中央站点分配名称就可以解析网络主机名的方法。它是零配置网络(Zeroconf)协议套件的一部分,该套件旨在让网络设备在没有额外配置的情况下互相发现和通信。

苹果的Bonjour服务(mDNS)通过使用.local后缀,实现了多址广播域名的设备识别

Linux -avahi-daemon

avahi-daemon 是一种Linux操作系统上运行在客户机上实施查找基于网络的Zeroconf service的服务守护进程。 该服务可以为Zeroconf网络实现DNS服务发现及DNS组播规范。 用户程序通过Linux D-Bus信息传递接收发现到网络服务和资源的通知

A simple Multicast Domain Name Service based on RFC 6762. Can be used as both a client (sending queries) or a server (responding to queries).

A higher level DNS Service Discovery based on RFC 6763 that automatically responds to any query for the service or service instance.

 

ping raspberrypi.local

Avahi 是一个免费的零配置网络 (zeroconf) 实现,包括一个用于组播 DNS/DNS-SD 服务发现的系统。它允许程序发布和发现在本地网络上运行的服务和主机,而无需特定配置。比如,traefik.local、homepage.local就可以轻松实现。

 

 

局域网内的电脑基本上都会加入这几个组播地址:224.0.0.251,224.0.0.252,239.255.255.250。

这三个组播地址分别是协议 mDNS,LLMNR 和 SSDP 协议的。mDNS实现了类似 DNS 的功能,使得主机在局域网内能够加入和通信;LLMNR 的功能和 mDNS 类似;SSDP 是 UPnP 协议的一部分,也是用来实现设备和服务的发现的。

 

组播IP:224.0.0.251,端口:5353,返回方式为QU(规定目标主机的返回方式为单播,QM:表示组播)。

 

基本流程就是基于某个自定义域名发布服务,发现设备,连接设备

jmDNS

jmDNS:JAVA实现(http://jmdns.sourceforge.net/)

 

方案

搭建一个UDP广播服务Server,然后Client端监听广播,收到广播之后即可知道IP地址信息,然后进行后续的数据传输操作。实现起来还是挺简单的,可以参考这个问答

有规范协议的,大概有这么些:

1. WS-Discovery

WS-Discovery(Web Services Dynamic Discovery,WSD)是一种局域网内的服务发现多播协议,WS-Discovery定义了两种基本的实现服务发现机制的操作模式,即Ad-Hoc和Managed。

在Ad-Hoc模式下,客户端在一定的网络范围了以广播的形式发送探测(Probe)消息以搜寻目标服务。在该探测消息中,包含相应的搜寻条件。服务该条件的目标服务在接收到探测消息之后将自身相关的信息(包括地址)回复给作为广播消息发送源的客户端。客户端根据获取到的服务信息,选择适合的服务进行调用。

在Managed模式下,一个维护所有可用目标服务的中心发现代理(Discovery Proxy)被建立起来,客户端只需要将探测消息发送到该发现代理就可以得到相应的目标服务信息。由于在Ad-Hoc模式下的广播探测机制在Managed模式下被转变成单播形式,带来的好处就是极大地减轻了网络负载(Network Traffic)。

这个技术是OASIS标准协议,并且在WCF中有完整实现,对应可以搜索UdpDiscoveryEndpoint就可以找到相关的信息。

最开始就是想使用这个协议的,不过WCF已经被弃用了,.NET Core没有对应的服务端支持,可惜。

2. Consul/ZooKeeper

既然WCF要被淘汰了,后续的替代,微软有一篇文章提到了这两个东西,基本上就是WS-Discovery的Managed方式,提供一个代理用于各种服务进行注册,但是还是需要提前配置这些服务注册服务器的地址,达不到我的要求。

3. MDNS

MDNS就是Multicast DNS,在内网没有DNS服务的时候,可以使用它来进行组播实现DNS。使用UDP协议的5353端口。基于这个协议比较著名的实现就是苹果的Bonjour,也有一个非常有名的zeroconf也是差不多这个意思,mDNS也是一个标准(RFC6762)。

在前面两个都用不了的情况下,只能用这个了。

实现

首先,新建一个 .NET Core  的项目,使用  nuget 命令引用如下包:

 

首先安装nuget包,这个包里面包含有server/Client端。

install-package Makaretu.Dns.Multicast.New
 

https://github.com/richardschneider/net-mdns

思路是这样的,基于ServiceDiscovery发布一个服务,并将额外的信息发布到然后监听各种mDNS请求客户端,通过服务名发送查询请求,并定位服务的地址信息,然后发送SRV,A和TXT查询请求获得服务全名,IP地址和额外配置信息。这样就获得了在局域网内的服务信息了。

客户端接收的时候,使用了服务名称作为筛选的依据。

 

 

mDNS服务端(网关-发起查询请求)

using Makaretu.Dns;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;

// ptr aaa etc:https://github.com/karlredgate/mDNS-sharp/blob/master/mDNS.cs

// server 发起查询的主机   Start MDNS server.  Find Device 

namespace mDNS_Discovery_ConsoleApp.Server
{
    internal class Program
    {
        static readonly object ttyLock = new object();
        static void Main(string[] args)
        {

            var mdns = new MulticastService();

            var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();

            foreach (var network in networkInterfaces)
            {
                Console.WriteLine(
                    $"Interface {network.Name}, Type {network.NetworkInterfaceType}, OP Status {network.OperationalStatus}, IP {network.GetIPProperties().UnicastAddresses.FirstOrDefault()}");
            }

            foreach (var a in MulticastService.GetIPAddresses())
            {
                Console.WriteLine($"Program.cs ->IP address {a}");
            }

           

            //   Find all services running on the local link.

            var sd1 = new ServiceDiscovery();
            sd1.ServiceDiscovered += (s, serviceName) =>
            {
                // Do something

                Console.WriteLine($"all services running on the local link {serviceName} \n");

            };

            //Find all service instances running on the local link.
            //https://github.com/richardschneider/net-mdns
            sd1.ServiceInstanceDiscovered += (s, e) =>
            {
                //if (e.Message.Answers.All(w => !w.Name.ToString().Contains("ipfs1"))) return;

                //Typically of the form "instance._service._tcp.local
                Console.WriteLine($"{DateTime.Now} Find all service instances running on the local link,Typically of the form instance._service._tcp.local '{e.ServiceInstanceName}'");

                // Ask for the service instance details.
                mdns.SendQuery(e.ServiceInstanceName, type: DnsType.SRV);
            };



          

            mdns.AnswerReceived += (s, e) =>
            {
                var names = e.Message.Answers
                    .Select(q => q.Name + " " + q.Type)
                    .Distinct();
                Console.WriteLine($"got answer for {String.Join(", ", names)} \n");
            };

            mdns.QueryReceived += AnswerReceived;


            mdns.NetworkInterfaceDiscovered += (s, e) =>
            {
                foreach (var nic in e.NetworkInterfaces)
                {
                    Console.WriteLine($"discovered NIC '{nic.Name}\n'");
                }


            };

            mdns.NetworkInterfaceDiscovered += (s, e)
               => mdns.SendQuery(ServiceDiscovery.ServiceName, type: DnsType.PTR);



            mdns.Start();

            MulticastService_GetIPAddresses();

            Console.ReadKey();

        }

        /// <summary>
        /// 收到mDNS查询请求后的回答
        /// https://github.com/oddbear/Loupedeck.KeyLight.Plugin/blob/0981a12e4c5aba5bc2efec7e29f185df559b4b7a/KeyLightPlugin/KeyLightPlugin.cs#L36
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void AnswerReceived(object sender, MessageEventArgs e)
        {


            lock (ttyLock)
            {
                var names = e.Message.Answers
                   .Select(q => q.Name + " " + q.Type);
                Console.WriteLine($"Answer Received for {String.Join(", ", names)} \n");

                Console.WriteLine("detail === {0:O} ===", DateTime.Now);
                Console.WriteLine(e.Message.ToString());


            }

            //https://www.cnblogs.com/xueyk/articles/mDNS.html
            try
            {
                //查看应答信息
                if (e.Message.AdditionalRecords.Count > 0)
                {
                    foreach (ResourceRecord rr in e.Message.AdditionalRecords)
                    {
                        Console.WriteLine($"DomainName:{rr.Name},Canonical:{rr.CanonicalName},Type:{rr.Type.ToString()}\r\n");
                        string resultStr = rr.ToString();
                        byte[] byteArray = rr.GetData();
                        Console.WriteLine($":{resultStr},[DataLength]{byteArray.Length}\r\n");
                    }
                }

                if (e.Message.Answers.Count > 0)
                {
                    Console.WriteLine($"\r\n***************Answers*****************\r\n");
                    foreach (ResourceRecord rr in e.Message.Answers)
                    {
                        Console.WriteLine($":{rr.ToString()}\r\n");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }


        }


        /// <summary>
        /// https://github.com/oddbear/Loupedeck.KeyLight.Plugin/blob/0981a12e4c5aba5bc2efec7e29f185df559b4b7a/KeyLightPlugin/KeyLightPlugin.cs#L36
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MulticastServiceOnAnswerReceived(object sender, MessageEventArgs e)
        {
            try
            {
                if (e.Message.Answers.All(resourceRecord => resourceRecord.CanonicalName != "_elg._tcp.local"))
                    return;

                var address = e.RemoteEndPoint.Address.ToString();
                if (e.RemoteEndPoint.Address.AddressFamily == AddressFamily.InterNetworkV6)
                    address = $"[{address}]"; //[...%10]:1234, the [] is needed to be allowed to specify a port (IPv6 contains ':' chars), and '%' is to scope to a interface number.

                var dnsName = e.Message.AdditionalRecords?
                    .First(resourceRecord => resourceRecord.Type == DnsType.TXT)
                    .Name
                    .Labels
                    .First();

                if (string.IsNullOrWhiteSpace(dnsName))
                    return;

                ////Light found, check if it's a new one or existing one:
                //if (!KeyLights.TryGetValue(dnsName, out var keyLight))
                //    KeyLights[dnsName] = keyLight = new DiscoveredKeyLight { Id = dnsName };

                ////Updates address:
                //keyLight.Address = address;

                ////Updates states:
                //var cancellationToken = CancellationToken.None;

                ////Update lights state, and raise lights updated events:
                //keyLight.Lights = KeyLightService.GetLights(address, cancellationToken);
                //LightsUpdated(dnsName, keyLight.Lights);

                //keyLight.AccessoryInfo = KeyLightService.GetAccessoryInfo(address, cancellationToken);
                //keyLight.Settings = KeyLightService.GetLightsSettings(address, cancellationToken);

                //AdditionalRecords:
                //A: IPv4 address record
                //AAAA: IPv6 address record
                //SRV: Service locator
                //TXT: Text record
                //NSEC (A, AAAA): Next Secure record
                //NSEC (TXT, SRV): Next Secure record
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }


        #region test MulticastService

        /// <summary>
        /// https://github.com/SteeBono/airplayreceiver/blob/806fd39ef263a2b38bdd7c8e636a9fd804a94c4e/AirPlay/AirPlayReceiver.cs#L66
        /// </summary>
        public static void MulticastService_GetIPAddresses()
        {
            foreach (var ip in MulticastService.GetIPAddresses())
            {
                Console.WriteLine($"MulticastService_GetIPAddresses() ->IP address {ip}");
            }
        }

        #endregion

    }
}

运行效果如下:

 

mDNS-Discovery-ConsoleApp.Client - mDNS智能子设备(客户端)

step 1.智能设备配网
通过通讯,从app往设备发送wifi账号和密码,通常由蓝牙实现。

step 2.设备注册路由组播

发 IGMP 报文加入分组 224.0.0.251

var sd = new ServiceDiscovery();
//发布一个服务,服务名称是有讲究的,一般都是_开头的,可以找一下相关资料
var p = new ServiceProfile("ipfs1", "_ipfs-discovery._udp", 5010);
p.AddProperty("connstr", "Server");
//必须要设置这一项,否则不解析TXT记录
sd.AnswersContainsAdditionalRecords = true;
sd.Advertise(p);
//sd.Announce(p);
Console.ReadKey();
sd.Unadvertise();

 

 

总结

最好有一定的DNS的了解才会更深入理解这个过程。由于这个服务使用5353端口,我想着是不是在同一台计算机上同时运行server和client会报错误来着。实际上可以正常运行,我本机上运行之后显示了很多docker虚拟出来的网卡地址,如果是不同计算机的话,就只显示同一网络的那个地址了,如果添加网段比较的话,就能变相获得本机确实可用的网络地址了。

source:https://www.cnblogs.com/podolski/p/14137404.html

参考资料

 

调试记录

https://www.cisco.com/c/en/us/support/docs/wireless/wireless-lan-controller-software/210835-Troubleshooting-mDNS.html

 

 

https://www.cnblogs.com/podolski/p/14137404.html

https://www.cnblogs.com/chengeng/p/17851665.html

 

以下仅仅利用UDP的组播的简单实现。如果你的产品是商业级的,建议使用更为通用的协议SSDP,mDNS,Bonjour等等

发送多播地址源码 c#

https://github.com/tomasmcguinness/dotnet-homebridge/blob/master/ColdBear.mDNS/mDNSService.cs

https://github.com/tomasmcguinness/dotnet-mdns/blob/master/ColdBear.mDNS/mDNSService.cs

 

blog

Apple Bonjour for .Net Core

https://tomasmcguinness.com/2018/01/31/run-my-mdns-service-on-windows-iot/

https://tomasmcguinness.com/2018/01/29/apple-bonjour-for-net-core-part-3/?relatedposts_hit=1&relatedposts_origin=1521&relatedposts_position=1

https://www.cnblogs.com/object-jw/p/12843465.html

https://www.cnblogs.com/xueyk/articles/mDNS.html

https://busynose.gitee.io/blog/post/multicast/