简介
这篇文章主要是通过官方提供的 HTTP 追踪来学习使用 Hook 的编程思想。
在了解使用 Go 语言编写 Hook 之前,最好先掌握 Context 的用法, go 1.7 中 context 已经进入标准库 context,直接 import "context" 就可以使用。
在标准库 context.go 中, Context 定义为一个 interface{}
接口类型。
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
标准库用了一个 emptyCtx 来作为默认的 implementer
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
...
由于默认的 emptyCtx 是未导出的,因此,标准库又提供了两个公共的函数 Background 和 TODO 来访问这个 implementer。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
当然,还提供了一个 CancelFunc 的回调函数原型,WithCancel() 函数可用来给 Context 注册一个 cancel 函数。
还提供了另外几个其他的 With 函数,分别是 WithDeadline(), WithTimeout() 和 WithValue()。
WithDeadline 和 WithTimeout 都是给 parent Context 加上一个 timeout 并返回一个 cancel func。
WithValue() 复制原 context,并保存一对 key、value 到副本中。
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
由于 Context 本身是 interface{} 接口类型,没法保存 key/value,其实质是通过一个包的私有类型 valueCtx 来保存的,如下:
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
可见,通过传入一个 parent context,以及 key/value,最终会保存在 valueCtx 中并返回。
再来看 http.Request,这是一个结构体,其有一个成员就是 ctx Context 接口类型(私有成员)。通过包方法 http.NewRequest() 来创建。默认 Context 成员未被初始化,也就是 nil。
request 提供了一个公共方法 Context() 来访问私有的 ctx 成员。
func (r *Request) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background()
}
可见,默认创建好的 request, 其 ctx 是 Background() 。
Request 结构体提供了 WithContext(ctx context.Context) 方法用来给自己的 ctx 成员赋值。
// WithContext returns a shallow copy of r with its context changed
// to ctx. The provided ctx must be non-nil.
func (r *Request) WithContext(ctx context.Context) *Request {
if ctx == nil {
panic("nil context")
}
r2 := new(Request)
*r2 = *r
r2.ctx = ctx
return r2
}
由于 Request 是客户端的行为,为了追踪 Request 请求过程中发生的各种事件及行为,标准库 trace.go 中提供了一个叫 ClientTrace 的结构体,它包含了一系列的钩子函数 hooks,如下:
// ClientTrace is a set of hooks to run at various stages of an outgoing HTTP request.
type ClientTrace struct {
// GetConn is called before a connection is created or retrieved from an idle pool.
GetConn func(hostPort string)
// GotConn is called after a successful connection is obtained.
GotConn func(GotConnInfo)
// PutIdleConn is called when the connection is returned to the idle pool.
PutIdleConn func(err error)
GotFirstResponseByte func()
Got100Continue func()
// DNSStart is called when a DNS lookup begins.
DNSStart func(DNSStartInfo)
// DNSDone is called when a DNS lookup ends.
DNSDone func(DNSDoneInfo)
// ConnectStart is called when a new connection's Dial begins.
ConnectStart func(network, addr string)
// ConnectDone is called when a new connection's Dial completes.
ConnectDone func(network, addr string, err error)
// TLSHandshakeStart is called when the TLS handshake is started.
TLSHandshakeStart func()
TLSHandshakeDone func(tls.ConnectionState, error)
WroteHeaders func()
Wait100Continue func()
WroteRequest func(WroteRequestInfo)
}
trace.go 还提供了一个 WithClientTrace() 函数,可以把 ClientTrace 结构体中的钩子都保存(注册)到 Context 中去(因为 Context 提供 key/value 存储嘛),
key 就是一个叫 clientEventContextKey 的空结构体,value 是 nettrace 包中的 Trace 结构体,这个结构体作用跟 ClientTrace 一样,都是包含了一堆 hook 函数作为成员,
在这里它的目的只是封装下 ClientTrace 中的 hook 函数。最终, WithClientTrace() 会返回一个 context,它保存了上述的 hook 函数。
type clientEventContextKey struct{}
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
if trace == nil {
panic("nil trace")
}
old := ContextClientTrace(ctx)
trace.compose(old)
ctx = context.WithValue(ctx, clientEventContextKey{}, trace)
if trace.hasNetHooks() {
nt := &nettrace.Trace{
ConnectStart: trace.ConnectStart,
ConnectDone: trace.ConnectDone,
}
if trace.DNSStart != nil {
nt.DNSStart = func(name string) {
trace.DNSStart(DNSStartInfo{Host: name})
}
}
if trace.DNSDone != nil {
...
}
ctx = context.WithValue(ctx, nettrace.TraceKey{}, nt)
}
return ctx
}
通过 ContextClientTrace() 的函数,可以把 ClientTrace 从 Context 中取出来。
// ContextClientTrace returns the ClientTrace associated with the
// provided context. If none, it returns nil.
func ContextClientTrace(ctx context.Context) *ClientTrace {
trace, _ := ctx.Value(clientEventContextKey{}).(*ClientTrace)
return trace
}
现在,我们知道,有了 WithClientTrace(),我们就可以把钩子函数保存在 Context 中了,现在,我们要把这些钩子函数挂到 Request 中去,改怎么弄?
很简单,直接通过 Request.WithContext() 保存到 Request 中就可以了。
现在 Request 有了这些钩子函数,那么什么时候会被调用呢? 当然会 http.Client.Do(request) 的时候啦。
接下来我们看看整个流程:
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptrace"
)
// transport is an http.RoundTripper that keeps track of the in-flight
// request and implements hooks to report HTTP tracing events.
type transport struct {
current *http.Request
}
// RoundTrip wraps http.DefaultTransport.RoundTrip to keep track
// of the current request.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
t.current = req
return http.DefaultTransport.RoundTrip(req)
}
// GotConn prints whether the connection has been used previously
// for the current request.
func (t *transport) GotConn(info httptrace.GotConnInfo) {
fmt.Printf("Connection reused for %v? %v\n", t.current.URL, info.Reused)
}
func main() {
t := &transport{}
req, _ := http.NewRequest("GET", "https://google.com", nil)
trace := &httptrace.ClientTrace{
GotConn: t.GotConn,
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client := &http.Client{Transport: t}
if _, err := client.Do(req); err != nil {
log.Fatal(err)
}
}
所有的钩子的调用,最终都会在 client.Do() 里面执行,我们看看是怎么执行的。
注意到这里的 transport 结构体,它其实是 RoundTripper 接口类型(在 client.go 中声明)的一个 implementer,这个 RoundTripper 实际只有一个方法:
// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
type RoundTripper interface {
// RoundTrip executes a single HTTP transaction, returning
// a Response for the provided Request.
RoundTrip(*Request) (*Response, error)
}
在 client.Do() 中,会调用 client.send(),如下:
resp, didTimeout, err = c.send(req, deadline)
c.send() 内部:
// didTimeout is non-nil only if err != nil.
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
...
resp, didTimeout, err = send(req, c.transport(), deadline)
...
return resp, nil, nil
}
send() 内部:
```go
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
...
resp, err = rt.RoundTrip(req)
...
}
可见,最终调用了 rt.RoundTrip() 函数。也就是上述 main.go 中 transport 实现的 RoundTrip() 函数。
在 rt.RoundTrip() 里面,把 req 赋给了 DefaultTransport.RoundTrip(req),
这个 DefaultTransport 是包提供的一个 RoundTripper 的默认实现,
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
然后,在它的 RoundTrip() 函数里面最终会调用上述的钩子函数。
// RoundTrip implements the RoundTripper interface.
//
// For higher-level HTTP client support (such as handling of cookies
// and redirects), see Get, Post, and the Client type.
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
ctx := req.Context()
trace := httptrace.ContextClientTrace(ctx)
for {
treq := &transportRequest{Request: req, trace: trace}
cm, err := t.connectMethodForRequest(treq)
...
pconn, err := t.getConn(treq, cm)
}
}
解析:
通过调用 httptrace.ContextClientTrace(ctx) 把 context 中的钩子函数都取出来,再在 t.getConn() 中调用钩子函数,如下:
有疑问加站长微信联系(非本文作者)