使用mDNS进行局域网服务发现(.NET Core)
想要服务写的好,配置文件不可少。如果是一个复杂的系统,甚至配置文件都是需要进行动态调整的,做起来好像就不是那么方便了,通常情况下,asp.net core中的IConfiguration只能用来读取,没有提供保存功能,如果真的要操作一下,只能通过另外写方法来写入配置文件。可能是这个玩意设计就是Immutable的吧,总之,很难受。
前言
最近在做一个系统,局域网内工作基于C/S结构,一些配置项目需要从Server端发送到Client端。于是我想的第一件事情,就是给Client一个配置文件,通过Client中指定Server地址,发起通讯,并通过WebAPI,GRPC之类的东西获得数据。
貌似挺完美的,然而,这个系统的Client端是可以有很多个的,一个个配置那不是很麻烦,万一服务器地址改了...不敢想。有没有什么方法可以让Client自动发现Server,然后自动下载配置的?
方案
当然可以自己搭建一个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)。
在前面两个都用不了的情况下,只能用这个了。
实现
首先安装nuget包,这个包里面包含有server/Client端。
install-package Makaretu.Dns.Multicast
思路是这样的,基于ServiceDiscovery
发布一个服务,并将额外的信息发布到然后监听各种mDNS请求,客户端通过服务名发送查询请求,并定位服务的地址信息,然后发送SRV,A和TXT查询请求获得服务全名,IP地址和额外配置信息。这样就获得了在局域网内的服务信息了。
客户端接收的时候,使用了服务名称作为筛选的依据。
服务发布端
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();
服务调用端
static void Main(string[] args)
{
var mdns = new MulticastService();
var sd = new ServiceDiscovery(mdns);
sd.ServiceInstanceDiscovered += (s, e) =>
{
if (e.Message.Answers.All(w => !w.Name.ToString().Contains("ipfs1"))) return;
Console.WriteLine($"service instance '{e.ServiceInstanceName}'");
// Ask for the service instance details.
mdns.SendQuery(e.ServiceInstanceName, type: DnsType.SRV);
};
mdns.AnswerReceived += (s, e) =>
{
if (e.Message.Answers.All(w => !w.Name.ToString().Contains("ipfs1"))) return;
// Is this an answer to a service instance details?
var servers = e.Message.Answers.OfType<SRVRecord>();
foreach (var server in servers)
{
Console.WriteLine($"host '{server.Target}' for '{server.Name}'");
// Ask for the host IP addresses.
mdns.SendQuery(server.Target, type: DnsType.A);
//mdns.SendQuery(server.Target, type: DnsType.AAAA);
}
// Is this an answer to host addresses?
var addresses = e.Message.Answers.OfType<AddressRecord>();
foreach (var address in addresses)
{
if (address.Address.AddressFamily== AddressFamily.InterNetwork)
Console.WriteLine($"host '{address.Name}' at {address.Address}");
}
// Get connectionstring from DNS TXT record.
var txts = e.Message.Answers.OfType<TXTRecord>();
foreach (var txt in txts)
{
//“connstr=Server”,获得对应connstr值
Console.WriteLine($"{txt.Strings.Single(w => w.Contains("connstr")).Split('=')[1]}");
//Console.WriteLine($"host '{address.Name}' at {address.Address}");
}
};
try
{
mdns.Start();
sd.QueryServiceInstances("_ipfs-discovery._udp");
Console.ReadKey();
}
finally
{
sd.Dispose();
mdns.Stop();
}
}
运行效果如下:
总结
最好有一定的DNS的了解才会更深入理解这个过程。由于这个服务使用5353端口,我想着是不是在同一台计算机上同时运行server和client会报错误来着。实际上可以正常运行,我本机上运行之后显示了很多docker虚拟出来的网卡地址,如果是不同计算机的话,就只显示同一网络的那个地址了,如果添加网段比较的话,就能变相获得本机确实可用的网络地址了。
参考资料
- https://github.com/richardschneider/net-mdns
- http://www.360doc.com/content/13/0426/11/3735408_281032366.shtml
- https://www.cnblogs.com/azhqiang/p/11024705.html