Golang 31_Golang标准库net/http实现原理

一、Golang标准库net/http简介

1.1 Golang标准库net/http 功能简介

Go 语言的 net/http 中同时封装好了 HTTP 客户端和服务端的实现,使用 net/http标准库,只需很少的代码就可以启动一个 客户端(Client)–服务端(Server) 应用:

 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
// server端
package server

import (
    "fmt"
    "net/http"
)

// HandleFunc 方法注册了一个请求路径 /hello 的 handler 函数
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello golang.")
})

http.ListenAndServe(":8080", nil)   // 指定了8080端口进行监听和启动一个HTTP服务端

// client 端
package client

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

resp, err := http.Get("http://example.com/")
if err != nil {
    fmt.Println(err)
    return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))

二、服务端 Server 详解

2.1 服务端整体架构

如下图所示为 http server 端在启动及运行过程中所做的事情, 主要有以下流程:

  • 注册handlermap中,mapkey是键值路由
  • handler注册完之后就开启循环监听,监听到一个连接就会异步创建一个 Goroutine
  • 在创建好的 Goroutine 内部会循环的等待接收请求数据
  • 接受到请求后,根据请求的地址去处理器路由表map中匹配对应的handler,然后执行handler

2.2 http Server 相关的结构体

1、 Server结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/net/http/server.go

type Server struct {
    Addr string             // 服务监听的地址   
    Handler Handler         // http请求处理器
    mu         sync.Mutex
    ReadTimeout time.Duration   // 请求超时
    WriteTimeout time.Duration  // 响应超时
    IdleTimeout time.Duration   // 空闲超时
    TLSConfig *tls.Config
    ConnState func(net.Conn, ConnState)
    activeConn map[*conn]struct{}   // 处于活动状态的连接
    doneChan   chan struct{}        // 接收关闭信息
    listeners  map[*net.Listener]struct{}   // 存储监听的网络
    ...
}

2、ServeMux结构体

是 标准库 net/http 包的 Handler 接口的一种实现,主页用于管理和处理程序来处理传入的HTTP请求路由。

Tips: gin 框架中实现另一种高效灵活的 Handler, 用户也可以自己实现一种 Handler 接口。

1
2
3
4
5
6
type ServeMux struct {
    mu sync.RWMutex   
    m map[string]muxEntry   // 用于路由精确匹配
    es []muxEntry           // 用于部分匹配模式
    hosts bool     
}
  • sync.RWMutex:这是读写互斥锁,允许goroutine 并发读取路由表,在修改路由map时独占
  • map[string]muxEntry:map结构维护 pattern (路由) 到 handler (处理函数) 的映射关系,精准匹配
  • []muxEntry:存储 “/” 结尾的路由,切片内按从最长到最短的顺序排列,用作模糊匹配patter的muxEntry
  • hosts:是否有任何模式包含主机名

Mux是【多路复用器】的意思,ServeMux就是服务端路由http请求的多路复用器。

原理:内部通过一个 map类型 维护了从 pattern (路由) 到 handler (处理函数) 的映射关系,收到请求后根据路径匹配找到对应的处理函数handler,处理函数进行逻辑处理。

2.3 路由注册

通过对 HandleFunc 的调用追踪,内部的调用核心实现如下:

1
2
3
4
5
6
7
8
var DefaultServeMux = &defaultServeMux
// 默认的ServeMux
var defaultServeMux ServeMux

// HandleFunc注册函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMux是ServeMux的默认实例。

http 包 的 Handler 接口及实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//接口
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

//HandlerFunc为函数类型
type HandlerFunc func(ResponseWriter, *Request)
//实现了Handler接口
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}


func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    ...
    // handler是真正处理请求的函数
    mux.Handle(pattern, HandlerFunc(handler))
}

HandlerFunc函数类型是一个适配器,是Handler接口的具体实现类型,因为它实现了ServeHTTP方法。

Tips: HandlerFunc(handler), 通过类型转换的方式【handler –>HandlerFunc】将一个出入参形式为func(ResponseWriter, *Request)的函数转换为HandlerFunc类型,而HandlerFunc实现了Handler接口,所以这个被转换的函数handler可以被当做一个Handler对象进行赋值。

HandlerFunc(handler)方式实现灵活的路由功能,方便的将普通函数转换为Http处理程序,兼容注册不同具体的业务逻辑的处理请求。

mux.Handle 的第二个参数Handler就是个接口,ServeMux.Handle就是路由模式和处理函数在map中进行关系映射。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    // 检查路由和处理函数
    ...
    //检查pattern是否存在
    ...
    //如果 mux.m 为nil 进行make初始化 map
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    e := muxEntry{h: handler, pattern: pattern}
    //注册好路由都会存放到mux.m里面
    mux.m[pattern] = e
    //patterm以'/'结尾
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }

    if pattern[0] != '/' {
        mux.hosts = true
    }
}

Handle的实现主要是将传进来的pattern和handler保存在muxEntry结构中,然后将pattern作为key,把muxEntry添加到DefaultServeMux的Map里。

如果路由表达式以 ‘/’ 结尾,则将对应的muxEntry对象加入到[]muxEntry切片中,然后通过appendSorted对路由按从长到短进行排序。

Tips: map[string]muxEntry 的map使用哈希表是用于路由精确匹配, []muxEntry用于部分匹配模式

2.4 监听和服务启动

http Server 的监听和服务启动的函数调用链路如下图所示

1、ListenAndServe启动服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)
}

net.Listen 实现了TCP协议上监听本地的端口 (ListenAndServe()中传过来的, 默认为8080),srv.Serve 接受 net.Listener实例传入,然后为每个连接创建一个新的服务 goroutine。

使用net.Listen函数实现网络监听需要经过以下几个步骤:

  • 调用net.Listen函数,指定网络类型和监听地址。
  • 使用listener.Accept函数接受客户端的连接请求。
  • 在一个独立的goroutine中处理每个连接。
  • 在处理完连接后,调用conn.Close()来关闭连接
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func (srv *Server) Serve(l net.Listener) error {
    origListener := l
    //内部实现Once是只执行一次动作的对象
    l = &onceCloseListener{Listener: l}
    defer l.Close()
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        //rw为可理解为tcp连接
        rw, err := l.Accept()
        ...
        connCtx := ctx
        ...
        c := srv.newConn(rw)
        //
        go c.serve(connCtx)
    }
}

使用 for + listener.accept 处理客户端请求

  • 在for 循环调用 Listener.Accept 方法循环读取新连接
  • 读取到客户端请求后会创建一个 goroutine 异步执行 conn.serve 方法负责处理
1
2
3
4
5
type onceCloseListener struct {
    net.Listener
    once     sync.Once
    closeErr error
}

onceCloseListener 是sync.Once的一次执行对象,当且仅当第一次被调用时才执行函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func (c *conn) serve(ctx context.Context) {
    ...
    // 初始化conn的一些参数
    c.remoteAddr = c.rwc.RemoteAddr().String()
    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
    for {
        // 读取客户端请求
        w, err := c.readRequest(ctx)
        ...
        // 调用ServeHTTP来处理请求
        serverHandler{c.server}.ServeHTTP(w, w.req)
    }
}

conn.serve是处理客户端连接的核心方法,主要是通过 for循环 不断循环读取客户端请求,然后根据请求调用相应的处理函数。

c.readRequest(ctx)方法是用来读取客户端的请求,然后返回一个response类型的w和一个错误 err

最终是通过 serverHandler{c.server}.ServeHTTP(w, w.req) 调用ServeHTTP处理连接客户端发送的请求。

2.5 请求处理

上面说到的 serverHandler{c.server}.ServeHTTP(w, w.req) 其实就是下面函数的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    ...
    // handler传的是nil就执行 DefaultServeMux.ServeHTTP() 方法
    handler.ServeHTTP(rw, req)
}

获取Server的handler流程:

  • 先获取 sh.srv.Handler 的值,判断是否为nil
  • 如果为nil则取全局单例 DefaultServeMux这个handler
  • PTIONS Method 请求且 URI 是 *,就使用globalOptionsHandler

Tips: 这个handler其实就是在ListenAndServe()中的第二个参数

1
2
3
4
5
6
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    ....
    h, _ := mux.Handler(r)
    // 执行匹配到的路由的ServeHTTP方法
    h.ServeHTTP(w, r)
}

ServeMux.ServeHTTP()方法主要代码可以分为两步:

  • 通过 ServerMux.Handler() 方法获取到匹配的处理函数 h
  • 调用 Handler.ServeHTTP() 执行匹配到该路由的函数来处理请求 (h实现了ServeHTTP方法)
1
2
3
4
5
6
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    ...
    //在mux.m和mux.es中
    //根据host/url.path寻找对应的handler
    return mux.handler(host, r.URL.Path)
}

ServeMux.Handler() 方法内部,会调用 ServerMux.handler(host, r.URL.Path) 方法来查找匹配的处理函数。

ServeMux.match()方法用于根据给定的具体路径 path 找到最佳匹配的路由,并返回Handler和路径。

值得一提的是,如果 mux.m 中不存在 path 完全匹配的路由时,会继续遍历 mux.es 字段中保存的模糊匹配路由。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // 是否完全匹配
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }
    // mux.es是按pattern从长到短排列
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

最后调用 handler.ServeHTTP 方法进行请求的处理和响应,而这个被调用的函数就是我们之前在路由注册时对应的函数。

1
2
3
4
5
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

三、客户端 Client 详解

3.1 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 事务,用于处理客户端的请求连接并等待服务端的响应;
  • CheckRedirect:处理重定向的策略
  • Jar:管理和存储请求中的 cookie
  • Timeout:超时设置
1
2
3
4
5
6
7
8
9
type Request struct {
    Method string
    URL *url.URL
    Header Header
    Body io.ReadCloser
    Host string
    Response *Response
    ...
}

Request字段较多,这里就列举一下常见的一些字段:

  • Method:指定的HTTP方法(GET、POST、PUT等)
  • URL:请求路径
  • Header:请求头
  • Body:请求体
  • Host:服务器主机
  • Response:响应参数

3.1 构造请求

1
2
3
4
5
var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
    return DefaultClient.Get(url)
}

HTTP 的 Get函数会调用到 DefaultClient 的 Get 方法,DefaultClient 是 Client 的一个空实例(跟DefaultServeMux有点子相似)。

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

func NewRequest(method, url string, body io.Reader) (*Request, error) {
    return NewRequestWithContext(context.Background(), method, url, body)
}

Client.Get() 根据用户的入参,请求参数 NewRequest使用上下文包装NewRequestWithContext ,接着通过 Client.Do 方法,处理这个请求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    ...
    // 解析url
    u, err := urlpkg.Parse(url)
    ...
    rc, ok := body.(io.ReadCloser)
    if !ok && body != nil {
        rc = ioutil.NopCloser(body)
    } 
    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,
    } 
    ...
    return req, nil
}

NewRequestWithContext 函数主要是功能是将请求封装成一个 Request 结构体并返回,这个结构体的名称是req。

3.2 准备发送请求

构造好的Request结构req,会传入c.Do()方法。

相关调用过程为: client.Do -> client.do -> client.send -> send -> RoundTripper.RoundTrip

Tips: 其实不管是Get还是Post请求的调用流程都是一样的,只是对外封装了Post和Get请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func (c *Client) do(req *Request) (retres *Response, reterr error) {
    ...
    for {
        ...
        resp, didTimeout, err = send(req, deadline)
        if err != nil {
            return nil, didTimeout, err
        }
    }
    ...
}
//Client 调用 Do 方法处理发送请求最后会调用到 send 函数中
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    resp, didTimeout, err = send(req, c.transport(), deadline)
    if err != nil {
        return nil, didTimeout, err
    }
    ...
    return resp, nil, nil
}

c.transport()方法是为了回去Transport的默认实例 DefaultTransport ,我们看下DefaultTransport长什么样。