hashicorp/mdns 介绍与源代码分析

mDNS

mDNS 即组播 DNS (multicast DNS)

主要实现了零配置,在局域网内主机间的相互发现

苹果的 Bonjour 就是一个基于 mDNS 的产品

hashicorp/mdns

一个 golang 版本,基于组播 DNS 信息,来实现服务发现的开源库

github 地址: https://github.com/hashicorp/mdns

没有实现 mDNS 的所有协议,且无法与 Bonjour 的产品相互发现

但是作为小规模局域网内服务发现功能,已足够用

该库主要目的用于对开发时、演示 demo 时,提供便利。请勿用于生产环境

实现思路

hashicorp/mdns 实现思路很简单,就 1 个协议,见下图

+--------------+                                2. udp reply dns info
|              |
|    Client    +<-----------------------------------------+--------------------+--------------------------+
|              |                                          ^                    ^                          ^
+------+-------+                                          |                    |                          |
       |                                                  |                    |                          |
       |                                                  |                    |                          |
       |                                          +-------+-------+   +--------+------+          +--------+------+
       |                                          |               |   |               |          |               |
       |                                          |    Server1    |   |    Server2    |   ...    |    ServerN    |
       |                                          |               |   |               |          |               |
       |                                          +-------+-------+   +--------+------+          +--------+------+
       |                                                  ^                    ^                          ^
       |     1. udp multicast requset dns info            |                    |                          |
       |                                                  |                    |                          |
       +--------------------------------------------------+--------------------+--------------------------+
  • 客户端 udp 组播 requset dns info 给整个局域网
  • 服务器 udp 监听组播地址 (224.0.0.251:5353),并应答来至客户端 requset dns info 请求

源代码分析

1. 目录文件介绍
.
├── LICENSE
├── README.md
├── client.go                     // 客户端 API
├── go.mod
├── go.sum
├── server.go                     // 服务器 API
├── server_test.go
├── zone.go                       // DNS 协议相关
└── zone_test.go
2. server.go 代码分析
  • 监听组播地址 (224.0.0.251:5353)

    // NewServer is used to create a new mDNS server from a config
    func NewServer(config *Config) (*Server, error) {
    	// Create the listeners
    	ipv4List, _ := net.ListenMulticastUDP("udp4", config.Iface, ipv4Addr)	// 监听地址
    	ipv6List, _ := net.ListenMulticastUDP("udp6", config.Iface, ipv6Addr)
    
    	// Check if we have any listener
    	if ipv4List == nil && ipv6List == nil {
    		return nil, fmt.Errorf("No multicast listeners could be started")
    	}
    
    	s := &Server{
    		config:     config,
    		ipv4List:   ipv4List,
    		ipv6List:   ipv6List,
    		shutdownCh: make(chan struct{}),
    	}
    
    	// 接收消息协程
    	if ipv4List != nil {
    		go s.recv(s.ipv4List)
    	}
    
    	if ipv6List != nil {
    		go s.recv(s.ipv6List)
    	}
    
    	return s, nil
    }
    

    学渣同学(比如我)可能会在这里有疑问:多个进程都监听同一个地址组播地址 (224.0.0.251:5353),不会冲突吗 = =|

  • 接收消息处理
    代码都是非常简单,摘录最重要的

    func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) {
    	records := s.config.Zone.Records(q)
    	// 细节代码略
    	return records, nil
    }
    

    从 s.recv 协程开始,每个消息处理,主要在 handleQuestion 函数中的 s.config.Zone.Records(q) 内,对 DNS 协议做解析,并组装 DNS 协议应答

3. zone.go

文件命名有点奇怪,实际上就是 DNS 协议解析处理并应答(且真正只有 1 个协议处理)

本文件中的 MDNSService 结构体,就是本服务信息,最终会组装成 DNS 信息发给客户端

剩余的就是DNS 协议做解析,并组装 DNS 协议应答,代码如下:

// Records returns DNS records in response to a DNS question.
func (m *MDNSService) Records(q dns.Question) []dns.RR {
	switch q.Name {
	case m.enumAddr:
		return m.serviceEnum(q)
	case m.serviceAddr:	// 只有这个分支,客户端会发。其他分支内容, m.serviceRecords(q) 内都会组装好发给客户端
		return m.serviceRecords(q)
	case m.instanceAddr:
		return m.instanceRecords(q)
	case m.HostName:
		if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA {
			return m.instanceRecords(q)
		}
		fallthrough
	default:
		return nil
	}
}
func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR {
	// 内容太多略

	// m.serviceRecord(q) 内部会调用本函数
	// 本函数会递归调用多次,把消息组装好
	// 实际上本函数所有 switch 内所有分支内容都会被递归调用到。把所有 DNS 信息组装好
}
3. client.go
  • 建立到组播地址 (224.0.0.251:5353)的连接(虚)

    // NewClient creates a new mdns Client that can be used to query
    // for records
    func newClient() (*client, error) {
    	// TODO(reddaly): At least attempt to bind to the port required in the spec.
    	// Create a IPv4 listener
    	uconn4, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
    	if err != nil {
    		log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
    	}
    	uconn6, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0})
    	if err != nil {
    		log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err)
    	}
    
    	if uconn4 == nil && uconn6 == nil {
    		return nil, fmt.Errorf("failed to bind to any unicast udp port")
    	}
    
    	mconn4, err := net.ListenMulticastUDP("udp4", nil, ipv4Addr)
    	if err != nil {
    		log.Printf("[ERR] mdns: Failed to bind to udp4 port: %v", err)
    	}
    	mconn6, err := net.ListenMulticastUDP("udp6", nil, ipv6Addr)
    	if err != nil {
    		log.Printf("[ERR] mdns: Failed to bind to udp6 port: %v", err)
    	}
    
    	if mconn4 == nil && mconn6 == nil {
    		return nil, fmt.Errorf("failed to bind to any multicast udp port")
    	}
    
    	c := &client{
    		ipv4MulticastConn: mconn4,
    		ipv6MulticastConn: mconn6,
    		ipv4UnicastConn:   uconn4,
    		ipv6UnicastConn:   uconn6,
    		closedCh:          make(chan struct{}),
    	}
    	return c, nil
    }
    

    学渣同学(比如我)可能会在这里有疑问:建立组播地址连接,也用 net.ListenMulticastUDP 吗 = =|

  • 请求局域网内某服务的 DNS 信息

    func (c *client) query(params *QueryParam) error {
    	// 代码略
    }
    
  • 接收服务器应答处理

    func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) {
    	// 代码略
    }
    

server.go zone.go 看懂后, client.go 就相对简单了

需要注意的一点是:query 内部会开启 recv 协程,这里该库应该做下优化, newClient 函数内开启一根常驻 recv 协程更好

其他

mDNS 可作为服务发现的一个补充,在 demo 演示 、开发环境,会对服务器端带来便利

前提是你的服务发现功能要能插件化,方便生产环境中使用诸如 etcd 、 consule 这种比较稳定的产品

发布了129 篇原创文章 · 获赞 73 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/u013272009/article/details/97546884