红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【Golang】Http标准库

参考视频:

【Golang http 标准库底层原理解析】

简单用法

GET/POST请求

使用http.Get()http.Post()可以发起简单的请求,返回一个指针和错误,调用后需要手动关闭

GET:

1
2
3
4
5
6
7
8
9
10
func main() {
  resp, err := http.Get("https://baidu.com")
  if err != nil {
    fmt.Println(err)
    return
  }
  defer resp.Body.Close()
  content, err := io.ReadAll(resp.Body)
  fmt.Println(string(content))
}

POST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
   person := Person{
      UserId:   "120",
      Username: "jack",
      Age:      18,
      Address:  "usa",
   }

   json, _ := json.Marshal(person)
   reader := bytes.NewReader(json)

   resp, err := http.Post("https://golang.org", "application/json;charset=utf-8", reader)
   if err != nil {
      fmt.Println(err)
   }
   defer resp.Body.Close()
}

配置客户端

若需要客户端携带Head,设置超时等,就需要自己配置一个客户端,会用到http.Client{}结构体,配置项如下:

  • Transport:配置 Http 客户端数据传输相关的配置项,没有就采用默认的策略

  • Timeout:请求超时时间配置

  • Jar:Cookie 相关配置

  • CheckRedirect:重定向配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    client := &http.Client{
        Timeout: 30 * time.Second,
        Transport: &http.Transport{
            MaxIdleConns:        10,
            IdleConnTimeout:     90 * time.Second,
            DisableCompression:  true,
        },
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            if len(via) >= 10 {
                return errors.New("too many redirects")
            }
            return nil
        },
        Jar: cookieJar, // 可以自动管理 cookie
    }

并通过 client.Do()发送请求,通过.Header.Add()添加请求头

1
2
3
4
5
6
7
func main() {
   client := &http.Client{}
   request, _ := http.NewRequest("GET", "https://golang.org", nil)
   request.Header.Add("Authorization","123456")
   resp, _ := client.Do(request)
   defer resp.Body.Close()
}

处理相应的时候要注意:

  • 处理完后关闭resp.Body
  • 检查resp.StatusCode,HTTP状态码这些不会作为error返回
1
2
3
4
if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("bad status: %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)

配置服务端

  • http.HandleFunc()用于注册路由,第一个参数是接口,第二个参数可以将一个普通函数作为HTTP处理器使用,该函数携带一个http.ResponseWriter来写响应,*http.Request为请求
  • http.ListenAndServe()可以直接泡一个服务器,第一个参数是监听的地址,可以只写端口,前面默认localhost;第二个参数是处理器,若为nil则使用默认的处理器
1
2
3
4
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", nil)

以下方法用于获取请求信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
func handler(w http.ResponseWriter, r *http.Request) {
    // 方法
    method := r.Method
    // URL 参数 /user?id=1
    id := r.URL.Query().Get("id")
    // 路径参数(需配合路由库,标准库只支持前缀匹配)
    path := r.URL.Path
    // 请求头
    auth := r.Header.Get("Authorization")
    // Body
    body, _ := io.ReadAll(r.Body)
    defer r.Body.Close()
}

同时可以自定义server端结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
}
go func() {
    if err := srv.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}()

// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

通过Header可以设置返回的响应的格式

  • 纯文本:

    1
    2
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("OK"))
  • JSON:

    1
    2
    3
    4
    5
    6
    7
    type User struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
    }
    data := User{Name: "John", Age: 30}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data) // 或者 json.Marshal + w.Write

在Write前可用WriteHeader()设置状态码,若不调用WriteHeader默认返回200 OK

go中自带了常用状态码的常量名

1
2
w.WriteHeader(http.StatusCreated) // 必须在 Write 之前调用
w.Write([]byte("created"))

简单创建一个服务端,注册路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request){
		fmt.Println("Req: ping")
		w.Write([]byte("pone"))
	})
	http.ListenAndServe(":8080", nil)
}

客户端写一个单测,向服务端发送一个post请求,得到服务端返回的pone以及error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package client

import (
	"io"
	"net/http"
	"testing"
)
func Test_client(t *testing.T) {
	resp, err := http.Post("http://localhost:8080/ping", "", nil)
	if err != nil {
		t.Error(err)
		return
	}

	body, _ :=io.ReadAll(resp.Body)
	defer resp.Body.Close()

	t.Log(string(body))
	t.Error("test failed")
}

服务端

Server

整个HTTP服务端模块被封装在Server类中,包括两个核心字段:

  • Addr string:服务端的地址

  • Handler Handler:路由处理器

    它的类型是一个Handler接口,其中定义了一个方法ServeHTTP,用于根据Http请求中的请求路径映射到对应的handler处理函数,对请求处理和响应

路由处理器用于处理客户端请求,根据请求参数中的路径映射到对应的处理方法,再调用对应方法并返回结果

ServerMux

对Handler的具体实现,内部维护了一个map用于映射从path到对应处理方法的关系

1
2
3
4
5
6
type serveMux121 struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}
  • 通过一把读写锁保证并发安全性
  • 使用map建立映射,以请求路径为key,对应处理函数作为value
  • es是一个切片,用于前置匹配

其中muxEntry是一个handler单元,包含了path和处理函数handler两部分,它冗余了一个path,将key和value都保存了下来,同样使用了Handler接口,也需要请求和响应的使用

1
2
3
4
type muxEntry struct {
	h       Handler
	pattern string
}

Handler注册流程

方法链:http.handleFunc()作为入口方法调用,进而会调用ServerMux.handleFunc()ServerMux.Handle()

http包下声明了一个单例var defaultServeMux ServeMux,当使用http.handleFunc()注册handler时,会使用该默认单例注册

1
2
3
4
5
6
func (mux *serveMux121) handleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
	mux.handle(pattern, HandlerFunc(handler))
}

HandleFunc函数接受路径和处理函数两个变量,并调用默认单例下的HandleFunc函数,该函数会将我们的处理函数handler转化为一个HandlerFunc类型,这个类型内部实现了Handler接口中需要的ServeHTTP方法,即调用函数自身,转换后会调用自己的mux.Handle()方法

接下来是Handle方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (mux *serveMux121) handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	// ...
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}
  • 通过读写锁保证并发安全性
  • handle函数将路径和处理函数组成一个键值对放入serveMux的map中
  • 若传入的请求路径以/结尾,HTTP服务端就会把它视作前置匹配的模式,将路径加入es切片中,并按照路径长度降序排列
  • 若不以/开头,说明这个路由模式是主机模式,需要匹配主机头和路径,而若以/开头,说明路由模式是路径模式,可以忽略主机名

服务端运行流程

主流程

流程图如下:

  • net.Listen()用于为当前端口申请一个监听器
  • 服务端启动时会以for循环自旋方式运行,每次循环都会调用Listener.Accept()方法,保证持续监听端口时,若当前没有来自客户端的http请求,则当前服务端对应的goroutine会被动陷入阻塞,可以节省CPU,一旦有请求到来,服务端会被唤醒,执行接下来的流程
  • 针对到来的每一个请求都会构造一个connection结构体并启动一个goroutine来负责完成当前请求的处理,主方法继续循环监听
  • 接下来封装请求,到Server.ServeHTTP()方法时,会通过map匹配到对应处理函数处理,并输出响应
1
2
3
4
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

ListenAndServe函数传入地址和handler,若handler为nil,会默认使用之前定义的default单例,传入后声明一个新的Server对象,并嵌套调用内部的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (s *Server) ListenAndServe() error {
	if s.shuttingDown() {
		return ErrServerClosed
	}
	addr := s.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return s.Serve(ln)
}

该方法根据用户传入的端口,调用listen方法针对端口号分配一个端口监听器,并调用Serve方法

serve方法

Serve
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (s *Server) Serve(l net.Listener) error {
	//...
	ctx := context.WithValue(baseCtx, ServerContextKey, s)
	for {
		rw, err := l.Accept()
		if err != nil {
			if s.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
				}
				s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return err
		}
		connCtx := ctx
		if cc := s.ConnContext; cc != nil {
			connCtx = cc(connCtx, rw)
			if connCtx == nil {
				panic("ConnContext returned nil")
			}
		}
		tempDelay = 0
		c := s.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}

可以发现通过for循环轮询,每次调用Accept()方法,当有请求到来时,会通过newConn()封装一个结构体,并为新的请求调用conn.serve()方法调用一个goroutine,主循环继续监听

调用的这个方法是整个逻辑的主要方法,用来读取context中的请求,并传入serverHandler.ServeHTTP()来处理请求

1
2
3
4
5
6
7
8
func (sh serverHandler) ServeHTTP(rw Responsewriter,req *Request){
	handler := sh.srv.Handler
	if handler ==nil{
		handler = DefaultServeMux
	}
	//..
	handler.ServeHTTP(rw,req)
}

这里的serverHandler就是我们ListenAndServe时传入的参数,若handler为nil就会指定为default,进而调用路由中的ServeHTTP方法

这个方法有两步:

  • 通过request找到对应的handler
  • 执行handler的ServeHTTP方法

如何找到对应的handler呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (mux *serveMux121) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

	// Host-specific pattern takes precedence over generic ones
	if mux.hosts {
		h, pattern = mux.match(host + path)
	}
	if h == nil {
		h, pattern = mux.match(path)
	}
	if h == nil {
		h, pattern = NotFoundHandler(), ""
	}
	return
}
  • 首先对应加一把读锁,防止并发问题
  • 根据hosts标记是否为真,选择路由模式,调用match函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (mux *serveMux121) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}
	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}
  • 首先精准匹配,从map例直接找,找到了就返回
  • 若匹配不上,就按照最长公共前缀从长到短来匹配,若当前拿到的path是我们传入的请求路径的前缀,就返回该前缀对应的处理函数,响应请求

客户端

Client

client类对整个客户端模块封装

1
2
3
4
5
6
type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}
  • Transport:http通信核心部分
  • Jar:Cookie管理
  • Timeout:超时控制

其中Transport的类型是RoundTripper接口,里面定义了方法Roundtrip(),通过传入请求和服务端交互后产生响应Response

Transport是实现类,核心字段:

1
2
3
4
5
6
type Transport struct {
    idleConn     map[connectMethodKey][]*persistConn
    //...
    DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
    //...
}
  • idleConn:空闲连接的map,可以实现复用
  • DialContext:新连接生成器,传入协议,目标地址等生成tcp连接

Request结构体用于封装请求数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Request struct {
    // 方法
    Method string
    // 请求路径
    URL *url.URL
    // 请求头
    Header Header
    // 请求参数内容
    Body io.ReadCloser
    // 服务器主机
    Host string
    // query 请求参数
    Form url. Values
    // 响应参数 struct
    Response *Response
    // 请求链路的上下文
    ctx context.Context
    // ...
}

Response结构体用于封装响应数据

1
2
3
4
5
6
7
8
9
10
11
12
13
type Response struct {
    // 请求状态,200 为 请求成功
    StatusCode int // e.g. 200
    // http 协议,如:HTTP/1.0
    Proto string // e.g. "HTTP/1.0"
    // 请求头
    Header Header
    // 响应参数内容
    Body io.ReadCloser
    // 指向请求参数
    Request *Request
    // ...
}

方法链路

客户端发起一次http请求的步骤:

  • 构造http请求参数
  • 获取用于与服务端交互的tcp连接
  • 通过tcp连接发送请求参数
  • 通过tcp连接接受响应结果
Client

假设调用http.Post()发送一个post请求:

  • 首先构造request
  • 通过Transport模块的roudetrip构建tcp链接
  • 构建的connection分为两个goroutine:writeLoopreadLoop
  • 通过往request channel中塞入参数,writeLoop就会和服务端通信
  • 服务端返回的响应由readLoop接收,通过response channel传回接受方

为什么要使用channel和goroutine呢

请求流程

传入请求

http.Post()方法需要传入服务端url,请求参数格式和请求参数的io reader,它会使用包中的单例客户端DefaultClient处理请求,方法中调用了Defaultclient.Post()方法

1
2
3
4
5
6
7
8
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)
}

将方法,url,请求体通过NewRequest()方法构造出完整的请求参数,设置Header,再传入client.Do()方法处理

获得响应

client.Do()方法通过调用client.send()方法设置cookie,发送请求并更新response的cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	cookieURL := req.URL
	if req.Host != "" {
		cookieURL = cloneURL(cookieURL)
		cookieURL.Host = req.Host
	}
	if c.Jar != nil {
		for _, cookie := range c.Jar.Cookies(cookieURL) {
			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(cookieURL, rc)
		}
	}
	return resp, nil, nil
}

发送请求使用send函数,传入请求参数和通信模块,默认transport类

1
2
3
4
5
6
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
}

send函数使用RoundTrip方法

1
2
3
4
5
6
7
8
9
10
func (t *Transport) roundTrip(req *Request) (_ *Response, err error) {
    // ...
	for {
    	treq := &transportRequest{Request: req, trace: trace, ctx: ctx, cancel: cancel}
        // ...
        pconn, err := t.getConn(treq, cm)
        // ...
        resp, err = pconn.roundTrip(treq)
    }
}
  • 该方法首先将传入参数格式转换为transport格式
  • 接下来从transport模块获取一个可用的tcp链接,再用该链接的roundTrip方法获得响应

获取tcp链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    // 获取连接的请求参数体
    w := &wantConn{
        cm:         cm,
        // key 由 http 协议、服务端地址等信息组成
        key:        cm.key(),
        ctx:        ctx,
        // 标识连接构造成功的信号发射器
        ready:      make(chan struct{}, 1),
    }
    // 倘若连接获取失败,在 wantConn.cancel 方法中,会尝试将 tcp 连接放回队列中以供后续复用
    defer func() {
        if err != nil {
            w.cancel(t, err)
        }
    }()
    // 尝试复用指向相同服务端地址的空闲连接
    if delivered := t.queueForIdleConn(w); delivered {
        pc := w.pc
        // ...
        return pc, nil
    }
    // 异步构造新的连接
    t.queueForDial(w)
    select {
    // 通过阻塞等待信号的方式,等待连接获取完成
    case <-w.ready:
        // ...
        return w.pc, w.err
    // ...
    }
}
  • 通过传入的HTTP协议等信息构建一个key,去之前构建的idle池中映射,尝试复用指向相同服务端地址的空闲链接,若可以则直接返回

  • 若无法复用,就直接异步构造新的连接,主程序暂时阻塞,等待连接构造完毕后返回

  • 使用queueForIdleConn()尝试复用连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
        // ...
        if list, ok := t.idleConn[w.key]; ok {
            // ...
            for len(list) > 0 && !stop {
                pconn := list[len(list)-1]
                // ...
                delivered = w.tryDeliver(pconn, nil)
                if delivered {
                    // ...
                    list = list[:len(list)-1]         
                }
                stop = true
            }
            // ...
            if stop {
                return delivered
            }
        }
        // ...    
        return false
    }

    从对应的key找符合的连接,若能符合的连接大约等于一个,就找最后一个返回,若没有就只能创造新的连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func (t *Transport) queueForDial(w *wantConn) {
        // ...
        go t.dialConnFor(w) 
        // ...
    }
    
    func (t *Transport) dialConnFor(w *wantConn) {
        // ...
        pc, err := t.dialConn(w.ctx, w.cm)
        delivered := w.tryDeliver(pc, err)
        // ...
    }

    queueForDial会异步调用dialConnFor函数,创建新的tcp连接

    因为tcp连接是有生命周期的,所以我们需要创建两个读写的守护进程

    • dialConn用于创建tcp连接
    • tryDeliver用于将连接绑定到wantConn上
    • 最后通过关闭ready channel操作唤醒上游读channel的goroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
    pconn = &persistConn{
        t:             t,
        reqch:         make(chan requestAndChan, 1),
        writech:       make(chan writeRequest, 1),
        // ...
    }
    conn, err := t.dial(ctx, "tcp", cm.addr())
    // ...
    pconn.conn = conn      
    // ...
    go pconn.readLoop()
    go pconn.writeLoop()
    return pconn, nil
}

writech由writeLoop持有,readch同理,当客户端想要发送请求后,使用writeLoop写入服务端,使用reqch接收来自服务端的信息到channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 func (pc *persistConn) readLoop() { 
    // ...
    alive := true
    for alive {
        // ...
        rc := <-pc.reqch
        // ...
        var resp *Response
        // ...
        resp, err = pc.readResponse(rc, trace)
        // ...
        select{
            rc.ch <- responseAndError{res: resp}:
            // ...
        }
        // ...        
    }
}

func (pc *persistConn) writeLoop() {    
    for {
        select {
        case wr := <-pc.writech:
            // ...
            err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
            // ...       
    }
}
  • readLoop接收到reqch后判断发生新一轮请求,和服务端建立通讯并读取响应,通过内置的channel接收来自服务的响应
  • writeLoop拿到客户端提交的请求后,将其发送到服务端

主干链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    // ...
    pc.writech <- writeRequest{req, writeErrCh, continueCh}
    resc := make(chan responseAndError)
    pc.reqch <- requestAndChan{
        req:        req.Request,
        cancelKey:  req.cancelKey,
        ch:         resc,
        // ...
    }
    // ...
    for {       
        select {
        // ...
        case re := <-resc:
            // ...
            return re.res, nil
        // ...
        }
    }
}
  • 该函数是和两个Loop通讯的主要函数
  • 将我们的请求塞入writech,使得writeLoop接收,并把请求放入服务端,并构造好接收响应的channel,塞入requestAndChan中,这样就可以被readLoop接收到
  • 后续使用for循环持续监听,当readLoop接收到了结果,就会被主流程接收到,进行处理