-
实现一个简单的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_second9
# 监听端口
http_listen_addr8081
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")
})
访问该 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