Go语言网络编程:HTTP服务端之底层原理与源码分析——http.HandleFunc()、http.ListenAndServe()

2023年 8月 13日 66.5k 0

一、启动 http 服务

import (
    "net/http"
)
func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ping...ping..."))
    })
    http.ListenAndServe(":8999", nil)
}

在 Golang只需要几行代码便能启动一个 http 服务,在上述代码中,完成了两件事:

  • 调用 http.HandleFunc 方法,注册了对应于请求路径 /ping 的 handler 函数
  • 调用 http.ListenAndServe,启动了一个端口为 8999 的 http 服务

二、server核心数据结构

2.1 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{}
    // ...还有其他变量
}
  • Addr :表示服务器监听的地址。如":8080"表示服务器在本地主机上的8080端口上监听。
  • Handler :用于处理HTTP请求的处理器。
  • mu :用于保护共享资源的互斥锁。在对 Server 进行并发操作时,使用该互斥锁确保线程安全。
  • ReadTimeout 、 WriteTimeout 、 IdleTimeout :分别表示服务器读取请求超时、写入响应超时和空闲超时的时间限制。
  • TLSConfig :用于配置HTTPS连接的TLS例如证书、私钥等。
  • ConnState :是一个回调函数,在每个网络连接状态变化时触发。可以用于跟踪连接状态的变化,如新建、活动、关闭等。
  • activeConn :map类型,用于存储当前处于活动状态的连接。将每个活动的连接映射到一个空结构体。
  • doneChan :channel类型,一个用于异步通知服务器已完成关闭的信道。当服务器关闭时,会向该信道发送信号。
  • listeners :map类型,用于存储服务器正在侦听的网络。
  • 这些变量在 Server 结构定义了服务器的配置和状态跟踪。它们用于控制服务器监听地址、处理请求、超时设置、连接状态跟踪等方面,并提供了并发安全性的保护。

    2.2 Handler 接口

    type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
    }
    

    Handler 是一个接口,定义了方法: ServeHTTP。任何结构只要实现了这个ServeHTTP方法,那么这个结构体就是一个Handler对象。
    ServeHTTP方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应。ServeHTTP方法有两个参数:ResponseWriter和*Request。

    • ResponseWriter是一个接口,用于向客户端发送HTTP响应;
    • *Request是一个指向Request结构体的指针,包含来自客户端的HTTP请求的各种信息,如请求方法、URL和请求头等。

    通过实现 Handler 接口并定义自己的 ServeHTTP 方法,可以创建HTTP处理程序。当收到一个HTTP请求时,服务器会调用这个处理程序的 ServeHTTP 方法,并将适当的ResponseWriter和*Request给它,以便处理请求并生成响应。

    2.3 muxEntry 具体路由

    type muxEntry struct {
        h Handler
        pattern string 
    }
    

    muxEntry是一个结构体类型,代表具体的路由条目。每个路由条目包含一个路径模式pattern和关联的处理函数h,用于匹配和处理特定的URL路径。

    • h是一个 Handler 接口类型的变量,表示与该路由条目关联的处理程序。通过该处理程序可以执行特定的逻辑来处理HTTP请求并生成响应。
    • pattern是一个字符串类型的变量,表示该路由条目所匹配的URL模式或路径模式。这个模式用于将特定的HTTP请求与路由条目关联起来,以便选择正确的处理程序进行处理。它用于确定请求的URL是否与该路由条目匹配,并决定选择哪个处理程序来处理该请求。以下是一些pattern的匹配规则
      • 完全匹配:pattern可以是一个普通的字符串,比如 "/about"。这意味着只有当请求的URL路径与该字符串完全相同时,才会匹配该路由条目。
      • 模糊匹配:pattern以斜线 / 结尾,如 "/products/"。这表示当请求的URL路径只要以该前缀开头时,该路由条目将会匹配。例如,对于请求的URL路径为 "/products/123"、"/products/456",这些路由条目都会与之匹配。

    2.4 ServeMux多路路由器

    type ServeMux struct {
        mu sync.RWMutex			//允许多个 goroutine 并发地读取路由表,但在修改路由表时需要独占锁。
        m map[string]muxEntry	//用于将 所有的URL 模式(或路径模式)映射到 muxEntry 结构体
        es []muxEntry 			//只存储 "/" 结尾的路由,即模糊匹配patter的muxEntry,且切片内按从最长到最短的顺序排列
        hosts bool 				//表示是否有路由模式包含主机名
    }
    

    ServeMux是一个用于路由HTTP请求的多路复用器(multiplexer)。它可以将收到的请求根据路径匹配与注册的处理函数进行对应关联,并分发给相应的处理函数进行处理,实际上是一个实现了http.Handler接口的结构体。内部通过一个 map 维护了从 path 到 handler 的映射关系, 有效地管理和选择正确的处理程序来处理传入的HTTP请求。当收到一个请求时,ServeMux 会根据请求的URL路径进行匹配,并检查注册的路由模式pattern,然后选择适合的处理程序handler进行处理。
    在这里插入图片描述

    2.5 defaultServeMux的全局多路路由器

    //全局变量
    var DefaultServeMux = &defaultServeMux
    var defaultServeMux ServeMux
    

    在http包的初始化程中,会创建一个名为defaultServeMux的全局ServeMux实例,然后通过&defaultServeMux将其地址赋值给DefaultServeMux。这样就得到了一个默认的全局变量,我们可以通过直接使用DefaultServeMux来进行请求路由和处理,而管理自己的多路复用器。当我们使用http.HandleFunc等函数来注册处理函数时,默认情况下它们会将路由信息添加到DefaultServeMux上。

    三、http.HandleFunc()注册路由底层原理

    在这里插入图片描述

    3.1 http.HandleFunc()

    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        DefaultServeMux.HandleFunc(pattern, handler)
    }
    

    如上所示,当调用http.HandleFunc()时,会直接调用全局变量DefaultServeMux.HandleFunc()。HandleFunc的第一个参数是路由表达式,也就是请求路径,第二个参数是一个函数类型,也就是真正处理请求的函数。

    3.2 DefaultServeMux.HandleFunc()

    type HandlerFunc func(ResponseWriter, *Request)
    
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
    }
    
    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        // ...
        mux.Handle(pattern, HandlerFunc(handler))
    }
    

    如上所示,在 ServeMux.HandleFunc 定义了接收一个 ResponseWriter 对象和一个 Request 对象的 HandlerFunc 类型。

    • HandlerFunc类型是一个适配器,并且这种类型实现了ServeHTTP方法,即是Handler接口的具体实现类型,并在ServeHTTP方法中又调用了被转换的函数自身,也就是说这个类型的函数其实就是一个Handler类型的对象,通过类型转换可以将一个具有func(ResponseWriter, *Request)签名的普通函数转换为一个Handler对象,而不需要再定义一个结构体,再让这个结构实现ServeHTTP方法,非常方便的将普通函数用作HTTP处理程序。

    通过将处理函数转换为 HandlerFunc 类型,并将其作为参数传递给 ServeMux 的 Handle 方法,我们可以通过mux.Handle将处理函数注册到 ServeMux 的路由 map 中,以便在收到请求时能够正确匹配处理函数并处理该请求。这种设计模式使得我们可以将不同的处理函数注册到 ServeMux,并根据请求的路径来选择相应的处理函数进行处理。这样可以实现灵活的路由功能,使我们能够根据具体的业务逻辑来处理不同的请求。

    3.3 mux.Handle()

    func (mux *ServeMux) Handle(pattern string, handler Handler) {
        mux.mu.Lock()
        defer mux.mu.Unlock()
        // 检查路由表达式是否为空
        if pattern == "" {
            panic("http: invalid pattern")
        }
        // 检查处理函数是否为空
        if handler == nil {
            panic("http: nil handler")
        }
        // 如果这个路由表达式已经注册过处理器函数,直接panic
        if _, exist := mux.m[pattern]; exist {
            panic("http: multiple registrations for " + pattern)
        }
        // 如果 mux.m 为nil 通过 make 函数初始化 map
        if mux.m == nil {
            mux.m = make(map[string]muxEntry)
        }
        // 用路由表达式和处理函数handler创建 muxEntry 对象
        e := muxEntry{h: handler, pattern: pattern}
        // 所有注册的路由都会存放到mux.m里面
        mux.m[pattern] = e
        // 如果路由以/做结尾,则在mux.es中存放一份规则,同时做好从长到短的排序
        if pattern[len(pattern)-1] == '/' {
            // 如果路由patterm以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry中,路由长的位于切片的前面
            mux.es = appendSorted(mux.es, e)
        }
        // 如果该路由路径不以 "/" 开始,标记该 mux 中有路由的路径带有主机名
        if pattern[0] != '/' {
            mux.hosts = true
        }
    }
    

    通过以上代码,Handle 方法实现了将传进来的pattern和handler保存在muxEntry结构中,并且pattern作为key,把muxEntry装入到DefaultServeMux的Map里面。简单来说就是保存当前路由和自己定义的那个处理函数。在收到请求时,ServeMux 可以根据请求的 URL 查找到对应的处理函数,并进行处理。同时,将以“/”结尾的模糊匹配路由添加到 es 切片中,同时在es切片中通过appendSorted对路由按从长到短进行排序,代码如下所示。

    func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
        n := len(es)
        i := sort.Search(n, func(i int) bool {
            return len(es[i].pattern) < len(e.pattern)
        })
        if i == n {
            return append(es, e)
        }
        es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
        copy(es[i+1:], es[i:])      // Move shorter entries down
        es[i] = e
        return es
    }
    

    四、http.ListenAndServe()监听与服务底层原理

    在这里插入图片描述

    4.1 Http.ListenAndServe()启动 server

    在上述http启动过程中,我们通过调用 net/http 包下的公开方法 http.ListenAndServe,实现了对服务端的一键启动。
    而在http.ListenAndServe 内部会声明一个新的 Server 对象,嵌套执行 Server.ListenAndServe 方法。我们使用Http包启动web服务时Handler一般设置为nil,所以使用的就是默认的DefaultServeMux。

    //The handler is typically nil, in which case the DefaultServeMux is used.
    //Handler通常是nil,此时,会默认使用DefaultServeMux。
    func ListenAndServe(addr string, handler Handler) error {
        server := &Server{Addr: addr, Handler: handler}
        return server.ListenAndServe()
    }
       
    

    4.2 server.ListenAndServe()

    func (srv *Server) ListenAndServe() error {
        // ...
        addr := srv.Addr
        if addr == "" {
            addr = ":http"
        }
        ln, err := net.Listen("tcp", addr)
        // ...    
        return srv.Serve(ln)
    }
    

    Server.ListenAndServe 方法中,根据用户传入的地址,申请到一个监听器TCPListener 监听地址addr,继而调用 Server.Serve 方法为这个连接提供服务

    4.3 Server.Serve()

    var ServerContextKey = &contextKey{"http-server"}
    
    type contextKey struct {
        name string
    }
    
    func (srv *Server) Serve(l net.Listener) error {
       // ...
       ctx := context.WithValue(baseCtx, ServerContextKey, srv)
        for {
        	//rw可视为tcp连接
            rw, err := l.Accept()
            // ...
            connCtx := ctx
            // ...
            //c为http连接
            c := srv.newConn(rw)
            // ...
            go c.serve(connCtx)
        }
    }
    

    在这里插入图片描述

    Server.Serve 方法体现了典型的服务端的运行架构,使用For + Listener.Accept的模式来接受并处理客户端请求。这种模式是一种常见的服务器设计模式,通常用于监听网络连接请求的处理。

    在这个方法中,通过循环调用listener.Accept()方法来接受客户端连接。每当有新的连接建立时,将 server 封装成一组 valueContext,并开启一个新的goroutine调用 conn.serve 来处理该连接并传入server 封装后的valueContext,这确保了可以同时处理多个同时到达的连接请求。

    4.4 *conn.serve()

    *conn.serve()代码很长,核心是下面几行代码,简化之后如下:

    func (c *conn) serve(ctx context.Context) {
    // ...
    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中的所有评论

    发布评论