图文讲透Golang标准库 Net/Http实现原理 服务端

2024年 1月 29日 107.7k 0

前言

今天分享下Go语言net/http标准库的内部实现逻辑,文章将从客户端(Client)--服务端(Server)两个方向作为切入点,进而一步步分析http标准库内部是如何运作的。

图片图片

由于会涉及到不少的代码流程的走读,写完后觉得放在一篇文章中会过于长,可能在阅读感受上会不算很好,因此分为【Server--Client两个篇文章】进行发布。

本文内容是【服务端Server部分】,文章代码版本是Golang 1.19,文中会涉及较多的代码,需要耐心阅读,不过我会在尽量将注释也逻辑阐述清楚。先看下所有内容的大纲:

图片图片

Go 语言的 net/http 中同时封装好了 HTTP 客户端和服务端的实现,这里分别举一个简单的使用示例。

Server启动示例

Server和Client端的代码实现来自net/http标准库的文档,都是简单的使用,而且用很少的代码就可以启动一个服务!

http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "xiaoxu code")
})
http.ListenAndServe(":8080", nil)

上面代码中:

HandleFunc 方法注册了一个请求路径 /hello 的 handler 函数

ListenAndServe指定了8080端口进行监听和启动一个HTTP服务端

Client发送请求示例

HTTP 包一样可以发送请求,我们以Get方法来发起请求,这里同样也举一个简单例子:

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

是不是感觉使用起来还是很简单的,短短几行代码就完成了http服务的启动和发送http请求,其背后是如何进行封装的,在接下的章节会讲清楚!

服务端 Server

我们先预览下图过程,对整个服务端做的事情有个了解

图片图片

从图中大致可以看出主要有这些流程:

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

    type Server struct {
        Addr string
        Handler Handler 
        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{}
        ...
    }

    我们在下图中解释了部分字段代表的意思

    图片图片

    ServeMux结构体

    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请求的多路复用器。

    👉 作用: 管理和处理程序来处理传入的HTTP请求

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

    图片图片

    路由注册

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

    图片图片

    了解完流程之后接下来继续追函数看代码

    var DefaultServeMux = &defaultServeMux
    // 默认的ServeMux
    var defaultServeMux ServeMux
    
    // HandleFunc注册函数
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        DefaultServeMux.HandleFunc(pattern, handler)
    }

    DefaultServeMux是ServeMux的默认实例。

    //接口
    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方法。

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

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

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

    ServeMux.Handle

    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对路由按从长到短进行排序。

    🚩 注:

    map[string]muxEntry 的map使用哈希表是用于路由精确匹配

    []muxEntry用于部分匹配模式

    到这里就完成了路由和handle的绑定注册了,至于为什么分了两个模式,在后面会说到,接下来就是启动服务进行监听的过程。

    监听和服务启动

    同样的我用图的方式监听和服务启动的函数调用链路画出来,让大家先有个印象。

    结合图会对后续结合代码逻辑更清晰,知道这块代码调用属于哪个阶段!

    图片图片

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

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

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

    1. 调用net.Listen函数,指定网络类型和监听地址。

    2. 使用listener.Accept函数接受客户端的连接请求。

    3. 在一个独立的goroutine中处理每个连接。

    4. 在处理完连接后,调用conn.Close()来关闭连接

    Server.Serve:

    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 方法负责处理
    type onceCloseListener struct {
        net.Listener
        once     sync.Once
        closeErr error
    }

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

    *conn.serve():

    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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论