Go开发企业级微服务网关(3)Golang 创建 HTTP 服务器和客户端

目录:

  • 服务端代码展示
  • 客户端代码展示
  • HTTP 服务器源码解读
    • 函数是一等公民
    • 注册原理
    • 服务启动与请求处理流程
  • HTTP 客户端源码解读
    • 请求流程
    • Transport RoundTrip 流程

服务端代码如下:

package main

import (
	"log"
	"net/http"
	"time"
)

var (
	Addr = ":1210"
)

func main() {
	// 创建路由器
	mux := http.NewServeMux()
	// 设置路由规则
	mux.HandleFunc("/bye", sayBye)
	// 创建服务器
	server := &http.Server{
		Addr:         Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	// 监听端口并提供服务
	log.Println("Starting httpserver at " + Addr)
	log.Fatal(server.ListenAndServe())
}

func sayBye(w http.ResponseWriter, r *http.Request) {
	time.Sleep(1 * time.Second)
	w.Write([]byte("bye bye, this is httpServer"))
}

客户端代码如下:

package main

import (
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"time"
)

func main() {
	// 创建连接池
	transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 连接超时
			KeepAlive: 30 * time.Second, // 长连接超时时间
		}).DialContext,
		MaxIdleConns:          100,              // 最大空闲连接
		IdleConnTimeout:       90 * time.Second, // 空闲超时时间
		TLSHandshakeTimeout:   10 * time.Second, // tls握手超时时间
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue状态码超时时间
	}
	// 创建客户端
	client := &http.Client{
		Timeout:   30 * time.Second, // 请求超时时间
		Transport: transport,
	}
	// 请求数据
	resp, err := client.Get("http://127.0.0.1:1210/bye")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	// 读取内容
	bds, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(bds))
}

HTTP 服务器源码解读

HTTP是工作中最常接触的协议

函数是一等公民

把函数作为一种数据类型去传递,去实现函数的回调(匿名函数、闭包)

这种定义可以将普通函数包装成实现 http.Handler 接口的对象

注册原理

本质上是有这样一个结构体:

type ServeMux struct {
	mu     sync.RWMutex
	tree   routingNode
	index  routingIndex
	mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
// A routingNode is a node in the decision tree.
// The same struct is used for leaf and interior nodes.
type routingNode struct {
	// A leaf node holds a single pattern and the Handler it was registered
	// with.
	pattern *pattern
	handler Handler

	// An interior node maps parts of the incoming request to child nodes.
	// special children keys:
	//     "/"	trailing slash (resulting from {$})
	//	   ""   single wildcard
	children   mapping[string, *routingNode]
	multiChild *routingNode // child with multi wildcard
	emptyChild *routingNode // optimization: child with key ""
}

那注册路由的过程实际上传入的就是一个 pattern 和一个 handler 方法。

内部的本质实现就是向这个树形结构里插入节点:

  1. 先按 host(如果有)添加子节点。
  2. 再按 method(如 GETPOST 或空字符串表示任意方法)添加子节点。
  3. 最后按路径的 segments 逐层添加,并在叶子节点存储 pattern 和 handler
func (root *routingNode) addPattern(p *pattern, h Handler) {
	// First level of tree is host.
	n := root.addChild(p.host)
	// Second level of tree is method.
	n = n.addChild(p.method)
	// Remaining levels are path.
	n.addSegments(p.segments, p, h)
}

服务启动与请求处理流程

启动监听:调用 s.ListenAndServe()

  1. 确定监听地址(默认 :http 即 80 端口)
  2. 创建 TCP 监听器 ln
  3. 将监听器交给 s.Serve(ln) 进一步处理

接受连接s.Serve(ln) 循环调用 ln.Accept()

for {
    rw, err := l.Accept()
    // 每个新连接创建一个 goroutine 处理
    go (c *conn).serve(ctx)
}

每个连接独立并发处理。

处理连接c.serve() 内部循环读取请求并调用 handler

  • 读取 HTTP 请求,构造 ResponseWriter 和 Request
  • 调用 serverHandler{c.server}.ServeHTTP(w, req) 找到并执行实际处理函数

路由到最终 handlerserverHandler.ServeHTTP

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux   // 默认使用标准库的路由器
    }
    // 特殊处理 OPTIONS * 请求
    handler.ServeHTTP(rw, req)
}

如果用户设置了自定义 Server.Handler,则使用之;否则使用 DefaultServeMux(它实现了路由规则匹配)。

最终响应:handler 处理完请求后,通过 ResponseWriter 返回数据,连接关闭或复用。

HTTP 客户端源码解读

请求流程

  • Timeout:从连接建立到读完响应体的总超时,零值表示不设超时(可能永久阻塞)。
  • Transport:真正执行网络通信的接口(http.RoundTripper),默认实现支持连接池、TLS 配置等。
	client := &http.Client{
		Timeout:   30 * time.Second, // 请求超时时间
		Transport: transport,
	}
  • NewRequest 构造一个 http.Request,包含方法、URL、可选的 body。
  • 实际处理交给 Do 方法,以便统一处理所有 HTTP 方法。
func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}

Do 本身只是加了一些文档说明和校验,核心逻辑在 do 方法中。

func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}

无限循环,实现发送操作:

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)
		}

具体的 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
}

在更内部的 send 函数中(p.s 怎么这么多层)去调用 RoundTrip 方法以得到 resp ,并最终逐层返回。

resp, err = rt.RoundTrip(req)

Transport RoundTrip 流程

RoundTrip 是 transport.go 中的 func (t *Transport) roundTrip(req *Request) (_ *Response, err error) 方法

在其中先执行 getConn 方法:

  1. 尝试拿去空闲链接
        // Queue for idle connection.
        if delivered := t.queueForIdleConn(w); !delivered {
            t.queueForDial(w)
        }
  2. 如果拿不到进入 select 并等待完成或者取消
// 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
	}

这段代码的核心是 “优先返回最准确的错误”。它确保了在请求被取消时,调用方总能收到 context.Canceled 或 context.DeadlineExceeded 这类明确的错误,而不是在网络层被包装过的其他错误。

而下面这段代码就实现了最终得到连接的过程:

		// Get the cached or newly-created connection to either the
		// host (for http or https), the http proxy, or the http proxy
		// pre-CONNECTed to https server. In any case, we'll be ready
		// to send it requests.
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			req.closeBody()
			return nil, err
		}

		var resp *Response
		if pconn.alt != nil {
			// HTTP/2 path.
			resp, err = pconn.alt.RoundTrip(req)
		} else {
			resp, err = pconn.roundTrip(treq)
		}
		if err == nil {
			if pconn.alt != nil {
				// HTTP/2 requests are not cancelable with CancelRequest,
				// so we have no further need for the request context.
				//
				// On the HTTP/1 path, roundTrip takes responsibility for
				// canceling the context after the response body is read.
				cancel(errRequestDone)
			}
			resp.Request = origReq
			return resp, nil
		}

		// Failed. Clean up and determine whether to retry.
		if http2isNoCachedConnError(err) {
			if t.removeIdleConn(pconn) {
				t.decConnsPerHost(pconn.cacheKey)
			}
		} else if !pconn.shouldRetryRequest(req, err) {
			// Issue 16465: return underlying net.Conn.Read error from peek,
			// as we've historically done.
			if e, ok := err.(nothingWrittenError); ok {
				err = e.error
			}
			if e, ok := err.(transportReadFromServerError); ok {
				err = e.err
			}
			if b, ok := req.Body.(*readTrackingBody); ok && !b.didClose {
				// Issue 49621: Close the request body if pconn.roundTrip
				// didn't do so already. This can happen if the pconn
				// write loop exits without reading the write request.
				req.closeBody()
			}
			return nil, err
		}
		testHookRoundTripRetried()

		// Rewind the body if we're able to.
		req, err = rewindBody(req)
		if err != nil {
			return nil, err
		}

上面是 HTTP/2 的实现流程,在 HTTP/1.X 中稍有区别。完成流程图参考如下:

Client 、Transport 配置

transport := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 连接超时
			KeepAlive: 30 * time.Second, // 长连接超时时间
		}).DialContext,
		MaxIdleConns:          100,              // 最大空闲连接
		IdleConnTimeout:       90 * time.Second, // 空闲超时时间
		TLSHandshakeTimeout:   10 * time.Second, // tls握手超时时间
		ExpectContinueTimeout: 1 * time.Second,  // 100-continue状态码超时时间
	}

从 Client.Do 开始,到请求结束(连接进入空闲状态),整个过程可以拆解为以下关键阶段:

  • Client.Do:整个请求的起始点,对应总超时。
  • Dial:建立 TCP 连接。
  • TLS handshake:如果使用 HTTPS,进行 TLS 握手。
  • Request:发送 HTTP 请求头部和正文(如果有)。
  • Resp. headers:读取服务器返回的响应头部。
  • Response body:读取响应正文。
  • Idle:请求完成后,连接进入空闲状态,等待复用。
阶段对应超时字段说明
整个请求http.Client.Timeout从 Client.Do 开始,到读完响应正文的总时间。如果超时,请求会被取消。
TCP 连接建立net.Dialer.Timeout建立 TCP 连接的最大等待时间(包括域名解析)。
TLS 握手http.Transport.TLSHandshakeTimeoutHTTPS 请求中,TLS 握手允许的最大时间。
发送请求(无独立超时)Go 没有单独的“请求发送”超时,发送过程通常很快,但如果网络阻塞,可能受底层 TCP 写超时影响,但一般不单独设置。
读取响应头http.Transport.ResponseHeaderTimeout从写完请求到读完响应头部允许的最大时间。如果服务器响应慢,此超时生效。
读取响应体(无独立超时)读取正文没有独立超时,但可以通过 Client.Timeout 整体控制,或通过 context.WithTimeout 在请求级别设置更细粒度的超时。
连接空闲http.Transport.IdleConnTimeout连接在连接池中保持空闲的最大时间,超时后会被关闭。

发表评论