refactor: simplify client initialization and enhance error handling
Kim committed
Oct 21, 2025 at 12:06 UTC
5fca4cb03c8e8d74056a5593c5347b47e327bdd1
3 files changed
+66
-46
README.md
+2
-3
@@ -90,19 +90,18 @@ import (
90
"context"
91
"time"
92
"github.com/gosuda/relaydns/relaydns"
93
- "github.com/libp2p/go-libp2p"
93
)
94
95
func main() {
96
ctx := context.Background()
98
- h, _ := libp2p.New(libp2p.EnableHolePunching(), libp2p.EnableNATService())
99
- client, _ := relaydns.NewClient(ctx, h, relaydns.ClientConfig{
97
+ client, _ := relaydns.NewClient(ctx, relaydns.ClientConfig{
98
Protocol: "/relaydns/http/1.0",
99
Topic: "relaydns.backends",
100
AdvertiseEvery: 5 * time.Second,
101
TargetTCP: "127.0.0.1:8081",
102
Name: "demo-http",
103
})
104
+ _ = client.Start(ctx)
105
defer client.Close()
106
select {}
107
}
cmd/example_client/main.go
+4
-14
@@ -72,12 +72,7 @@ func runClient(cmd *cobra.Command, args []string) error {
72
}()
73
74
// 2) libp2p host
75
- h, err := relaydns.MakeHost(ctx, 0, true)
76
- if err != nil {
77
- return fmt.Errorf("make host: %w", err)
78
- }
79
-
80
- client, err := relaydns.NewClient(ctx, h, relaydns.ClientConfig{
75
+ client, err := relaydns.NewClient(ctx, relaydns.ClientConfig{
76
Protocol: "/relaydns/http/1.0",
77
Topic: "relaydns.backends",
78
AdvertiseEvery: 3 * time.Second,
@@ -92,15 +87,10 @@ func runClient(cmd *cobra.Command, args []string) error {
87
if err != nil {
88
return fmt.Errorf("new client: %w", err)
89
}
95
- defer client.Close()
96
-
97
- if addrs := h.Addrs(); len(addrs) > 0 {
98
- for _, a := range addrs {
99
- log.Info().Msgf("[client] host addr: %s/p2p/%s", a.String(), h.ID().String())
100
- }
101
- } else {
102
- log.Info().Msgf("[client] host peer: %s (no listen addrs yet)", h.ID().String())
90
+ if err := client.Start(ctx); err != nil {
91
+ return fmt.Errorf("start client: %w", err)
92
}
93
+ defer client.Close()
94
95
// wait for termination
96
sig := make(chan os.Signal, 1)
relaydns/client.go
+60
-29
@@ -56,51 +56,69 @@ type RelayClient struct {
56
stop context.CancelFunc
57
}
58
59
-// NewClient wires a reusable backend node that other apps can embed.
60
-// It registers a stream handler and starts an advertiser loop.
61
-// Call Close() to stop.
62
-func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient, error) {
59
+// NewClient constructs a client with defaults applied and an initialized libp2p host.
60
+// It does not start networking. Call Start(ctx) to begin handlers, pubsub, and advertising.
61
+func NewClient(ctx context.Context, cfg ClientConfig) (*RelayClient, error) {
62
if cfg.AdvertiseEvery <= 0 {
63
cfg.AdvertiseEvery = 5 * time.Second
64
}
65
if cfg.HTTPTimeout <= 0 {
66
cfg.HTTPTimeout = 3 * time.Second
67
}
68
+ if cfg.Protocol == "" {
69
+ cfg.Protocol = "/relaydns/http/1.0"
70
+ }
71
+ if cfg.Topic == "" {
72
+ cfg.Topic = "relaydns.backends"
73
+ }
74
+
75
+ h, err := MakeHost(ctx, 0, true)
76
+ if err != nil {
77
+ return nil, fmt.Errorf("make host: %w", err)
78
+ }
79
b := &RelayClient{
80
h: h,
81
cfg: cfg,
82
protoID: protocol.ID(cfg.Protocol),
83
}
84
+ return b, nil
85
+}
86
75
- boot := make([]string, 0, len(cfg.Bootstraps)+4)
76
- if len(cfg.Bootstraps) > 0 {
77
- boot = append(boot, cfg.Bootstraps...)
87
+// Start connects bootstraps, sets stream handler, joins pubsub, and starts advertising.
88
+func (b *RelayClient) Start(ctx context.Context) error {
89
+ if b.stop != nil {
90
+ return fmt.Errorf("client already started")
91
}
79
- if cfg.ServerURL != "" {
80
- if addrs, err := fetchMultiaddrsFromHealth(cfg.ServerURL, cfg.HTTPTimeout); err != nil {
81
- log.Warn().Err(err).Msgf("relaydns: fetch /health from %s failed", cfg.ServerURL)
92
+ // resolve bootstraps (from flags and optional server health)
93
+ boot := make([]string, 0, len(b.cfg.Bootstraps)+4)
94
+ if len(b.cfg.Bootstraps) > 0 {
95
+ boot = append(boot, b.cfg.Bootstraps...)
96
+ }
97
+ if b.cfg.ServerURL != "" {
98
+ if addrs, err := fetchMultiaddrsFromHealth(b.cfg.ServerURL, b.cfg.HTTPTimeout); err != nil {
99
+ log.Warn().Err(err).Msgf("relaydns: fetch /health from %s failed", b.cfg.ServerURL)
100
} else {
83
- sortMultiaddrs(addrs, cfg.PreferQUIC, cfg.PreferLocal)
101
+ sortMultiaddrs(addrs, b.cfg.PreferQUIC, b.cfg.PreferLocal)
102
boot = append(boot, addrs...)
103
}
104
}
105
boot = uniq(boot)
106
if len(boot) > 0 {
89
- ConnectBootstraps(ctx, h, boot)
107
+ ConnectBootstraps(ctx, b.h, boot)
108
} else {
109
log.Warn().Msg("relaydns: no bootstrap sources provided (Bootstraps/ServerURL); discovery may fail")
110
}
111
94
- // 1) stream handler
112
+ // stream handler
113
switch {
96
- case cfg.Handler != nil:
97
- h.SetStreamHandler(b.protoID, cfg.Handler)
98
- case cfg.TargetTCP != "":
99
- h.SetStreamHandler(b.protoID, func(s network.Stream) {
114
+ case b.cfg.Handler != nil:
115
+ b.h.SetStreamHandler(b.protoID, b.cfg.Handler)
116
+ case b.cfg.TargetTCP != "":
117
+ b.h.SetStreamHandler(b.protoID, func(s network.Stream) {
118
defer s.Close()
101
- c, err := net.Dial("tcp", cfg.TargetTCP)
119
+ c, err := net.Dial("tcp", b.cfg.TargetTCP)
120
if err != nil {
103
- log.Error().Err(err).Msgf("relaydns: dial %s", cfg.TargetTCP)
121
+ log.Error().Err(err).Msgf("relaydns: dial %s", b.cfg.TargetTCP)
122
return
123
}
124
defer c.Close()
@@ -109,27 +127,27 @@ func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient
127
io.Copy(s, c)
128
})
129
default:
112
- return nil, fmt.Errorf("relaydns: either Handler or TargetTCP must be set")
130
+ return fmt.Errorf("relaydns: either Handler or TargetTCP must be set")
131
}
132
115
- // 2) pubsub join
116
- ps, err := pubsub.NewGossipSub(ctx, h, pubsub.WithMessageSigning(true))
133
+ // pubsub join
134
+ ps, err := pubsub.NewGossipSub(ctx, b.h, pubsub.WithMessageSigning(true))
135
if err != nil {
118
- return nil, err
136
+ return err
137
}
120
- t, err := ps.Join(cfg.Topic)
138
+ t, err := ps.Join(b.cfg.Topic)
139
if err != nil {
122
- return nil, err
140
+ return err
141
}
142
b.ps, b.t = ps, t
143
126
- // 3) advertiser loop
144
+ // advertiser loop
145
advCtx, cancel := context.WithCancel(ctx)
146
b.stop = cancel
147
b.wg.Add(1)
148
go func() {
149
defer b.wg.Done()
132
- ticker := time.NewTicker(cfg.AdvertiseEvery)
150
+ ticker := time.NewTicker(b.cfg.AdvertiseEvery)
151
defer ticker.Stop()
152
for {
153
select {
@@ -156,7 +174,19 @@ func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient
174
}
175
}()
176
159
- return b, nil
177
+ if addrs := b.Host().Addrs(); len(addrs) > 0 {
178
+ for _, a := range addrs {
179
+ log.Info().Msgf("[client] host addr: %s/p2p/%s", a.String(), b.Host().ID().String())
180
+ }
181
+ } else {
182
+ log.Info().Msgf("[client] host peer: %s (no listen addrs yet)", b.Host().ID().String())
183
+ }
184
+
185
+ return nil
186
+}
187
+
188
+func (b *RelayClient) Host() host.Host {
189
+ return b.h
190
}
191
192
func (b *RelayClient) Close() error {
@@ -167,6 +197,7 @@ func (b *RelayClient) Close() error {
197
// leaving topic is optional; libp2p will clean up on host close
198
return nil
199
}
200
+
201
func fetchMultiaddrsFromHealth(base string, timeout time.Duration) ([]string, error) {
202
u, err := url.Parse(base)
203
if err != nil {
@@ -201,7 +232,7 @@ func fetchMultiaddrsFromHealth(base string, timeout time.Duration) ([]string, er
232
}
233
addrs := make([]string, 0, len(payload.Multiaddrs))
234
for _, s := range payload.Multiaddrs {
204
- // 아주 기본적인 sanity check
235
+ // sanity check
236
if strings.Contains(s, "/p2p/") && (strings.Contains(s, "/ip4/") || strings.Contains(s, "/ip6/")) {
237
addrs = append(addrs, s)
238
}