golang实现查询一个host的所有cname,request请求中io.NopCloser实现IO流多次读取,在中间件开发、API网关和测试框架中极为常见,它解决了HTTP响应体只能读取一次的核心

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)

  1. 输入:主机名(如 “www.example.com”)
  2. 输出
    • 字符串数组:所有CNAME记录(包括链式CNAME)
    • 错误信息(如果发生错误)

实现原理

  1. CNAME链解析

    • 从初始主机开始查询CNAME
    • 对返回的CNAME再次查询,形成链式解析
    • 最大深度限制(10级)防止无限循环
  2. 循环检测

    • 使用visited映射记录已访问的域名
    • 检测到循环时返回错误
  3. 反向解析

    • 当CNAME查询失败时,尝试解析主机的IP地址
    • 对IP地址进行反向DNS查询获取PTR记录
    • 将PTR记录作为可能的CNAME结果
  4. 标准化处理

    • 去除域名尾部的点(如 “www.example.com.” → “www.example.com”)
    • 避免重复记录

错误处理

  1. DNS查询错误:返回标准DNSError
  2. 循环检测:返回"CNAME loop detected"错误
  3. 最大深度:达到最大深度时停止查询

示例输出

查询 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记录

使用说明

  1. 直接调用LookupAllCNAMEs(host)函数
  2. 返回结果是一个字符串数组,包含所有找到的CNAME记录
  3. 结果按照解析顺序排列(从初始主机到最终CNAME)

注意事项

  1. DNS解析依赖:结果取决于本地DNS解析器和网络环境
  2. 超时设置:示例中设置了5秒超时,可根据需要调整
  3. DNS缓存:结果可能受系统或Go的DNS缓存影响
  4. 非标准配置:某些域名可能有复杂的CNAME链或循环

这个实现能够处理大多数标准DNS配置,包括链式CNAME和反向解析,同时提供了防止循环和深度限制的安全机制。



request请求中io.NopCloser实现IO流多次读取

resp.Body = io.NopCloser(strings.NewReader(string(data)))

这行代码在HTTP请求处理中扮演着非常重要的角色,主要作用是重置响应体并使其可重复读取

resp.Body = io.NopCloser(strings.NewReader(string(data)))

作用分解:

  1. 重建响应体流

    • 原始resp.Body是只读流,读取后会被关闭且内容耗尽
    • 这行代码创建了一个包含新数据的全新可读流
  2. 数据类型转换

    • datastring*strings.Readerio.ReadCloser
    • 最终创建了一个可重复读取的内存型响应体
  3. 关键组件

    • strings.NewReader():创建基于字符串的读取器
    • io.NopCloser():添加无操作的Close方法
    • 组合后满足io.ReadCloser接口要求

主要目的:

  1. 响应体重用

    // 原始响应体只能读取一次
    origBody, _ := io.ReadAll(resp.Body) // 此时body已耗尽
    
    // 重置后可以再次读取
    resp.Body = io.NopCloser(strings.NewReader(string(data)))
    newBody, _ := io.ReadAll(resp.Body) // 可再次读取相同内容
    
  2. 修改响应内容

    // 1. 读取原始响应
    data, _ := io.ReadAll(resp.Body)
    
    // 2. 修改内容(示例:替换文本)
    modified := bytes.ReplaceAll(data, []byte("旧文本"), []byte("新文本"))
    
    // 3. 重置响应体
    resp.Body = io.NopCloser(bytes.NewReader(modified))
    
  3. 中间件处理

    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)
      })
    }
    

为什么需要这样做?

  1. 流式特性限制

    • HTTP响应体是io.ReadCloser类型
    • 标准流只能被读取一次
    • 读取后底层连接会被关闭
  2. 常见使用场景

    • 响应日志记录
    • 内容修改/过滤
    • 响应缓存
    • 错误恢复处理
    • 单元测试模拟
  3. 性能考量

    • 比磁盘缓存更高效
    • 避免大文件内存问题(需控制data大小)

替代实现方式:

更高效的字节切片版本(避免字符串转换):

import "bytes"

// 直接使用字节切片
resp.Body = io.NopCloser(bytes.NewReader(data))

注意事项:

  1. 资源管理

    • 重置后仍需调用resp.Body.Close()
    • io.NopCloser的Close是空操作
    • 实际资源由strings.Reader管理
  2. 大小限制

    • 适用于合理大小的响应体
    • 大文件应考虑流式处理
  3. 编码问题

    • string(data)转换可能造成编码丢失
    • 二进制数据应使用bytes.Reader

在中间件开发、API网关和测试框架中极为常见,它解决了HTTP响应体只能读取一次的核心限制,为响应处理提供了灵活性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码讲故事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值