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 : 一个树状结构,用于高效地匹配长路径。routingNodeServeMux 内部使用的节点类型,用于构建路由树。

index : 一个索引结构,用于快速匹配短路径。routingIndexServeMux 内部使用的索引类型,用于存储和查找短路径的路由。

patterns : 存储所有添加到 ServeMux 中的路由模式。每个 pattern 表示一个路由模式,例如 /path/:param。这个字段用于存储这些模式,以便在添加或匹配路由时使用。

mux121 : 这是一个仅在特定调试模式下使用的字段。当环境变量 GODEBUG=httpmuxgo121=1 被设置时,ServeMux 会使用这个字段来实现一个不同的路由匹配逻辑,用于调试和测试。

作用:

ServeMuxhttp 包中的核心组件,它允许开发者为不同的URL路径指定不同的处理函数。通过调用 http.HandleFunchttp.Handle 方法,可以将URL路径与处理函数关联起来。当HTTP服务器接收到请求时,ServeMux 会根据请求的URL找到相应的处理函数,并调用它来处理请求。

2.注册 handler

(1)DefaultServeMux

复制代码
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

DefaultServeMuxhttp 包中预定义的全局 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行为。

具体来说,当use121true时,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是否为空字符串,是则返回一个错误,表示无效的模式。

如果handlernil,返回一个错误,表示处理函数为空。

如果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.treemux.indexmux.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 结构体的实例,并将传入的 addrhandler 赋值给这个实例的 AddrHandler 字段。然后,函数调用 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
}

如果服务器没有指定自定义的处理器,那么使用默认的多路复用器 DefaultServeMuxDefaultServeMux 是一个全局的 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,还会在响应头中设置 Connectionclose,表示关闭连接。

复制代码
var h Handler
if use121 {
    h, _ = mux.mux121.findHandler(r)
} else {
    h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}

这段代码尝试找到与请求 URL 匹配的处理函数。muxServeMux 的一个实例,它维护了一个 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 存储请求的主机名,escapedPathpath 存储请求的路径。

复制代码
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 请求和响应的往返行程。默认情况下,TransportDefaultTransport,但可以被替换以支持自定义的 HTTP 传输逻辑,例如添加自定义的重定向策略、代理、日志记录等。

CheckRedirect: CheckRedirect 字段是一个函数,用于在客户端遇到重定向时进行检查。这个函数接收两个参数:req 是即将发送的重定向请求,via 是之前的所有请求(包括原始请求和中间的重定向请求)。如果这个函数返回一个非 nil 错误,那么重定向将被停止,并且返回该错误。如果为 nil,则重定向将继续进行。这个字段是可选的,如果为 nil,则客户端会使用默认的重定向策略。

Jar: Jar 字段是一个 CookieJar 类型,它定义了如何处理 HTTP (cookies)。CookieJar 是一个接口,其 CookiesSetCookies 方法分别用于获取和设置请求和响应中的 cookies。默认情况下,Jarnil,这意味着客户端不会自动处理 cookies。如果需要处理 cookies,可以设置一个实现了 CookieJar 接口的实例,例如 http.CookieJar

Timeout: Timeout 字段定义了客户端的超时时间。这个时间用于控制请求的最大等待时间。如果设置了超时时间,客户端在等待服务器响应时,如果超过了这个时间限制,请求将被取消,并返回一个超时错误。如果 Timeout 为 0,那么客户端将使用全局的默认超时设置。

(2)RoundTripper

复制代码
type RoundTripper interface {
	RoundTrip(*Request) (*Response, error)
}

RoundTrip 方法接收一个 *http.Request 类型的参数,表示要发送的 HTTP 请求,返回一个 *http.Response 类型的响应对象和一个错误值。如果请求成功发送并接收到响应,则返回非 nilResponsenil 错误;如果请求失败,则返回 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 接口,表示建立的连接,以及一个错误值。如果 DialContextnil,则 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 的函数,它是一个简单的包装器,用于 DefaultClientPost 方法。这个函数允许用户发送一个 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)

使用 ClientDo 方法发送创建好的请求 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 的类型设置 ContentLengthGetBody 函数。

复制代码
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

这一步是为了避免在方法执行过程中的某个阶段才检测到客户端 cnil,从而导致延迟 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)

这一步调用 RoundTripperRoundTrip 方法发送请求 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方法返回错误,调用wantConncancel方法来取消拨号。

复制代码
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
}

这个方法的主要目的是控制每个主机的并发连接数,以避免过多的并发连接导致资源耗尽。它通过检查当前主机的连接数,并在达到最大值时将请求排队等待,直到有可用的连接槽位。如果没有连接数限制,则直接开始拨号。

相关推荐
Tony Bai4 小时前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
小阿宁的猫猫5 小时前
CSRF漏洞的原理、防御和比赛中的运用
安全·http·xss·csrf
小徐Chao努力9 小时前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang
锥锋骚年9 小时前
go语言异常处理方案
开发语言·后端·golang
教练、我想打篮球10 小时前
120 同样的 url, header, 参数, 使用 OkHttp 能够成功获取数据, 使用 RestTemplate 报错
http·okhttp·resttemplate·accept
zfj32110 小时前
websocket为什么需要在tcp连接成功后先发送一个标准的http请求,然后在当前tcp连接上升级协议成websocket
websocket·tcp/ip·http
杀手不太冷!10 小时前
Jenkins的安装与使用;git clone url的时候,url为http和ssh时候的区别
git·http·jenkins
moxiaoran575311 小时前
Go语言的map
开发语言·后端·golang
小信啊啊11 小时前
Go语言数组
开发语言·后端·golang
IT艺术家-rookie11 小时前
golang-- sync.WaitGroup 和 errgroup.Group 详解
开发语言·后端·golang