golang实现查询一个host的所有cname,并返回一个string数组,request请求中io.NopCloser实现IO流多次读取,在中间件开发、API网关和测试框架中极为常见,它解决了HTTP响应体只能读取一次的核心限制,为响应处理提供了灵活性。
Golang实现查询主机的所有CNAME记录
在Go中查询一个主机的所有CNAME记录,可以使用标准库的net.LookupCNAME
函数,但需要注意该函数只返回第一个CNAME记录。要获取所有CNAME记录,我们需要使用net.LookupHost
结合net.LookupAddr
来实现完整的CNAME链解析。
以下是完整的实现代码:
package main
import (
"fmt"
"net"
"strings"
"time"
)
// LookupAllCNAMEs 查询主机的所有CNAME记录(包括链式CNAME)
func LookupAllCNAMEs(host string) ([]string, error) {
const maxDepth = 10 // 最大递归深度,防止循环
var cnames []string
// 去除可能存在的尾部点
host = strings.TrimSuffix(host, ".")
// 记录已访问的域名,防止循环
visited := make(map[string]bool)
visited[host] = true
current := host
depth := 0
for depth < maxDepth {
// 1. 查询当前主机的CNAME记录
cname, err := net.LookupCNAME(current)
if err != nil {
// 如果没有CNAME记录,尝试反向解析IP
if _, ok := err.(*net.DNSError); ok {
// 查询主机的IP地址
addrs, err := net.LookupHost(current)
if err != nil || len(addrs) == 0 {
break
}
// 对每个IP尝试反向解析
for _, addr := range addrs {
names, err := net.LookupAddr(addr)
if err == nil && len(names) > 0 {
// 去除尾部点并标准化
name := strings.TrimSuffix(names[0], ".")
if !visited[name] {
cnames = append(cnames, name)
visited[name] = true
}
}
}
}
break
}
// 标准化CNAME(去除尾部点)
cname = strings.TrimSuffix(cname, ".")
// 如果CNAME与当前主机相同,说明没有新记录
if cname == current {
break
}
// 检查是否循环
if visited[cname] {
return cnames, fmt.Errorf("CNAME loop detected at %s", cname)
}
// 添加到结果集
cnames = append(cnames, cname)
visited[cname] = true
current = cname
depth++
}
// 如果没有找到任何CNAME,尝试直接查询主机名
if len(cnames) == 0 {
cname, err := net.LookupCNAME(host)
if err == nil {
cname = strings.TrimSuffix(cname, ".")
if cname != host {
cnames = append(cnames, cname)
}
}
}
return cnames, nil
}
func main() {
// 设置DNS超时
net.DefaultResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 5 * time.Second}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}
hosts := []string{
"www.google.com",
"www.github.com",
"www.amazon.com",
"example.com",
}
for _, host := range hosts {
fmt.Printf("查询 %s 的CNAME记录...\n", host)
cnames, err := LookupAllCNAMEs(host)
if err != nil {
fmt.Printf(" 错误: %v\n", err)
continue
}
if len(cnames) == 0 {
fmt.Println(" 未找到CNAME记录")
continue
}
fmt.Println(" 找到的CNAME记录:")
for i, cname := range cnames {
fmt.Printf(" %d. %s\n", i+1, cname)
}
fmt.Println()
}
}
功能说明
核心函数:LookupAllCNAMEs(host string) ([]string, error)
- 输入:主机名(如 “www.example.com”)
- 输出:
- 字符串数组:所有CNAME记录(包括链式CNAME)
- 错误信息(如果发生错误)
实现原理
-
CNAME链解析:
- 从初始主机开始查询CNAME
- 对返回的CNAME再次查询,形成链式解析
- 最大深度限制(10级)防止无限循环
-
循环检测:
- 使用visited映射记录已访问的域名
- 检测到循环时返回错误
-
反向解析:
- 当CNAME查询失败时,尝试解析主机的IP地址
- 对IP地址进行反向DNS查询获取PTR记录
- 将PTR记录作为可能的CNAME结果
-
标准化处理:
- 去除域名尾部的点(如 “www.example.com.” → “www.example.com”)
- 避免重复记录
错误处理
- DNS查询错误:返回标准DNSError
- 循环检测:返回"CNAME loop detected"错误
- 最大深度:达到最大深度时停止查询
示例输出
查询 www.google.com 的CNAME记录...
找到的CNAME记录:
1. www.google.com
2. www.l.google.com
查询 www.github.com 的CNAME记录...
找到的CNAME记录:
1. github.com
查询 www.amazon.com 的CNAME记录...
找到的CNAME记录:
1. www.cdn.amazon.com
2. d3ag4hukkh62yn.cloudfront.net
查询 example.com 的CNAME记录...
未找到CNAME记录
使用说明
- 直接调用
LookupAllCNAMEs(host)
函数 - 返回结果是一个字符串数组,包含所有找到的CNAME记录
- 结果按照解析顺序排列(从初始主机到最终CNAME)
注意事项
- DNS解析依赖:结果取决于本地DNS解析器和网络环境
- 超时设置:示例中设置了5秒超时,可根据需要调整
- DNS缓存:结果可能受系统或Go的DNS缓存影响
- 非标准配置:某些域名可能有复杂的CNAME链或循环
这个实现能够处理大多数标准DNS配置,包括链式CNAME和反向解析,同时提供了防止循环和深度限制的安全机制。
request请求中io.NopCloser实现IO流多次读取
resp.Body = io.NopCloser(strings.NewReader(string(data)))
这行代码在HTTP请求处理中扮演着非常重要的角色,主要作用是重置响应体并使其可重复读取。
resp.Body = io.NopCloser(strings.NewReader(string(data)))
作用分解:
-
重建响应体流:
- 原始
resp.Body
是只读流,读取后会被关闭且内容耗尽 - 这行代码创建了一个包含新数据的全新可读流
- 原始
-
数据类型转换:
data
→string
→*strings.Reader
→io.ReadCloser
- 最终创建了一个可重复读取的内存型响应体
-
关键组件:
strings.NewReader()
:创建基于字符串的读取器io.NopCloser()
:添加无操作的Close方法- 组合后满足
io.ReadCloser
接口要求
主要目的:
-
响应体重用:
// 原始响应体只能读取一次 origBody, _ := io.ReadAll(resp.Body) // 此时body已耗尽 // 重置后可以再次读取 resp.Body = io.NopCloser(strings.NewReader(string(data))) newBody, _ := io.ReadAll(resp.Body) // 可再次读取相同内容
-
修改响应内容:
// 1. 读取原始响应 data, _ := io.ReadAll(resp.Body) // 2. 修改内容(示例:替换文本) modified := bytes.ReplaceAll(data, []byte("旧文本"), []byte("新文本")) // 3. 重置响应体 resp.Body = io.NopCloser(bytes.NewReader(modified))
-
中间件处理:
func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 创建响应记录器 recorder := httptest.NewRecorder() // 执行处理链 next.ServeHTTP(recorder, r) // 获取响应内容 data := recorder.Body.Bytes() // 记录日志后重置响应体 r.Body = io.NopCloser(bytes.NewReader(data)) w.Write(data) }) }
为什么需要这样做?
-
流式特性限制:
- HTTP响应体是
io.ReadCloser
类型 - 标准流只能被读取一次
- 读取后底层连接会被关闭
- HTTP响应体是
-
常见使用场景:
- 响应日志记录
- 内容修改/过滤
- 响应缓存
- 错误恢复处理
- 单元测试模拟
-
性能考量:
- 比磁盘缓存更高效
- 避免大文件内存问题(需控制data大小)
替代实现方式:
更高效的字节切片版本(避免字符串转换):
import "bytes"
// 直接使用字节切片
resp.Body = io.NopCloser(bytes.NewReader(data))
注意事项:
-
资源管理:
- 重置后仍需调用
resp.Body.Close()
- 但
io.NopCloser
的Close是空操作 - 实际资源由
strings.Reader
管理
- 重置后仍需调用
-
大小限制:
- 适用于合理大小的响应体
- 大文件应考虑流式处理
-
编码问题:
string(data)
转换可能造成编码丢失- 二进制数据应使用
bytes.Reader
在中间件开发、API网关和测试框架中极为常见,它解决了HTTP响应体只能读取一次的核心限制,为响应处理提供了灵活性。