Golang DNS解析
我们平时都会使用func Dial(network, address string) (Conn, error)
去创建一个连接,包括golang的httpclient也是调用这样一个函数去创建连接。这个方法里使用了net.Dialer
net.Dialer
type Dialer struct {
Timeout time.Duration //连接超时
Deadline time.Time
LocalAddr Addr
DualStack bool
FallbackDelay time.Duration
KeepAlive time.Duration
Resolver *Resolver //DNS解析器
Cancel <-chan struct{}
Control func(network, address string, c syscall.RawConn) error
}
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
Dialer里有一个DialContext
方法,它先使用Resolver *Resolver
解析DNS,然后在创建连接。
Resolver *Resolver
是一个struct,不是个interface
type Resolver struct {
PreferGo bool
StrictErrors bool
Dial func(ctx context.Context, network, address string) (Conn, error) //创建到nameserver的连接
}
Resolver的工作流程如下:
使用cgo还是purego去解析
在图中步骤4中,会计算出解析方式的顺序,顺序有5种。分为两类
var lookupOrderName = map[hostLookupOrder]string{
hostLookupCgo: "cgo",
hostLookupFilesDNS: "files,dns",
hostLookupDNSFiles: "dns,files",
hostLookupFiles: "files",
hostLookupDNS: "dns",
}
其中hostLookupCgo
是一类,表示直接调用libc的getaddrinfo方法去解析。
其它四个是另一类,表示go去读取文件/etc/hosts和/etc/resolv.conf去解析,files表示先看看/etc/hosts有没有对应的记录,dns表示通过/etc/resolv.conf的server去解析。四个指定了不同的解析方式顺序
这个顺序计算是依赖平台类型,/etc/nsswitch.conf
等的。比如goos为darwin和android的平台都是用cgo解析,这可能是因为这两个平台上找不到/etc/hosts和/etc/resolv.conf文件吧。然后先files还是先dns的关系是使用/etc/nsswitch.conf的配置的。对应linux都是使用purego去解析的。
如果你确定你机器上有/etc/hosts和/etc/resolv.conf这两个文件,而且格式正确,应用程序有访问权限,那个你可以设置Resovler.PreferGo=true,强制使用purego。
DNS缓存
但你通过net.Dial或者net.DialContext去创建连接,或者使用Resolver去解析DNS时,都是没有缓存的。这个意思就是说你每创建一个连接,Resolver都回去解析一次。那我们肯定需要一个dns缓存,如果dns server挂掉了不会影响到我们呀。
但有个特别坑爹的地方, Resolver是个struct,不是个interface,这样我们就没办法直接实现一个Resolver,然后替换掉Dialer里的Resolver。
还有个坑爹地方,我们没法调用Resolver.resolverAddrList
方法,因为这是个内部方法。只能调用图中3步骤的Resolver.LookupIPAddr
, 这个方法你只能给一个host,不能带port,也不能是unxi socket,不然报错。
下面我自己做了个DialContext, 这个方法可以用来设置到http.Transport等地方。代码中的dialer.Dialer.DialContext
就是net库下的Dialer.DialContext
func (dialer *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ips, _ := dialer.Resolver.Get(ctx, host)//这里自己实现了一个带缓存的Resolver,但是这个Resolver没有识别unix socket的功能,如果host里有port也不能识别,所以host不能带port
for _, ip := range ips {
conn, err := dialer.Dialer.DialContext(ctx, network, ip+":"+port)//这里我们已经解析出来了ip和port,那么net.Dialer判断出来是个ip就不会再去解析了
if err == nil {
return conn, nil
}
}
return dialer.Dialer.DialContext(ctx, network, address) 如果前面解析失败了就老老实实用原address去调用吧,可能address是个unix socket呢。这里是个兜底,前面都失败了那么我们还可以用原来的方式去做
}
带缓存的Resolver,很简单,就是定时去解析一下,你也可以实现一个带有过期时间的缓存。
type Resolver struct {
lock sync.RWMutex
cache map[string][]string
ResolverTimeout time.Duration
}
func NewResolver(refreshRate time.Duration) *Resolver {
resolver := &Resolver{
cache: make(map[string][]string, 64),
ResolverTimeout: 30 * time.Second,
}
if refreshRate > 0 {
go resolver.autoRefresh(refreshRate)
}
return resolver
}
func (r *Resolver) Get(ctx context.Context, host string) ([]string, error) {
r.lock.RLock()
ips, exists := r.cache[host]
r.lock.RUnlock()
if exists {
return ips, nil
}
return r.Lookup(ctx, host)
}
func (r *Resolver) Refresh() {
i := 0
r.lock.RLock()
addresses := make([]string, len(r.cache))
for key, _ := range r.cache {
addresses[i] = key
i++
}
r.lock.RUnlock()
for _, host := range addresses {
ctx, _ := context.WithTimeout(context.Background(), r.ResolverTimeout)
r.Lookup(ctx, host)
time.Sleep(time.Second)
}
}
func (r *Resolver) Lookup(ctx context.Context, host string) ([]string, error) {
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) //调用默认的resolver
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, nil
}
strIPs := make([]string, len(ips))
for index, ip := range ips{
strIPs[index] = ip.String()
}
r.lock.Lock()
r.cache[host] = strIPs
r.lock.Unlock()
return strIPs, nil
}
func (r *Resolver) autoRefresh(rate time.Duration) {
for {
time.Sleep(rate)
r.Refresh()
}
}
go的Dialer和Resover需要改进的地方
现在的Dialer和Resover太过于耦合了
- Dialer的Resolver是个struct, 我要实现我自己的Resolver呢
- Resover的resolveAddrList、internetAddrList方法里包含了对addr检查和过滤的逻辑,但是没有暴露给用户。比如是否为unxi检查,是否有端口的检查,还有一些地址的校验逻辑。