http 探针工具

1 http 探针工具

  • 实现一个简单的http探测的web

  • gin写一个web /probe/http?host=baidu.com&is_https=1

  • host代表探测的地址或ip

  • is_https=1代表探测 https://baidu.com否则是 http://baidu.com

  • 返回探测的结果

    • 域名的ip

    • status_code

    • http各阶段的耗时

  • 需要一个yaml解析的配置

    • http的listen的地址

    • 探测超时时间

  • 总的来说就是实现上述工程,go mod管理

我们先来看一下该项目的目录结构

  • config 文件是用来加载和读取 yaml 配置文件

  • http 调用 gin 框架用来实现 http 的访问

  • probe 实现对 http 头部探针的访问

  • main.go 文件调用所有的功能

  • simple_http_probe.yaml 文件编辑了我们的 yaml 配置文件

1.1 先定义 yaml 配置文件

当然可以定义别的配置文件,这样做的目的是为了在工程项目中能够先读取加载配置文件

# 定义超时时间
http_probe_timeout_second: 9

# 监听端口
http_listen_addr: :8081

1.2 编写 config 文件,实现对配置文件的读取

config.go

package config

import (
    "io/ioutil"
    "log"

    // 导入 yaml 包
    "gopkg.in/yaml.v2"
)

/*
- 需要一个yaml解析的配置
    - http的listen的地址
    - 探测超时时间
*/

// 全局超时变量
var GlobalTwSec int

// 生成 config 结构体,获取 yaml 配置文件中的具体信息,用于反序列化
type Config struct {
    HttpListenAddr         string `yaml:"http_listen_addr"`
    HttpProbeTimeoutSecond int    `yaml:"http_probe_timeout_second"`
}

// yaml解析
func Load(in []byte) (*Config, error) {

    cfg := &Config{}

    // 对 cfg 变量进行反序列化
    err := yaml.Unmarshal(in, cfg)
    if err != nil {
        return nil, err
    }
    return cfg, nil

}

// 读取由 main 程序中传过来的配置文件
func LoadFile(filename string) (*Config, error) {
    content, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }

    // 1.先把读取到的配置文件内容 content 变量调用 Load 函数进行反序列化
    // 2.接收 load 函数反序列化后的返回值交给 cfg 和 err 变量
    cfg, err := Load(content)
    if err != nil {
        log.Printf("load.yaml.error :%v", err)
        return nil, err
    }

    // 如果配置文件中的超时时常 = 0 的时候全局的超时时常为 5
    if cfg.HttpProbeTimeoutSecond == 0 {
        GlobalTwSec = 5
    } else {
        GlobalTwSec = cfg.HttpProbeTimeoutSecond
    }

    return cfg, nil
}

1.3 编写 http 访问文件

当我们读取完了配置文件之后,就需要调用 gin 框架开启 web 访问。

gin.go 程序源码如下:

package http

import (
    "net/http"
    "simple-http-probe/config"

    "github.com/gin-gonic/gin"
)

// 定义 启动 gin 函数
func StartGin(c *config.Config) {
    // 初始化gin 实例
    r := gin.Default()
    // 绑定路由
    Routes(r)

    // 运行监听端口
    r.Run(c.HttpListenAddr)

}

// 添加路由的函数
//  /probe/http?host=baidu.com&is_https=1
func Routes(r *gin.Engine) {
    // api group贡献前缀path
    api := r.Group("/api")

    // 当我们访问这个 /probe/http uri 就会自动获取 HttpProbe 函数
    api.GET("/probe/http", HttpProbe)

    // 当我们访问这个 /v1 uri 就会自动调用 func(c *gin.Context) 这个钩子函数
    api.GET("/v1", func(c *gin.Context) {
        c.String(http.StatusOK, "你好我是 http prober")
    })
}

我们编写完了 http 访问的程序之后再编写一个 url .go 程序,实现对 url 的封装

package http

import (
    "fmt"
    "net/http"
    "simple-http-probe/probe"

    "github.com/gin-gonic/gin"
)

// 定义 httpProbe 为 gin 的钩子函数
func HttpProbe(c *gin.Context) {
    // 解析传过来的 host 和 isHttps 查询字符串参数
    host := c.Query("host")
    isHttps := c.Query("is_https")

    // validate 校验入参,
    // 也就是我们的在传递 host 的时候没有指定主机名输出默认 empty host 空主机
    if host == "" {
        c.String(http.StatusBadRequest, "empty host")
        return
    }

    // 定义 url 参数
    schema := "http"

    // 如果访问的 isHttps = 1 的时候则为 https url
    if isHttps == "1" {
        schema = "https"
    }

    // 字串拼接
    url := fmt.Sprintf("%s://%s", schema, host)

    // 如果 host 不为空则解析该 url
    // 得到 simple-http-probe/probe 包种的 DoHttpProbe 函数返回值
    res := probe.DoHttpProbe(url)

    // 最后输出状态码
    c.String(http.StatusOK, res)

}

1.4 编写 probe.go 文件(实现 http 访问时间的探针)

用来实现对 http 请求的时间处理

probe.go

package probe

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httptrace"
    "simple-http-probe/config"
    "strings"
    "time"
)

//用 net/http/httptrace 写个http耗时探测的项目 simple-http-probe
func DoHttpProbe(url string) string {
    // 定义来自 simple-http-probe/config 包中的 GlobalTwSec 变量
    twSec := config.GlobalTwSec

    // 结果 string
    dnsStr := ""
    targetAddr := ""
    // 提前定义好这些计算时间的对象
    var t0, t1, t2, t3, t4 time.Time

    // 全局或出错使用
    start := time.Now()

    // 初始化http req对象,通过 get 方法
    // 返回 Request 结构体,并请求忽略错误
    req, _ := http.NewRequest("GET", url, nil)

    // 定义 trace 赋值为 httptrace.ClientTrace{} 结构体
    // ClientTrace是一组在传出HTTP的不同阶段运行的钩子
    trace := &httptrace.ClientTrace{

        // DNS 开始查询的时候调用
        DNSStart: func(_ httptrace.DNSStartInfo) {
            // 开始dns解析的时候我 赋值t0 为当前时间
            t0 = time.Now()
        },

        // DNS查找结束时调用 DNSDone
        // 并且赋值 dnsInfo 变量为 httptrace.DNSDoneInfo 结构体
        DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
            // dns解析完成时间为 t1
            t1 = time.Now()
            ips := make([]string, 0)

            // 遍历 dnsInfo.Addrs 切片是一个 net.IPAddr 类型
            for _, d := range dnsInfo.Addrs {
                ips = append(ips, d.IP.String())
            }
            // 将地得到的 ip 地址和 , 拼接
            dnsStr = strings.Join(ips, ",")
        },

        // 开始连接时调用 ConnectStart
        ConnectStart: func(network, addr string) {
            // 如果连接时间为 0 时
            if t1.IsZero() {
                // 直接传ip没dns解析 开始连接算start
                t1 = time.Now()
            }
        },

        // 拨号成功时调用
        ConnectDone: func(network, addr string, err error) {
            if err != nil {
                log.Printf("[无法建立和探测目标的连接][addr:%v][err:%v]", addr, err)
                return
            }

            // 将地址传入给 targetAddr 变量
            targetAddr = addr
            t2 = time.Now()
        },

        // 在连接成功后调用 GotConn
        // 固定写法 GotConn func(GotConnInfo) {} 获取连接信息
        GotConn: func(_ httptrace.GotConnInfo) {
            // 获取到信息之后 t3 开始计数
            t3 = time.Now()
        },

        // 当响应第一个字节的时候 t4 开始计数
        GotFirstResponseByte: func() {
            t4 = time.Now()
        },
    }
    // 标准用法
    // req.WithContext() 返回一个 request 指针给 req
    // http.WithContext 接收 context.Context 正好 httptrace.WithClientTrace() 返回也是 context.Context
    // 传入 req.Context 方法 返回一个 Context 链接请求的上下文和 trace 结构体 ClientTrace 钩子
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
    // 这里的目的就是造个有超时时间的客户端
    client := http.Client{
        Timeout: time.Duration(twSec) * time.Second,
    }

    // 发起一个 http 请求,返回 http 响应
    resp, err := client.Do(req)
    // 出错的情况
    if err != nil {
        msg := fmt.Sprintf("[http探测出错]\n"+
            "[http探测的目标:%s]\n"+
            "[错误详情:%v]\n"+
            "[总耗时:%s]\n",
            url,
            err,

            // 调用 msDurationStr 自定义函数,然后通过 time.Now().Sub() 传入 start ,计算出 总共耗时
            msDurationStr(time.Now().Sub(start)),
        )
        log.Printf(msg)
        return msg

    }
    // 关闭连接
    defer resp.Body.Close()
    end := time.Now()

    // 没有dns
    if t0.IsZero() {
        t0 = t1
    }

    dnsLookup := msDurationStr(t1.Sub(t0))        // DNS查找结束时调用 DNSDone ,所以 t1 时间减掉 t0 开始链接时间
    tcpConnection := msDurationStr(t3.Sub(t1))    // 在连接成功后调用 GotConn 然后给 t3 ,Gotonn 获取链接信息包括 tcp
    serverProcessing := msDurationStr(t4.Sub(t3)) // 当响应第一个字节的时候 t4 开始计数
    totoal := msDurationStr(end.Sub(t0))          // 总共耗时通过 end 减掉 t0,t0 = start
    probeResStr := fmt.Sprintf(
        "[http探测的目标:%s]\n"+
            "[dns解析的结果:%s]\n"+
            "[连接的ip和端口:%s]\n"+
            "[状态码:%d]\n"+
            "[dns解析耗时:%s]\n"+
            "[tcp连接耗时:%s]\n"+
            "[服务端处理耗时:%s]\n"+
            "[总耗时:%s]\n",
        url,
        dnsStr,
        targetAddr,
        resp.StatusCode,
        dnsLookup,
        tcpConnection,
        serverProcessing,
        totoal,
    )
    return probeResStr
}

// 定义一个 msDurationStr 函数将 int 类型 通过 fmt.Sprintf 函数转为 string
func msDurationStr(d time.Duration) string {
    return fmt.Sprintf("%dms", int(d/time.Millisecond))
}

1.5 编写 main.go 主程序

package main

import (
    "flag"
    "log"
    "simple-http-probe/config"
    "simple-http-probe/http"
)

var (
    configFile string
)

func main() {
    // 传入配置文件路径为当前的 simple_http_probe.yml 文件
    flag.StringVar(&configFile, "c", "simple_http_probe.yml", "config file path")
    // 解析yaml ,将 configfile 文件传递给 config 文件中的 LoadFile() 函数
    conf, err := config.LoadFile(configFile)
    if err != nil {
        log.Printf("[config.Load.error][err:%v]", err)
        return
    }
    log.Printf("配置是:%v", conf)

    // 调用 http 文件中的 StartGin 函数 启动 gin 开启一个协程,异步处理
    go http.StartGin(conf)

    // select 等待 go 协程处理不退出
    select {}
}

1.6 执行该程序

[14:59:56 root@go simple-http-probe]#go run main.go 
2021/07/19 14:59:57 配置是:&{:8081 9}
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/probe/http           --> simple-http-probe/http.HttpProbe (3 handlers)
[GIN-debug] GET    /api/v1                   --> simple-http-probe/http.Routes.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8081

访问该 url 调用 下面函数

api.GET("/v1", func(c *gin.Context) {
        c.String(http.StatusOK, "你好我是 http prober")
    })

http://10.0.0.10:8081/api/v1

访问该 url 调用下面函数

api.GET("/probe/http", HttpProbe)

if host == "" {
        c.String(http.StatusBadRequest, "empty host")
        return
    }

http://10.0.0.10:8081/api/probe/http

http://10.0.0.10:8081/api/probe/http?host=www.baidu.com

解析百度

http://10.0.0.10:8081/api/probe/http?host=1.1.1.1

解析错误的 rul

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇