Golang HTTP 标准库的使用实现原理
一.使用:
启动http服务:
package main
import "net/http"
func main() {
http.HandleFunc("/wecunge", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("wecunge"))
})
http.ListenAndServe(":8080", nil)
}
调用了http.HandleFunc方法,注册了对应于请求路径 /wecunge 的 handler 函数,然后调用了http.ListenAndServe在8080端口启动了服务。
发送 http 请求
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Post("http://localhost:8080/wecunge", "", nil)
if err != nil {
return
}
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
resp.Body.Close()
}
首先调用http.Post向/wecunge发送一个post请求,然后调用ioutil.ReadAll方法获得resp中的数据,打印获取到的数据,最后关闭响应体。
二.服务端
1.核心数据结构
(1)server
type Server struct {
Addr string
Handler Handler
DisableGeneralOptionsHandler bool
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
BaseContext func(net.Listener) context.Context
ConnContext func(ctx context.Context, c net.Conn) context.Context
inShutdown atomic.Bool
disableKeepAlives atomic.Bool
nextProtoOnce sync.Once
nextProtoErr error
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
onShutdown []func()
listenerGroup sync.WaitGroup
}
各字段解析:
Addr: 服务器监听的地址。
Handler: 处理HTTP请求的处理器。
DisableGeneralOptionsHandler: 是否禁用HTTP OPTIONS请求的默认处理器。
TLSConfig: TLS配置,用于HTTPS。
ReadTimeout: 读取请求的超时时间。
ReadHeaderTimeout: 读取请求头部的超时时间。
WriteTimeout: 写入响应的超时时间。
IdleTimeout: 连接空闲超时时间。
MaxHeaderBytes: 最大请求头大小。
TLSNextProto: 用于ALPN(应用层协议协商)的协议映射。
ConnState: 连接状态变化回调函数。
ErrorLog: 错误日志记录器。
BaseContext: 为监听器创建基础上下文的函数。
ConnContext: 为连接创建上下文的函数。
inShutdown: 原子布尔值,表示服务器是否正在关闭。
disableKeepAlives: 原子布尔值,表示是否禁用长连接。
nextProtoOnce: 同步原语,用于确保NextProto只被设置一次。
nextProtoErr: NextProto设置时的错误。
mu: 互斥锁,用于同步对共享资源的访问。
listeners: 正在监听的监听器集合。
activeConn: 活跃连接集合。
onShutdown: 服务器关闭时调用的回调函数列表。
listenerGroup: 等待组,用于等待所有监听器完成。
(2)Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
作用:
根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应.Handler
接口的实现允许自定义如何处理HTTP请求。任何实现了 ServeHTTP
方法的类型都可以用作HTTP处理器。这使得Go的HTTP服务器非常灵活,可以轻松地集成自定义逻辑。
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
patterns []*pattern // TODO(jba): remove if possible
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
各字段解析:
mu: 用于保护 ServeMux
结构体中的共享数据,确保并发安全。读锁用于处理请求时的读操作,写锁用于添加或删除路由时的写操作。
tree: 一个树状结构,用于高效地匹配长路径。routingNode
是 ServeMux
内部使用的节点类型,用于构建路由树。
index: 一个索引结构,用于快速匹配短路径。routingIndex
是 ServeMux
内部使用的索引类型,用于存储和查找短路径的路由。
patterns: 存储所有添加到 ServeMux
中的路由模式。每个 pattern
表示一个路由模式,例如 /path/:param
。这个字段用于存储这些模式,以便在添加或匹配路由时使用。
mux121: 这是一个仅在特定调试模式下使用的字段。当环境变量 GODEBUG=httpmuxgo121=1
被设置时,ServeMux
会使用这个字段来实现一个不同的路由匹配逻辑,用于调试和测试。
作用:
ServeMux
是 http
包中的核心组件,它允许开发者为不同的URL路径指定不同的处理函数。通过调用 http.HandleFunc
或 http.Handle
方法,可以将URL路径与处理函数关联起来。当HTTP服务器接收到请求时,ServeMux
会根据请求的URL找到相应的处理函数,并调用它来处理请求。
2.注册 handler
(1)DefaultServeMux
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
DefaultServeMux
是 http
包中预定义的全局 ServeMux
实例,用于处理HTTP请求。
(2)HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if use121 {
DefaultServeMux.mux121.handleFunc(pattern, handler)
} else {
DefaultServeMux.register(pattern, HandlerFunc(handler))
}
}
use121:这个变量的值是通过读取环境变量GODEBUG
中的httpmuxgo121
设置来决定的。如果httpmuxgo121
的值被设置为"1",则use121
会被设置为true
,这将启用Go 1.21版本的ServeMux
行为。
具体来说,当use121
为true
时,ServeMux
的行为会使用serveMux121
结构体来处理HTTP请求,这个结构体包含了Go 1.21版本中ServeMux
的实现。这意味着,如果开发者需要在Go 1.22或更高版本中保持与Go 1.21相同的行为,可以通过设置GODEBUG=httpmuxgo121=1
来实现。这样做的目的是为了向后兼容性,允许开发者在新版本的Go语言中使用旧版本的ServeMux
行为,直到他们准备好迁移到新的行为。
(3)register
func (mux *ServeMux) register(pattern string, handler Handler) {
if err := mux.registerErr(pattern, handler); err != nil {
panic(err)
}
}
作用:用于将一个URL模式(pattern
)和一个处理函数(handler
)注册到ServeMux
中。
(4)registerErr
func (mux *ServeMux) registerErr(patstr string, handler Handler) error {
if patstr == "" {
return errors.New("http: invalid pattern")
}
if handler == nil {
return errors.New("http: nil handler")
}
if f, ok := handler.(HandlerFunc); ok && f == nil {
return errors.New("http: nil handler")
}
pat, err := parsePattern(patstr)
if err != nil {
return fmt.Errorf("parsing %q: %w", patstr, err)
}
// Get the caller's location, for better conflict error messages.
// Skip register and whatever calls it.
_, file, line, ok := runtime.Caller(3)
if !ok {
pat.loc = "unknown location"
} else {
pat.loc = fmt.Sprintf("%s:%d", file, line)
}
mux.mu.Lock()
defer mux.mu.Unlock()
// Check for conflict.
if err := mux.index.possiblyConflictingPatterns(pat, func(pat2 *pattern) error {
if pat.conflictsWith(pat2) {
d := describeConflict(pat, pat2)
return fmt.Errorf("pattern %q (registered at %s) conflicts with pattern %q (registered at %s):\n%s",
pat, pat.loc, pat2, pat2.loc, d)
}
return nil
}); err != nil {
return err
}
mux.tree.addPattern(pat, handler)
mux.index.addPattern(pat)
mux.patterns = append(mux.patterns, pat)
return nil
}
作用:用于将一个URL模式(patstr
)和一个处理函数(handler
)注册到ServeMux
中,并返回一个错误值(如果有的话)
流程:
(1)参数检查:
首先判断patstr
是否为空字符串,是则返回一个错误,表示无效的模式。
如果handler
为nil
,返回一个错误,表示处理函数为空。
如果handler
是一个HandlerFunc
类型,并且其函数值为nil
,返回一个错误,表示处理函数为空。
(2)解析模式:
调用parsePattern
函数解析patstr
,如果解析失败,返回一个错误,包括解析失败的具体信息。
(3)获取调用者位置:
使用runtime.Caller
获取调用registerErr
方法的代码的位置信息,以便在发生冲突时提供更详细的错误信息。
如果获取位置信息失败,将位置设置为"unknown location";否则,将位置信息格式化为文件名和行号。
(4)锁定ServeMux
:
使用mux.mu.Lock()
锁定ServeMux
,以确保注册操作的线程安全性。
使用defer mux.mu.Unlock()
确保在方法返回时解锁。
(5)检查冲突:
调用mux.index.possiblyConflictingPatterns
检查是否有与新注册的模式冲突的现有模式。
如果发现冲突,返回一个详细的错误信息,包括冲突的模式和它们的位置。
(6)添加模式:
如果没有冲突,将新模式添加到mux.tree
、mux.index
和mux.patterns
中。
mux.tree.addPattern
将模式添加到路由树中。
mux.index.addPattern
将模式添加到索引中,以便快速查找和冲突检测。
mux.patterns
是一个包含所有模式的切片,用于记录所有注册的模式。
(7)返回结果:
如果所有操作成功,返回nil
表示没有错误。
3.启动 server
(1)ListenAndServe
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
函数内部创建了一个 Server
结构体的实例,并将传入的 addr
和 handler
赋值给这个实例的 Addr
和 Handler
字段。然后,函数调用 server
实例的 ListenAndServe
方法来启动服务器,并返回这个方法的返回值。
(2)(srv *Server) ListenAndServe()
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
这段代码是 ListenAndServe
方法的实现,它是 Server
结构体的一个方法。这个方法的作用是启动服务器并使其开始监听和处理传入的 HTTP 请求。
首先,方法检查服务器是否正在关闭。如果 srv.shuttingDown()
返回 true
,则表示服务器正在关闭或已经关闭,此时方法会返回一个错误 ErrServerClosed
,表示服务器已经关闭,不能再接受新的连接。
然后,获取 Server
结构体的 Addr
字段,这个字段包含了服务器要监听的地址。如果 Addr
字段为空字符串,那么将地址设置为默认的 HTTP 端口,即 ":http"。在 Go 的网络编程中,空字符串地址会被解析为默认端口,对于 HTTP 来说,默认端口是 80。
使用 net.Listen
函数在指定的地址和协议(这里是 TCP)上监听。如果监听失败,err
将包含错误信息,方法会返回这个错误。
如果监听成功,方法会创建一个监听器 ln
,然后调用 srv.Serve(ln)
方法来开始处理连接。Serve
方法会接受 ln
作为参数,ln
是一个 net.Listener
接口,用于接受新的 TCP 连接。
(3)(srv *Server) Serve
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
baseCtx := context.Background()
if srv.BaseContext != nil {
baseCtx = srv.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
if srv.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
这段代码是 Serve
方法的实现,它是 Server
结构体的一个方法,用于处理传入的连接。这个方法的主要作用是接受客户端的连接请求,并为每个连接创建一个新的 Conn
对象来处理请求。这个方法会一直运行,直到服务器关闭。对于每个新的连接,它都会创建一个新的 Conn
对象,并在一个独立的 goroutine 中处理请求。这样可以同时处理多个连接,提高服务器的并发能力。
if fn := testHookServerServe; fn != nil { fn(srv, l) }
这是一个测试钩子(hook),如果 testHookServerServe
函数不为空,则调用它。这通常用于测试中,以便在 Serve
方法执行之前进行一些额外的操作。
origListener := l
保存原始的监听器 l
,以便在需要时使用。
l = &onceCloseListener{Listener: l}
创建一个 onceCloseListener
对象,它包装了原始的监听器 l
。这个包装器确保监听器只会被关闭一次,即使 Serve
方法被多次调用。
defer l.Close()
当 Serve
方法退出时,关闭监听器。
if err := srv.setupHTTP2_Serve(); err != nil { return err }
如果服务器配置了 HTTP/2,这个方法会进行一些设置。如果设置过程中出现错误,则返回错误。
if !srv.trackListener(&l, true) { return ErrServerClosed }
跟踪监听器的状态,如果服务器正在关闭,则返回 ErrServerClosed
。
defer srv.trackListener(&l, false)
当 Serve
方法退出时,更新监听器的状态。
if err := srv.setupHTTP2_Serve(); err != nil { return err }
如果服务器配置了 HTTP/2,这个方法会进行一些设置。如果设置过程中出现错误,则返回错误。
if !srv.trackListener(&l, true) { return ErrServerClosed }
跟踪监听器的状态,如果服务器正在关闭,则返回 ErrServerClosed
。
defer srv.trackListener(&l, false)
当 Serve
方法退出时,更新监听器的状态。
baseCtx := context.Background()
创建一个基础的上下文对象。
if srv.BaseContext != nil { baseCtx = srv.BaseContext(origListener) }
如果服务器提供了 BaseContext
方法,使用它来创建一个新的上下文对象。
var tempDelay time.Duration
定义一个变量,用于记录在连接失败时的重试间隔。
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
在上下文中设置服务器对象。
for { ... }
开始一个无限循环,不断接受新的连接。
rw, err := l.Accept()
接受一个新的连接,如果出现错误,则根据错误类型进行处理。如果服务器正在关闭,则返回 ErrServerClosed
。如果是临时错误,则等待一段时间后重试。
connCtx := ctx
创建一个新的上下文对象,用于当前连接。
if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) }
如果服务器提供了 ConnContext
方法,使用它来创建一个新的上下文对象。
tempDelay = 0
重置重试间隔。
c := srv.newConn(rw)
为新的连接创建一个新的 Conn
对象。
c.setState(c.rwc, StateNew, runHooks)
设置连接的状态。
go c.serve(connCtx)
为新的连接启动一个新的 goroutine 来处理请求。
(4) (c *conn) serve
func (c *conn) serve(ctx context.Context) {
if ra := c.rwc.RemoteAddr(); ra != nil {
c.remoteAddr = ra.String()
}
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
var inFlightResponse *response
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if inFlightResponse != nil {
inFlightResponse.cancelCtx()
inFlightResponse.disableWriteContinue()
}
if !c.hijacked() {
if inFlightResponse != nil {
inFlightResponse.conn.r.abortPendingRead()
inFlightResponse.reqBody.Close()
}
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
tlsTO := c.server.tlsHandshakeTimeout()
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
c.rwc.SetReadDeadline(dl)
c.rwc.SetWriteDeadline(dl)
}
if err := tlsConn.HandshakeContext(ctx); err != nil {
// If the handshake failed due to the client not speaking
// TLS, assume they're speaking plaintext HTTP and write a
// 400 response on the TLS conn's underlying net.Conn.
var reason string
if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
re.Conn.Close()
reason = "client sent an HTTP request to an HTTPS server"
} else {
reason = err.Error()
}
c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), reason)
return
}
// Restore Conn-level deadlines.
if tlsTO > 0 {
c.rwc.SetReadDeadline(time.Time{})
c.rwc.SetWriteDeadline(time.Time{})
}
c.tlsState = new(tls.ConnectionState)
*c.tlsState = tlsConn.ConnectionState()
if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
if fn := c.server.TLSNextProto[proto]; fn != nil {
h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
// Mark freshly created HTTP/2 as active and prevent any server state hooks
// from being run on these connections. This prevents closeIdleConns from
// closing such connections. See issue https://golang.org/issue/39776.
c.setState(c.rwc, StateActive, skipHooks)
fn(c.server, tlsConn, h)
}
return
}
}
// HTTP/1.x from here on.
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive, runHooks)
}
if err != nil {
const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"
switch {
case err == errTooLarge:
// Their HTTP client may or may not be
// able to read this if we're
// responding to them and hanging up
// while they're still writing their
// request. Undefined behavior.
const publicErr = "431 Request Header Fields Too Large"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
c.closeWriteAndWait()
return
case isUnsupportedTEError(err):
// Respond as per RFC 7230 Section 3.3.1 which says,
// A server that receives a request message with a
// transfer coding it does not understand SHOULD
// respond with 501 (Unimplemented).
code := StatusNotImplemented
// We purposefully aren't echoing back the transfer-encoding's value,
// so as to mitigate the risk of cross side scripting by an attacker.
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
return
case isCommonNetReadError(err):
return // don't reply
default:
if v, ok := err.(statusError); ok {
fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
return
}
const publicErr = "400 Bad Request"
fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
return
}
}
// Expect 100 Continue support
req := w.req
if req.expectsContinue() {
if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
// Wrap the Body reader with one that replies on the connection
req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
w.canWriteContinue.Store(true)
}
} else if req.Header.get("Expect") != "" {
w.sendExpectationFailed()
return
}
c.curReq.Store(w)
if requestBodyRemains(req.Body) {
registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
} else {
w.conn.r.startBackgroundRead()
}
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
// But we're not going to implement HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
inFlightResponse = w
serverHandler{c.server}.ServeHTTP(w, w.req)
inFlightResponse = nil
w.cancelCtx()
if c.hijacked() {
return
}
w.finishRequest()
c.rwc.SetWriteDeadline(time.Time{})
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
c.setState(c.rwc, StateIdle, runHooks)
c.curReq.Store(nil)
if !w.conn.server.doKeepAlives() {
// We're in shutdown mode. We might've replied
// to the user without "Connection: close" and
// they might think they can send another
// request, but such is life with HTTP/1.1.
return
}
if d := c.server.idleTimeout(); d > 0 {
c.rwc.SetReadDeadline(time.Now().Add(d))
} else {
c.rwc.SetReadDeadline(time.Time{})
}
// Wait for the connection to become readable again before trying to
// read the next request. This prevents a ReadHeaderTimeout or
// ReadTimeout from starting until the first bytes of the next request
// have been received.
if _, err := c.bufr.Peek(4); err != nil {
return
}
c.rwc.SetReadDeadline(time.Time{})
}
}
这段代码展示了 HTTP 服务器如何处理客户端连接,包括错误处理、TLS 握手、请求读取、请求处理和连接管理
if ra := c.rwc.RemoteAddr(); ra != nil {
c.remoteAddr = ra.String()
}
如果连接的远程地址(客户端地址)不为空,则将其转换为字符串并存储在 c.remoteAddr
中。
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
在上下文中增加本地地址(服务器地址),以便在处理请求时可以访问。
var inFlightResponse *response
defer func() {
// 异常捕获和日志记录
if err := recover(); err != nil && err != ErrAbortHandler {
// ...
}
// 取消正在进行的响应和关闭连接
if inFlightResponse != nil {
// ...
}
if !c.hijacked() {
// ...
}
}()
定义一个延迟执行的函数,用于捕获 panic 异常、记录日志,并在请求处理结束后进行资源清理,如取消响应和关闭连接。
if tlsConn, ok := c.rwc.(*tls.Conn); ok {
// ...
}
如果连接是 TLS 连接,则执行 TLS 握手。如果握手失败,记录错误并返回。如果握手成功,检查是否支持 ALPN(应用层协议协商),如果支持,则调用相应的处理函数。
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
创建一个新的上下文和取消函数,用于控制请求处理的生命周期。
for {
w, err := c.readRequest(ctx)
// ...
}
进入一个无限循环,不断读取请求。如果读取请求时发生错误,根据错误类型发送相应的响应或直接返回。
inFlightResponse = w
serverHandler{c.server}.ServeHTTP(w, w.req)
对于每个请求,创建一个响应对象 w
,并使用 ServeHTTP
方法处理请求。
w.finishRequest()
请求处理完成后,调用 finishRequest
方法完成请求。
if !w.shouldReuseConnection() {
// ...
return
}
检查连接是否可以重用,如果不可以,则关闭连接。
if _, err := c.bufr.Peek(4); err != nil {
return
}
在读取下一个请求之前,等待连接可读,以防止超时。
(5)(sh serverHandler) ServeHTTP
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
这个方法是 http.Handler
接口的核心方法,用于处理 HTTP 请求并将响应写入 ResponseWriter
。通过这种方式,ServeHTTP
方法能够将任何进入的 HTTP 请求正确地路由到相应的处理函数。
handler := sh.srv.Handler
从 serverHandler
结构体中的 srv
字段获取处理器(Handler
),这个处理器用于处理所有接收到的 HTTP 请求。
if handler == nil {
handler = DefaultServeMux
}
如果服务器没有指定自定义的处理器,那么使用默认的多路复用器 DefaultServeMux
。DefaultServeMux
是一个全局的 ServeMux
实例,用于将 URL 路径映射到处理函数。
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
如果服务器没有禁用通用的 OPTIONS 请求处理器,并且请求的 URI 是 *
(表示预检查所有路由),同时请求方法是 OPTIONS
,则使用 globalOptionsHandler
作为处理器。OPTIONS
请求通常用于跨源资源共享(CORS)预检,globalOptionsHandler
会返回允许的所有 HTTP 方法。
handler.ServeHTTP(rw, req)
最后,调用获取到的处理器的 ServeHTTP
方法,传入响应写入器 rw
和请求对象 req
,以处理请求并返回响应。
(6)(mux *ServeMux) ServeHTTP
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
h.ServeHTTP(w, r)
}
ServeMux
是一个多路复用器,它将 HTTP 请求的 URL 路径映射到不同的处理函数。
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
如果请求的 URI 是 *
,这通常不是一个有效的请求,因此服务器会发送一个 HTTP 400 错误(Bad Request)。如果请求的 HTTP 版本至少是 1.1,还会在响应头中设置 Connection
为 close
,表示关闭连接。
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
这段代码尝试找到与请求 URL 匹配的处理函数。mux
是 ServeMux
的一个实例,它维护了一个 URL 到处理函数的映射。findHandler
方法会查找这个映射,并返回相应的处理函数。use121
是一个布尔值,用于决定是否使用 HTTP/1.2.1 版本的处理逻辑。
h.ServeHTTP(w, r)
一旦找到处理函数 h
,就调用它的 ServeHTTP
方法,传入响应写入器 w
和请求对象 r
,以处理请求并返回响应。
(7)findHandler
func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *pattern, matches []string) {
var n *routingNode
host := r.URL.Host
escapedPath := r.URL.EscapedPath()
path := escapedPath
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
_, _, u := mux.matchOrRedirect(host, r.Method, path, r.URL)
if u != nil {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
}
// Redo the match, this time with r.Host instead of r.URL.Host.
// Pass a nil URL to skip the trailing-slash redirect logic.
n, matches, _ = mux.matchOrRedirect(r.Host, r.Method, path, nil)
} else {
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host = stripHostPort(r.Host)
path = cleanPath(path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
var u *url.URL
n, matches, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
if u != nil {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
}
if path != escapedPath {
// Redirect to cleaned path.
patStr := ""
if n != nil {
patStr = n.pattern.String()
}
u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
return RedirectHandler(u.String(), StatusMovedPermanently), patStr, nil, nil
}
}
if n == nil {
// We didn't find a match with the request method. To distinguish between
// Not Found and Method Not Allowed, see if there is another pattern that
// matches except for the method.
allowedMethods := mux.matchingMethods(host, path)
if len(allowedMethods) > 0 {
return HandlerFunc(func(w ResponseWriter, r *Request) {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
Error(w, StatusText(StatusMethodNotAllowed), StatusMethodNotAllowed)
}), "", nil, nil
}
return NotFoundHandler(), "", nil, nil
}
return n.handler, n.pattern.String(), n.pattern, matches
}
这个方法负责根据给定的 HTTP 请求 *Request
找到相应的处理函数 Handler
。这个方法是 ServeMux
的核心,它负责将请求路由到正确的处理函数。它处理了路径清理、重定向、方法匹配等多个方面,确保了请求能够被正确地处理。
var n *routingNode
host := r.URL.Host
escapedPath := r.URL.EscapedPath()
path := escapedPath
定义了一些变量,包括 n
用于存储路由节点,host
存储请求的主机名,escapedPath
和 path
存储请求的路径。
if r.Method == "CONNECT" {
// ...
}
对于 CONNECT
方法的请求,有特殊的处理逻辑。如果请求的路径没有注册处理器,会尝试进行重定向。
} else {
// ...
}
对于非 CONNECT
请求,会清除路径中的端口号,并清理路径(例如,将 /tree
转换为 /tree/
以保持路径的标准形式)。
n, matches, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
使用 matchOrRedirect
方法尝试找到匹配的处理器。如果找到了重定向的 URL,则返回重定向处理器和重定向的路径。
if path != escapedPath {
// ...
}
如果原始路径和清理后的路径不同,则需要重定向到清理后的路径。
if n == nil {
// ...
}
如果没有找到匹配的处理器,会检查是否有其他方法可以处理这个路径。如果有,返回一个方法不允许的处理器;如果没有,返回一个未找到处理器。
return n.handler, n.pattern.String(), n.pattern, matches
如果找到了匹配的处理器,返回处理器、路径模式字符串、路径模式对象和匹配的变量。
(8)matchOrRedirect
func (mux *ServeMux) matchOrRedirect(host, method, path string, u *url.URL) (_ *routingNode, matches []string, redirectTo *url.URL) {
mux.mu.RLock()
defer mux.mu.RUnlock()
n, matches := mux.tree.match(host, method, path)
// If we have an exact match, or we were asked not to try trailing-slash redirection,
// or the URL already has a trailing slash, then we're done.
if !exactMatch(n, path) && u != nil && !strings.HasSuffix(path, "/") {
// If there is an exact match with a trailing slash, then redirect.
path += "/"
n2, _ := mux.tree.match(host, method, path)
if exactMatch(n2, path) {
return nil, nil, &url.URL{Path: cleanPath(u.Path) + "/", RawQuery: u.RawQuery}
}
}
return n, matches, nil
}
这个方法用于检查给定的请求是否与任何注册的路由匹配,并且如果可能的话,提供重定向信息。
mux.mu.RLock()
defer mux.mu.RUnlock()
在开始查找之前,使用读锁来锁定 ServeMux
,以确保在查找路由时 ServeMux
的路由树不会被修改。这是为了保持线程安全。defer
关键字确保在函数返回时释放锁。
n, matches := mux.tree.match(host, method, path)
调用 mux.tree.match
方法来查找与请求的主机、方法和路径匹配的路由节点。这个方法返回一个路由节点 n
和一个匹配变量的切片 matches
。
if !exactMatch(n, path) && u != nil && !strings.HasSuffix(path, "/") {
// ...
}
如果找到的节点 n
与路径 path
不是精确匹配,并且请求的 URL 对象 u
不为空,且路径 path
不以斜杠 /
结尾,那么考虑进行重定向。
path += "/"
n2, _ := mux.tree.match(host, method, path)
if exactMatch(n2, path) {
return nil, nil, &url.URL{Path: cleanPath(u.Path) + "/", RawQuery: u.RawQuery}
}
如果路径 path
以斜杠 /
结尾时能找到精确匹配的路由节点 n2
,则构造一个新的 URL 对象,该对象的路径是原始路径加上尾随斜杠,并保留原始 URL 的查询参数。然后返回 nil
(表示没有找到匹配的路由节点),空的匹配变量切片,以及这个新的重定向 URL。
return n, matches, nil
如果没有重定向发生,或者没有找到带有尾随斜杠的匹配路由节点,那么返回原始找到的路由节点 n
,匹配变量切片 matches
,以及 nil
表示没有重定向。
三.客户端
1.核心数据结构
(1)Client
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
各字段解析:
Transport: Transport
字段是一个 RoundTripper
类型,它定义了如何发送 HTTP 请求和接收 HTTP 响应。RoundTripper
是一个接口,其 RoundTrip
方法负责完成一次 HTTP 请求和响应的往返行程。默认情况下,Transport
是 DefaultTransport
,但可以被替换以支持自定义的 HTTP 传输逻辑,例如添加自定义的重定向策略、代理、日志记录等。
CheckRedirect: CheckRedirect
字段是一个函数,用于在客户端遇到重定向时进行检查。这个函数接收两个参数:req
是即将发送的重定向请求,via
是之前的所有请求(包括原始请求和中间的重定向请求)。如果这个函数返回一个非 nil
错误,那么重定向将被停止,并且返回该错误。如果为 nil
,则重定向将继续进行。这个字段是可选的,如果为 nil
,则客户端会使用默认的重定向策略。
Jar: Jar
字段是一个 CookieJar
类型,它定义了如何处理 HTTP (cookies)。CookieJar
是一个接口,其 Cookies
和 SetCookies
方法分别用于获取和设置请求和响应中的 cookies。默认情况下,Jar
是 nil
,这意味着客户端不会自动处理 cookies。如果需要处理 cookies,可以设置一个实现了 CookieJar
接口的实例,例如 http.CookieJar
。
Timeout: Timeout
字段定义了客户端的超时时间。这个时间用于控制请求的最大等待时间。如果设置了超时时间,客户端在等待服务器响应时,如果超过了这个时间限制,请求将被取消,并返回一个超时错误。如果 Timeout
为 0,那么客户端将使用全局的默认超时设置。
(2)RoundTripper
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
RoundTrip
方法接收一个 *http.Request
类型的参数,表示要发送的 HTTP 请求,返回一个 *http.Response
类型的响应对象和一个错误值。如果请求成功发送并接收到响应,则返回非 nil
的 Response
和 nil
错误;如果请求失败,则返回 nil
Response
和非 nil
错误。
(3)Transport
type Transport struct {
idleConn map[connectMethodKey][]*persistConn // most recently used at end
// ...
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// ...
}
idleConn:idleConn
是一个映射,它存储了与不同服务器连接的空闲(持久)连接。这些连接被复用以提高性能,减少每次请求都需要建立新连接的开销。键 connectMethodKey
是一个自定义类型,用于唯一标识一个连接的方法和地址。值是 *persistConn
指针的切片,persistConn
是一个包含 net.Conn
的结构体,表示一个持久连接。
DialContext:DialContext
字段是一个函数,用于创建新的网络连接。它接收三个参数:一个 context.Context
对象,用于控制请求的取消和超时;一个字符串 network
,表示网络类型(如 "tcp"、"udp" 等);一个字符串 addr
,表示服务器的地址。函数返回一个 net.Conn
接口,表示建立的连接,以及一个错误值。如果 DialContext
为 nil
,则 Transport
会使用默认的 dialContext
函数来创建连接。
(4)Request
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
Host string
Form url.Values
Response *Response
ctx context.Context
// ...
}
Method:Method
字段存储了 HTTP 请求的方法,比如 "GET"、"POST"、"PUT"、"DELETE" 等。
URL:URL
字段是一个指向 url.URL
类型的指针,它包含了请求的 URL 信息,如路径、查询字符串等。
Header:Header
字段是一个 Header
类型,它实际上是 map[string][]string
类型的别名,存储了 HTTP 请求头。
Body:Body
字段是一个 io.ReadCloser
接口,它允许读取请求体的内容,并且在不需要时关闭请求体。
Host:Host
字段存储了请求中的 Host 头部字段,表示请求的目标主机和端口。
Form:Form
字段是一个 url.Values
类型,它是一个字符串映射,用于存储 URL 编码的表单数据。
Response:Response
字段是一个指向 Response
结构体的指针,它存储了与请求对应的 HTTP 响应。
ctx:ctx
字段是一个 context.Context
类型的值,它用于在请求处理过程中传递请求范围的值、取消信号和其他请求相关的信息。
(5)Response
type Response struct {
StatusCode int
Proto string
Header Header
Body io.ReadCloser
Request *Request
// ...
}
StatusCode:StatusCode
字段存储了 HTTP 响应的状态码,如 200(OK)、404(Not Found)等。
Proto:Proto
字段存储了 HTTP 协议版本,通常是 "HTTP/1.0" 或 "HTTP/1.1"。
Header:Header
字段是一个 Header
类型,实际上是 map[string][]string
类型的别名,存储了 HTTP 响应头。
Body:Body
字段是一个 io.ReadCloser
接口,它允许读取响应体的内容,并且在不再需要时关闭响应体。
Request:Request
字段是一个指向 Request
结构体的指针,它存储了生成此响应的原始 HTTP 请求。
2.请求流程
(1)DefaultClient
var DefaultClient = &Client{}
通过使用 &Client{}
创建了一个 Client
的实例,并将其地址赋值给 DefaultClient
。
(2)Post
func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
return DefaultClient.Post(url, contentType, body)
}
这段代码定义了一个名为 Post
的函数,它是一个简单的包装器,用于 DefaultClient
的 Post
方法。这个函数允许用户发送一个 HTTP POST 请求到指定的 url
,并且可以指定内容类型 contentType
和请求体 body
。
url
:要请求的 URL 地址。
contentType
:请求体的内容类型(MIME 类型),例如 "application/json" 或 "application/x-www-form-urlencoded"。
body
:请求体的内容,实现了 io.Reader
接口,可以是从字符串、文件或任何其他类型的 io.Reader
实现。
resp
:返回的 Response
对象,包含了服务器的响应。
err
:如果请求过程中出现错误,这个变量将被赋值。
(3)(c *Client) Post
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
req, err := NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}
这段代码是 net/http
包中 Client
结构体的 Post
方法的实现。这个方法用于发送一个 HTTP POST 请求,并返回服务器的响应和可能发生的错误。
req, err := NewRequest("POST", url, body)
使用 NewRequest
函数创建一个新的 Request
对象,指定请求方法为 "POST",URL 为 url
,请求体为 body
。如果创建请求失败,返回错误。
req.Header.Set("Content-Type", contentType)
设置请求头中的 "Content-Type" 字段,以指定请求体的内容类型。
return c.Do(req)
使用 Client
的 Do
方法发送创建好的请求 req
,并返回响应 resp
和可能发生的错误。
(4)NewRequest
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
(5)NewRequestWithContext
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
if method == "" {
method = "GET"
}
if !validMethod(method) {
return nil, fmt.Errorf("net/http: invalid method %q", method)
}
if ctx == nil {
return nil, errors.New("net/http: nil Context")
}
u, err := urlpkg.Parse(url)
if err != nil {
return nil, err
}
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = io.NopCloser(body)
}
// The host's colon:port should be normalized. See Issue 14836.
u.Host = removeEmptyPort(u.Host)
req := &Request{
ctx: ctx,
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
if body != nil {
switch v := body.(type) {
case *bytes.Buffer:
req.ContentLength = int64(v.Len())
buf := v.Bytes()
req.GetBody = func() (io.ReadCloser, error) {
r := bytes.NewReader(buf)
return io.NopCloser(r), nil
}
case *bytes.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
case *strings.Reader:
req.ContentLength = int64(v.Len())
snapshot := *v
req.GetBody = func() (io.ReadCloser, error) {
r := snapshot
return io.NopCloser(&r), nil
}
default:
}
if req.GetBody != nil && req.ContentLength == 0 {
req.Body = NoBody
req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
}
}
return req, nil
}
这个函数用于创建一个新的 Request
对象,并且允许指定一个 context.Context
对象,用于控制请求的取消和超时等行为。
if method == "" {
method = "GET"
}
如果 method
参数为空字符串,函数将其设置为 "GET"。
if !validMethod(method) {
return nil, fmt.Errorf("net/http: invalid method %q", method)
}
使用 validMethod
函数检查 method
是否是一个有效的 HTTP 方法。
if ctx == nil {
return nil, errors.New("net/http: nil Context")
}
如果 ctx
参数为 nil
,返回错误。
u, err := urlpkg.Parse(url)
使用 urlpkg.Parse
解析 url
参数,如果解析失败,返回错误。
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = io.NopCloser(body)
}
检查 body
是否实现了 io.ReadCloser
接口,如果没有,但 body
不为 nil
,则使用 io.NopCloser
包装 body
。
u.Host = removeEmptyPort(u.Host)
使用 removeEmptyPort
函数标准化 URL 的主机名,移除空的端口。
req := &Request{
ctx: ctx,
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: rc,
Host: u.Host,
}
创建一个新的 Request
对象,并设置其字段。
if body != nil {
// ...
}
如果 body
不为 nil
,则根据 body
的类型设置 ContentLength
和 GetBody
函数。
return req, nil
返回新创建的 Request
对象和 nil
错误。
(6)Do
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
(7)do
func (c *Client) do(req *Request) (retres *Response, reterr error) {
if testHookClientDoResult != nil {
defer func() { testHookClientDoResult(retres, reterr) }()
}
if req.URL == nil {
req.closeBody()
return nil, &url.Error{
Op: urlErrorOp(req.Method),
Err: errors.New("http: nil Request.URL"),
}
}
_ = *c // panic early if c is nil; see go.dev/issue/53521
var (
deadline = c.deadline()
reqs []*Request
resp *Response
copyHeaders = c.makeHeadersCopier(req)
reqBodyClosed = false // have we closed the current req.Body?
// Redirect behavior:
redirectMethod string
includeBody bool
)
uerr := func(err error) error {
// the body may have been closed already by c.send()
if !reqBodyClosed {
req.closeBody()
}
var urlStr string
if resp != nil && resp.Request != nil {
urlStr = stripPassword(resp.Request.URL)
} else {
urlStr = stripPassword(req.URL)
}
return &url.Error{
Op: urlErrorOp(reqs[0].Method),
URL: urlStr,
Err: err,
}
}
for {
// For all but the first request, create the next
// request hop and replace req.
if len(reqs) > 0 {
loc := resp.Header.Get("Location")
if loc == "" {
// While most 3xx responses include a Location, it is not
// required and 3xx responses without a Location have been
// observed in the wild. See issues #17773 and #49281.
return resp, nil
}
u, err := req.URL.Parse(loc)
if err != nil {
resp.closeBody()
return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
}
host := ""
if req.Host != "" && req.Host != req.URL.Host {
// If the caller specified a custom Host header and the
// redirect location is relative, preserve the Host header
// through the redirect. See issue #22233.
if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
host = req.Host
}
}
ireq := reqs[0]
req = &Request{
Method: redirectMethod,
Response: resp,
URL: u,
Header: make(Header),
Host: host,
Cancel: ireq.Cancel,
ctx: ireq.ctx,
}
if includeBody && ireq.GetBody != nil {
req.Body, err = ireq.GetBody()
if err != nil {
resp.closeBody()
return nil, uerr(err)
}
req.ContentLength = ireq.ContentLength
}
// Copy original headers before setting the Referer,
// in case the user set Referer on their first request.
// If they really want to override, they can do it in
// their CheckRedirect func.
copyHeaders(req)
// Add the Referer header from the most recent
// request URL to the new one, if it's not https->http:
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL, req.Header.Get("Referer")); ref != "" {
req.Header.Set("Referer", ref)
}
err = c.checkRedirect(req, reqs)
// Sentinel error to let users select the
// previous response, without closing its
// body. See Issue 10069.
if err == ErrUseLastResponse {
return resp, nil
}
// Close the previous response's body. But
// read at least some of the body so if it's
// small the underlying TCP connection will be
// re-used. No need to check for errors: if it
// fails, the Transport won't reuse it anyway.
const maxBodySlurpSize = 2 << 10
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
io.CopyN(io.Discard, resp.Body, maxBodySlurpSize)
}
resp.Body.Close()
if err != nil {
// Special case for Go 1 compatibility: return both the response
// and an error if the CheckRedirect function failed.
// See https://golang.org/issue/3795
// The resp.Body has already been closed.
ue := uerr(err)
ue.(*url.Error).URL = loc
return resp, ue
}
}
reqs = append(reqs, req)
var err error
var didTimeout func() bool
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// c.send() always closes req.Body
reqBodyClosed = true
if !deadline.IsZero() && didTimeout() {
err = &timeoutError{err.Error() + " (Client.Timeout exceeded while awaiting headers)"}
}
return nil, uerr(err)
}
var shouldRedirect bool
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
req.closeBody()
}
}
if testHookClientDoResult != nil {
defer func() { testHookClientDoResult(retres, reterr) }()
}
这一步检查是否有测试钩子 testHookClientDoResult
设置。如果有,它会在函数执行完毕后调用这个钩子,并传入返回的响应和错误。
if req.URL == nil {
req.closeBody()
return nil, &url.Error{...}
}
这一步确保请求对象 req
中的 URL
字段不为空。如果为空,则关闭请求体并返回一个错误。
_ = *c // panic early if c is nil
这一步是为了避免在方法执行过程中的某个阶段才检测到客户端 c
为 nil
,从而导致延迟 panic。这里通过解引用 c
来尽早发现 c
是否为 nil
。
var (
deadline = c.deadline()
reqs []*Request
resp *Response
copyHeaders = c.makeHeadersCopier(req)
reqBodyClosed = false
redirectMethod string
includeBody bool
)
这一步定义了多个局部变量,包括处理请求的截止时间、请求数组、响应对象、复制头部的函数、请求体是否已关闭的标志、重定向方法和是否包含请求体的标志。
uerr := func(err error) error {
// ...
}
定义了一个匿名函数 uerr
,用于包装错误,并添加操作和 URL 信息,以便返回一个 url.Error
类型的错误。
for {
// ...
}
进入一个无限循环,处理重定向逻辑。
loc := resp.Header.Get("Location")
if loc == "" {
return resp, nil
}
如果响应中包含 Location
头,则尝试解析它。如果没有 Location
头,说明不需要重定向,直接返回当前响应。
u, err := req.URL.Parse(loc)
// ...
req = &Request{...}
创建一个新的请求对象,用于下一次重定向请求。
copyHeaders(req)
复制原始请求的头部到新请求,保留用户设置的头部信息。
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL, req.Header.Get("Referer")); ref != "" {
req.Header.Set("Referer", ref)
}
根据当前请求和新请求的 URL 设置 Referer
头部。
err = c.checkRedirect(req, reqs)
调用 checkRedirect
方法检查重定向是否被允许,并处理用户定义的重定向逻辑。
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// ...
}
调用 c.send
方法发送请求,并处理可能的超时错误。
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
根据响应和请求方法确定是否应该重定向。如果不重定向,则返回当前响应。
req.closeBody()
关闭当前请求体,准备下一次重定向请求。
(8)(c *Client) send
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
if c.Jar != nil {
for _, cookie := range c.Jar.Cookies(req.URL) {
req.AddCookie(cookie)
}
}
resp, didTimeout, err = send(req, c.transport(), deadline)
if err != nil {
return nil, didTimeout, err
}
if c.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
c.Jar.SetCookies(req.URL, rc)
}
}
return resp, nil, nil
}
这个方法负责发送一个 HTTP 请求并返回响应,同时处理 cookies。
if c.Jar != nil {
for _, cookie := range c.Jar.Cookies(req.URL) {
req.AddCookie(cookie)
}
}
如果客户端 c
有一个 Jar
(用于管理 cookies),则从 Jar
中获取与请求 URL 相关的所有 cookies,并添加到请求 req
中。
resp, didTimeout, err = send(req, c.transport(), deadline)
调用 send
函数(这里是简化的实现,通常 send
会是 Transport
的方法),传入请求和截止时间,返回响应、是否超时的函数和错误。
if err != nil {
return nil, didTimeout, err
}
如果发送请求出现错误,则直接返回错误和是否超时的函数。
if c.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
c.Jar.SetCookies(req.URL, rc)
}
}
如果客户端 c
有一个 Jar
,并且响应 resp
中包含 cookies,则将这些 cookies 保存到 Jar
中。
return resp, nil, nil
返回响应、nil
(表示没有超时)和 nil
错误。
(9)DefaultTransport
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
这是一个全局变量,用于在没有指定自定义 Transport
时,作为 http.Client
的默认运输层。
(10)transport()
func (c *Client) transport() RoundTripper {
if c.Transport != nil {
return c.Transport
}
return DefaultTransport
}
这个方法用于获取 Client
实例的 RoundTripper
,它负责执行请求的发送和响应的接收。
if c.Transport != nil {
return c.Transport
}
如果 Client
实例的 Transport
字段不为空(即已经被设置为一个自定义的 RoundTripper
),则直接返回这个自定义的 RoundTripper
。
return DefaultTransport
如果 Client
实例没有设置自定义的 Transport
,则返回 DefaultTransport
,它是 http
包提供的默认 RoundTripper
实现。
(11)send
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
// ...
resp, err = rt.RoundTrip(req)
// ...
return resp, nil, nil
}
resp, err = rt.RoundTrip(req)
这一步调用 RoundTripper
的 RoundTrip
方法发送请求 ireq
并接收响应。RoundTrip
方法会处理请求的发送和响应的接收,并返回响应对象和可能发生的错误。
(12)RoundTripper
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
(13)RoundTrip
func (t *Transport) roundTrip(req *Request) (*Response, error) {
// ...
for {
// ...
treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey}
// ...
pconn, err := t.getConn(treq, cm)
// ...
resp, err = pconn.roundTrip(treq)
// ...
}
}
这个方法负责执行一个HTTP请求并返回相应的响应。
treq := &transportRequest{Request: req, trace: trace, ctx: ctx, cancel: cancel}
为每次重试创建一个新的transportRequest
。
pconn, err := t.getConn(treq, cm)
获取连接。
var resp *Response
if pconn.alt != nil {
resp, err = pconn.alt.RoundTrip(req)
} else {
resp, err = pconn.roundTrip(treq)
}
根据连接类型(HTTP/1或HTTP/2),发送请求并获取响应。
(14)getConn
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (_ *persistConn, err error) {
req := treq.Request
trace := treq.trace
ctx := req.Context()
if trace != nil && trace.GetConn != nil {
trace.GetConn(cm.addr())
}
// Detach from the request context's cancellation signal.
// The dial should proceed even if the request is canceled,
// because a future request may be able to make use of the connection.
//
// We retain the request context's values.
dialCtx, dialCancel := context.WithCancel(context.WithoutCancel(ctx))
w := &wantConn{
cm: cm,
key: cm.key(),
ctx: dialCtx,
cancelCtx: dialCancel,
result: make(chan connOrError, 1),
beforeDial: testHookPrePendingDial,
afterDial: testHookPostPendingDial,
}
defer func() {
if err != nil {
w.cancel(t, err)
}
}()
// Queue for idle connection.
if delivered := t.queueForIdleConn(w); !delivered {
t.queueForDial(w)
}
// Wait for completion or cancellation.
select {
case r := <-w.result:
// Trace success but only for HTTP/1.
// HTTP/2 calls trace.GotConn itself.
if r.pc != nil && r.pc.alt == nil && trace != nil && trace.GotConn != nil {
info := httptrace.GotConnInfo{
Conn: r.pc.conn,
Reused: r.pc.isReused(),
}
if !r.idleAt.IsZero() {
info.WasIdle = true
info.IdleTime = time.Since(r.idleAt)
}
trace.GotConn(info)
}
if r.err != nil {
// If the request has been canceled, that's probably
// what caused r.err; if so, prefer to return the
// cancellation error (see golang.org/issue/16049).
select {
case <-treq.ctx.Done():
err := context.Cause(treq.ctx)
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
default:
// return below
}
}
return r.pc, r.err
case <-treq.ctx.Done():
err := context.Cause(treq.ctx)
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
}
}
这个方法的目的是获取一个与服务器的连接,无论是重用现有的空闲连接还是创建一个新的连接。它处理了连接的获取、错误处理、请求取消等多种情况。
req := treq.Request
trace := treq.trace
ctx := req.Context()
从transportRequest
结构体中获取请求对象和追踪信息,以及请求的上下文。
if trace != nil && trace.GetConn != nil {
trace.GetConn(cm.addr())
}
如果请求中有追踪GetConn
事件的函数,调用它并传递服务器地址。
dialCtx, dialCancel := context.WithCancel(context.WithoutCancel(ctx))
创建一个新的上下文dialCtx
,它与请求上下文ctx
分离,即使请求被取消,拨号操作也会继续进行。这是因为未来的请求可能能够利用这个连接。
w := &wantConn{
cm: cm,
key: cm.key(),
ctx: dialCtx,
cancelCtx: dialCancel,
result: make(chan connOrError, 1),
beforeDial: testHookPrePendingDial,
afterDial: testHookPostPendingDial,
}
创建一个wantConn
结构体,用于存储连接方法、键、上下文、取消函数、结果通道以及测试钩子。
defer func() {
if err != nil {
w.cancel(t, err)
}
}()
如果getConn
方法返回错误,调用wantConn
的cancel
方法来取消拨号。
if delivered := t.queueForIdleConn(w); !delivered {
t.queueForDial(w)
}
尝试将wantConn
放入等待空闲连接的队列,如果队列已满,则放入拨号队列。
select {
case r := <-w.result:
// ...
case <-treq.ctx.Done():
// ...
}
如果wantConn
的结果是一个连接,则根据是否重用、是否追踪等条件处理。如果结果是一个错误,则检查请求是否被取消,并返回相应的错误。
case <-treq.ctx.Done():
err := context.Cause(treq.ctx)
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
如果请求上下文被取消,则返回取消错误。
(15)queueForIdleConn
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
if t.DisableKeepAlives {
return false
}
t.idleMu.Lock()
defer t.idleMu.Unlock()
// Stop closing connections that become idle - we might want one.
// (That is, undo the effect of t.CloseIdleConnections.)
t.closeIdle = false
if w == nil {
// Happens in test hook.
return false
}
// If IdleConnTimeout is set, calculate the oldest
// persistConn.idleAt time we're willing to use a cached idle
// conn.
var oldTime time.Time
if t.IdleConnTimeout > 0 {
oldTime = time.Now().Add(-t.IdleConnTimeout)
}
// Look for most recently-used idle connection.
if list, ok := t.idleConn[w.key]; ok {
stop := false
delivered := false
for len(list) > 0 && !stop {
pconn := list[len(list)-1]
// See whether this connection has been idle too long, considering
// only the wall time (the Round(0)), in case this is a laptop or VM
// coming out of suspend with previously cached idle connections.
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
if tooOld {
// Async cleanup. Launch in its own goroutine (as if a
// time.AfterFunc called it); it acquires idleMu, which we're
// holding, and does a synchronous net.Conn.Close.
go pconn.closeConnIfStillIdle()
}
if pconn.isBroken() || tooOld {
// If either persistConn.readLoop has marked the connection
// broken, but Transport.removeIdleConn has not yet removed it
// from the idle list, or if this persistConn is too old (it was
// idle too long), then ignore it and look for another. In both
// cases it's already in the process of being closed.
list = list[:len(list)-1]
continue
}
delivered = w.tryDeliver(pconn, nil, pconn.idleAt)
if delivered {
if pconn.alt != nil {
// HTTP/2: multiple clients can share pconn.
// Leave it in the list.
} else {
// HTTP/1: only one client can use pconn.
// Remove it from the list.
t.idleLRU.remove(pconn)
list = list[:len(list)-1]
}
}
stop = true
}
if len(list) > 0 {
t.idleConn[w.key] = list
} else {
delete(t.idleConn, w.key)
}
if stop {
return delivered
}
}
// Register to receive next connection that becomes idle.
if t.idleConnWait == nil {
t.idleConnWait = make(map[connectMethodKey]wantConnQueue)
}
q := t.idleConnWait[w.key]
q.cleanFrontNotWaiting()
q.pushBack(w)
t.idleConnWait[w.key] = q
return false
}
这个方法的主要目的是重用空闲的持久连接,以减少连接建立的开销。它通过检查空闲连接列表、移除损坏或过时的连接,并尝试将可用连接分配给请求来实现这一点。
(16)queueForDial
func (t *Transport) queueForDial(w *wantConn) {
w.beforeDial()
t.connsPerHostMu.Lock()
defer t.connsPerHostMu.Unlock()
if t.MaxConnsPerHost <= 0 {
t.startDialConnForLocked(w)
return
}
if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
if t.connsPerHost == nil {
t.connsPerHost = make(map[connectMethodKey]int)
}
t.connsPerHost[w.key] = n + 1
t.startDialConnForLocked(w)
return
}
if t.connsPerHostWait == nil {
t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
}
q := t.connsPerHostWait[w.key]
q.cleanFrontNotWaiting()
q.pushBack(w)
t.connsPerHostWait[w.key] = q
}
这个方法的主要目的是控制每个主机的并发连接数,以避免过多的并发连接导致资源耗尽。它通过检查当前主机的连接数,并在达到最大值时将请求排队等待,直到有可用的连接槽位。如果没有连接数限制,则直接开始拨号。