完整的示例代码可以参考前面的文章go-grpc客户端调用.
在调用grpc.NewClient方法的时候会进行地址的解析.
编辑
initParsedTargetAndResolverBuilder方法:
1.解析原始target:
编辑
parseTarget方法:
编辑
Target结构体和实现方法:
编辑
getResolver方法:
编辑
编辑
2.① parseTarget 解析失败;② scheme 没有注册对应的 resolver .需要回退使用默认 scheme,拼接成 canonicalTarget:
编辑
编辑
resolver.Builder就已经找到了.
stream.go的newClientStream方法:
OnCallBegin方法:
ExitIdleMode方法:
func (m *Manager) ExitIdleMode() {
// idleMu 保证和 tryEnterIdleMode(进入空闲的逻辑)互斥
m.idleMu.Lock()
defer m.idleMu.Unlock()
// 已经关闭 或者 本身就不在空闲状态,直接返回(幂等保护)
if m.isClosed() || !m.actuallyIdle {
// 注释里描述3种并发场景:
// 1. 定时器触发进入idle,还没拿到锁,新RPC进来抢先调用ExitIdleMode
// 2. 空闲状态,多个RPC并发同时触发OnCallBegin,抢锁,第一个成功,后面的进来发现已经退出idle直接return
// 3. 通道本来就非空闲,用户手动调用 cc.Connect() 触发ExitIdleMode
return
}
// 调用 ClientConn 的 ExitIdleMode,真正唤醒连接链路【重点】
m.cc.ExitIdleMode()
// 撤销进入idle时对 activeCallsCount 的修改,恢复计数状态
atomic.AddInt32(&m.activeCallsCount, math.MaxInt32)
m.actuallyIdle = false
// 重置空闲计时器,重新开始计时:从现在开始多久无RPC就再次进入idle
m.resetIdleTimerLocked(m.timeout)
}
exitIdleMode方法:
1.修改状态:
2.构建resolver,resolver 可能同步更新状态、上报错误:
3.start方法:
4.build方法:
func (b *dnsBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
// 1. 从 target.Endpoint() 解析 host、port,没有端口用 defaultPort
host, port, err := parseTarget(target.Endpoint(), defaultPort)
if err != nil {
return nil, err
}
// 2. 如果 host 本身就是IP地址(IPv4/IPv6)
if ipAddr, err := formatIP(host); err == nil {
addr := []resolver.Address{{Addr: ipAddr + ":" + port}}
// 直接回调 UpdateState,把IP地址返回给 ClientConn
cc.UpdateState(resolver.State{
Addresses: addr,
Endpoints: []resolver.Endpoint{{Addresses: addr}},
})
// 返回 deadResolver:空实现,不需要后台协程监听,不需要ResolveNow
return deadResolver{}, nil
}
// 3. 域名场景(非IP,需要做DNS查询)
ctx, cancel := context.WithCancel(context.Background())
d := &dnsResolver{
host: host,
port: port,
ctx: ctx,
cancel: cancel,
cc: cc, // resolver.ClientConn,就是ccResolverWrapper
rn: make(chan struct{}, 1), // ResolveNow信号通道,缓冲1
enableServiceConfig: envconfig.EnableTXTServiceConfig && !opts.DisableServiceConfig,
}
// 创建底层DNS解析器对象
d.resolver, err = internal.NewNetResolver(target.URL.Host)
if err != nil {
return nil, err
}
// 启动后台 watcher 协程,做DNS轮询解析
d.wg.Add(1)
go d.watcher()
return d, nil
}
5.watch方法:
func (d *dnsResolver) watcher() {
defer d.wg.Done()
backoffIndex := 1
for {
// 执行一次完整DNS查询(A/AAAA + TXT service‑config)
state, err := d.lookup()
if err != nil {
// DNS查询出错,向上报告错误给ClientConn
d.cc.ReportError(err)
} else {
// 查询成功,把地址、service‑config回调上层
err = d.cc.UpdateState(*state)
}
var nextResolutionTime time.Time
if err == nil {
// ✅ 查询成功分支
backoffIndex = 1 // 成功重置退避计数器
// MinResolutionInterval:最小重解析间隔,默认30s,防止频繁刷屏解析
nextResolutionTime = internal.TimeNowFunc().Add(MinResolutionInterval)
select {
case <-d.ctx.Done():
return // resolver被Close,退出协程
case <-d.rn: // 等待外部 ResolveNow() 触发信号
}
} else {
// ❌ 查询失败分支:DNS出错 或者 UpdateState返回错误
// 指数退避计算下次重试时间
nextResolutionTime = internal.TimeNowFunc().Add(backoff.DefaultExponential.Backoff(backoffIndex))
backoffIndex++
}
// 等待:到下次解析时间,或者resolver被关闭
select {
case <-d.ctx.Done():
return
case <-internal.TimeAfterFunc(internal.TimeUntilFunc(nextResolutionTime)):
}
}
}
6.lookup方法:
func (d *dnsResolver) lookup() (*resolver.State, error) {
// 设置单次DNS查询超时 ResolvingTimeout(通常默认5s)
ctx, cancel := context.WithTimeout(d.ctx, ResolvingTimeout)
defer cancel()
// 1. 查询 DNS SRV 记录(用于grpclb负载均衡)
srv, srvErr := d.lookupSRV(ctx)
// 2. 查询 A/AAAA 记录,拿到后端业务IP地址
addrs, hostErr := d.lookupHost(ctx)
// 错误判断:A/AAAA查询失败,并且 SRV也失败 / SRV结果为空 → 返回hostErr,本次lookup整体失败
if hostErr != nil && (srvErr != nil || len(srv) == 0) {
return nil, hostErr
}
// 把普通IP地址封装成 Endpoint 结构
eps := make([]resolver.Endpoint, 0, len(addrs))
for _, addr := range addrs {
eps = append(eps, resolver.Endpoint{Addresses: []resolver.Address{addr}})
}
// 组装基础State:业务后端地址列表
state := resolver.State{
Addresses: addrs,
Endpoints: eps,
}
// 如果拿到SRV记录,把grpclb的lb服务地址存入state自定义字段
if len(srv) > 0 {
state = grpclbstate.Set(state, &grpclbstate.State{BalancerAddresses: srv})
}
// 开启TXT service‑config时,查询TXT记录,填入ServiceConfig
if d.enableServiceConfig {
state.ServiceConfig = d.lookupTXT(ctx)
}
return &state, nil
}