diff --git a/README.md b/README.md index 6d0442ec..f93d9a25 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,6 @@ This package requires **Go 1.7** (context in std lib) or later. ## Browse for services in your local network ```go -// Discover all services on the network (e.g. _workstation._tcp) -resolver, err := zeroconf.NewResolver(nil) -if err != nil { - log.Fatalln("Failed to initialize resolver:", err.Error()) -} - entries := make(chan *zeroconf.ServiceEntry) go func(results <-chan *zeroconf.ServiceEntry) { for entry := range results { @@ -45,6 +39,7 @@ go func(results <-chan *zeroconf.ServiceEntry) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() +// Discover all services on the network (e.g. _workstation._tcp) err = resolver.Browse(ctx, "_workstation._tcp", "local.", entries) if err != nil { log.Fatalln("Failed to browse:", err.Error()) diff --git a/client.go b/client.go index 270394ab..a6787cd3 100644 --- a/client.go +++ b/client.go @@ -26,6 +26,13 @@ const ( IPv4AndIPv6 = (IPv4 | IPv6) //< Default option. ) +// Client structure encapsulates both IPv4/IPv6 UDP connections. +type client struct { + ipv4conn *ipv4.PacketConn + ipv6conn *ipv6.PacketConn + ifaces []net.Interface +} + type clientOpts struct { listenOn IPType ifaces []net.Interface @@ -52,85 +59,63 @@ func SelectIfaces(ifaces []net.Interface) ClientOption { } } -// Resolver acts as entry point for service lookups and to browse the DNS-SD. -type Resolver struct { - c *client -} - -// NewResolver creates a new resolver and joins the UDP multicast groups to -// listen for mDNS messages. -func NewResolver(options ...ClientOption) (*Resolver, error) { - // Apply default configuration and load supplied options. - var conf = clientOpts{ - listenOn: IPv4AndIPv6, - } - for _, o := range options { - if o != nil { - o(&conf) - } - } - - c, err := newClient(conf) +// Browse for all services of a given type in a given domain. +func Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry, opts ...ClientOption) error { + cl, err := newClient(applyOpts(opts...)) if err != nil { - return nil, err + return err } - return &Resolver{ - c: c, - }, nil -} - -// Browse for all services of a given type in a given domain. -func (r *Resolver) Browse(ctx context.Context, service, domain string, entries chan<- *ServiceEntry) error { params := defaultParams(service) if domain != "" { params.Domain = domain } params.Entries = entries params.isBrowsing = true - ctx, cancel := context.WithCancel(ctx) - go r.c.mainloop(ctx, params) + return cl.run(ctx, params) +} - err := r.c.query(params) +// Lookup a specific service by its name and type in a given domain. +func Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry, opts ...ClientOption) error { + cl, err := newClient(applyOpts(opts...)) if err != nil { - cancel() return err } - // If previous probe was ok, it should be fine now. In case of an error later on, - // the entries' queue is closed. - go func() { - if err := r.c.periodicQuery(ctx, params); err != nil { - cancel() - } - }() - - return nil -} - -// Lookup a specific service by its name and type in a given domain. -func (r *Resolver) Lookup(ctx context.Context, instance, service, domain string, entries chan<- *ServiceEntry) error { params := defaultParams(service) params.Instance = instance if domain != "" { params.Domain = domain } params.Entries = entries - ctx, cancel := context.WithCancel(ctx) - go r.c.mainloop(ctx, params) - err := r.c.query(params) - if err != nil { - // cancel mainloop - cancel() - return err + return cl.run(ctx, params) +} + +func applyOpts(options ...ClientOption) clientOpts { + // Apply default configuration and load supplied options. + var conf = clientOpts{ + listenOn: IPv4AndIPv6, } - // If previous probe was ok, it should be fine now. In case of an error later on, - // the entries' queue is closed. - go func() { - if err := r.c.periodicQuery(ctx, params); err != nil { - cancel() + for _, o := range options { + if o != nil { + o(&conf) } + } + return conf +} + +func (c *client) run(ctx context.Context, params *lookupParams) error { + ctx, cancel := context.WithCancel(ctx) + done := make(chan struct{}) + go func() { + defer close(done) + c.mainloop(ctx, params) }() - return nil + // If previous probe was ok, it should be fine now. In case of an error later on, + // the entries' queue is closed. + err := c.periodicQuery(ctx, params) + cancel() + <-done + return err } // defaultParams returns a default set of QueryParams. @@ -138,13 +123,6 @@ func defaultParams(service string) *lookupParams { return newLookupParams("", service, "local", false, make(chan *ServiceEntry)) } -// Client structure encapsulates both IPv4/IPv6 UDP connections. -type client struct { - ipv4conn *ipv4.PacketConn - ipv6conn *ipv6.PacketConn - ifaces []net.Interface -} - // Client structure constructor func newClient(opts clientOpts) (*client, error) { ifaces := opts.ifaces @@ -381,6 +359,10 @@ func (c *client) periodicQuery(ctx context.Context, params *lookupParams) error } }() for { + // Do periodic query. + if err := c.query(params); err != nil { + return err + } // Backoff and cancel logic. wait := bo.NextBackOff() if wait == backoff.Stop { @@ -391,6 +373,7 @@ func (c *client) periodicQuery(ctx context.Context, params *lookupParams) error } else { timer.Reset(wait) } + select { case <-timer.C: // Wait for next iteration. @@ -399,12 +382,11 @@ func (c *client) periodicQuery(ctx context.Context, params *lookupParams) error // Done here. Received a matching mDNS entry. return nil case <-ctx.Done(): + if params.isBrowsing { + return nil + } return ctx.Err() } - // Do periodic query. - if err := c.query(params); err != nil { - return err - } } } @@ -428,11 +410,7 @@ func (c *client) query(params *lookupParams) error { m.SetQuestion(serviceName, dns.TypePTR) } m.RecursionDesired = false - if err := c.sendQuery(m); err != nil { - return err - } - - return nil + return c.sendQuery(m) } // Pack the dns.Msg and write to available connections (multicast) diff --git a/examples/resolv/client.go b/examples/resolv/client.go index 83186bc4..45332306 100644 --- a/examples/resolv/client.go +++ b/examples/resolv/client.go @@ -18,12 +18,6 @@ var ( func main() { flag.Parse() - // Discover all services on the network (e.g. _workstation._tcp) - resolver, err := zeroconf.NewResolver(nil) - if err != nil { - log.Fatalln("Failed to initialize resolver:", err.Error()) - } - entries := make(chan *zeroconf.ServiceEntry) go func(results <-chan *zeroconf.ServiceEntry) { for entry := range results { @@ -34,7 +28,8 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(*waitTime)) defer cancel() - err = resolver.Browse(ctx, *service, *domain, entries) + // Discover all services on the network (e.g. _workstation._tcp) + err := zeroconf.Browse(ctx, *service, *domain, entries) if err != nil { log.Fatalln("Failed to browse:", err.Error()) } diff --git a/service_test.go b/service_test.go index 2c5a23ed..f45d83de 100644 --- a/service_test.go +++ b/service_test.go @@ -40,12 +40,8 @@ func TestBasic(t *testing.T) { time.Sleep(time.Second) - resolver, err := NewResolver(nil) - if err != nil { - t.Fatalf("Expected create resolver success, but got %v", err) - } entries := make(chan *ServiceEntry, 100) - if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { + if err := Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { t.Fatalf("Expected browse success, but got %v", err) } <-ctx.Done() @@ -69,11 +65,6 @@ func TestBasic(t *testing.T) { } func TestNoRegister(t *testing.T) { - resolver, err := NewResolver(nil) - if err != nil { - t.Fatalf("Expected create resolver success, but got %v", err) - } - // before register, mdns resolve shuold not have any entry entries := make(chan *ServiceEntry) go func(results <-chan *ServiceEntry) { @@ -84,7 +75,7 @@ func TestNoRegister(t *testing.T) { }(entries) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { + if err := Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { t.Fatalf("Expected browse success, but got %v", err) } <-ctx.Done() @@ -100,12 +91,8 @@ func TestSubtype(t *testing.T) { time.Sleep(time.Second) - resolver, err := NewResolver(nil) - if err != nil { - t.Fatalf("Expected create resolver success, but got %v", err) - } entries := make(chan *ServiceEntry, 100) - if err := resolver.Browse(ctx, mdnsSubtype, mdnsDomain, entries); err != nil { + if err := Browse(ctx, mdnsSubtype, mdnsDomain, entries); err != nil { t.Fatalf("Expected browse success, but got %v", err) } <-ctx.Done() @@ -136,12 +123,8 @@ func TestSubtype(t *testing.T) { time.Sleep(time.Second) - resolver, err := NewResolver(nil) - if err != nil { - t.Fatalf("Expected create resolver success, but got %v", err) - } entries := make(chan *ServiceEntry, 100) - if err := resolver.Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { + if err := Browse(ctx, mdnsService, mdnsDomain, entries); err != nil { t.Fatalf("Expected browse success, but got %v", err) } <-ctx.Done()