当前位置: 首页 > article >正文

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 会使用这个字段来实现一个不同的路由匹配逻辑,用于调试和测试。

作用:

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
}

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


http://www.kler.cn/a/416787.html

相关文章:

  • Pytorch使用手册- PyTorch 中 non_blocking 和 pin_memory() 的使用指南(专题十一)
  • SpringBoot开发——详细讲解 Spring Boot 项目中的 POM 配置
  • 在oracle下载jdk显示400 Bad Request Request Header Or Cookie Too Large
  • 洛谷P1115
  • 麒麟系统x86安装达梦数据库
  • JMeter实时性能压测可视化系统整合
  • 【设计模式系列】解释器模式(十七)
  • HTTP的主要特点
  • Java面试之多线程并发篇(9)
  • 单机环境下Caffeine和Redis两级缓存的实现与问题解决
  • 记一次 Golang pkg 性能提升 38147125738.8 倍之旅
  • 网络安全(一):信息收集之玩转nmap(理论篇)
  • C++多态---面向对象的心动信号:多态之美
  • Vue 将推出「无虚拟DOM」版本,又是新的前端框架趋势?
  • uniapp中父组件数组更新后与页面渲染数组不一致实战记录
  • 力扣98:验证二叉搜索树
  • [CTF/网络安全] 攻防世界 upload1 解题详析
  • js 中 file 文件 应用
  • 应急响应靶机——Windows挖矿事件
  • Istio笔记01--快速体验Istio
  • 使用 Spring AI + Elasticsearch 让 RAG 变得简单
  • jmeter基础(超详细总结)
  • 第五讲:运算符与表达式:算术、关系、逻辑、赋值等运算符及其优先级
  • 超级灵感:前端页面功能统一管理方案
  • Flutter 权限申请
  • 数据结构——有序二叉树的构建遍历查找