likes
comments
collection
share

「GO标准库」net/http 包的全面解析 :Server 服务端

作者站长头像
站长
· 阅读数 13

前言

对于 Golang 来说,实现一个简单的 http server 非常容易,只需要短短几行代码。同时有了协程的加持,Go 实现的 http server 能够取得非常优秀的性能。这篇文章将会对 go 标准库 net/http 实现 http 服务的原理进行较为深入的探究,以此来学习了解网络编程的常见范式以及设计思路。

HTTP 处理流程

基于 HTTP 构建的网络应用包括两个端,即客户端 ( Client ) 和服务端 ( Server )。两个端的交互行为包括从客户端发出 request接收 request 进行处理并返回 response 以及客户端处理 response

所以 http 服务器的工作就在于如何接受来自客户端的 request,并向客户端返回 response

「GO标准库」net/http 包的全面解析 :Server 服务端

服务器在接收到请求时,首先会进入路由 ( router ),路由的工作在于为这个 request 找到对应的处理器 ( handler ),处理器对 request 进行处理,并构建 response。Golang 实现的 http server 同样遵循这样的处理流程。

如何实现一个简单的 HTTP Server ?

这里有两种方式:

第一种方式:最常用,最简单

package main

import (
    "fmt"
    "net/http"
)

func helloWord(w http.ResponseWriter, r *http.Request) {
    fmt.Println(w, "hello word")
}

func main() {
    http.HandleFunc("/", helloWord)
    http.ListenAndServe(":9000", nil)
}

第二种方式:利用 go 的特性,实现 http.Handle 接口 中的 ServeHTTP 方法

package main

import (
    "fmt"
    "net/http"
)

type HelloWord struct {
    content string
}

func (h *HelloWord) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Println(w, h.content)
}

func main() {
    http.Handle("/", &HelloWord{content: "hello word"})
    http.ListenAndServe(":9000", nil)
}

Go 实现的 http 服务步骤非常简单,首先注册路由,然后创建服务并开启监听即可。

注册路由

在 http 包中,注册路由有两种方法:HandleFuncHandle

  • http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
  • http.Handle(pattern string, handler Handler)

HandleFunc 和 Handle 的区别

  • HandleFunc
// HandleFunc 在 DefaultServeMux 中注册给定模式的处理程序函数。
// ServeMux 的文档解释了模式是如何匹配的。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

// HandleFunc 为给定的模式注册处理程序函数。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
       panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}
  • Handle
// Handle 在 DefaultServeMux 中 注册给定模式的处理程序。
// ServeMux 的文档解释了模式是如何匹配的。
func Handle(pattern string, handler Handler) { 
    DefaultServeMux.Handle(pattern, handler) 
}

可以发现两者的区别在第二个形参:

  • HandleFunc 是一个 func(ResponseWriter, *Request) 签名的函数
  • Handle 是一个 handler 结构体, 该结构体实现了一个 ServeHTTP(ResponseWriter, *Request) 签名的方法

最终两者都是由 DefaultServeMux 调用 Handle 来完成注册路由的操作。

Handler

Handler 是一个接口:

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

Handler 接口中,定义了 ServeHTTP 的函数签名,只要任何结构体实现了 ServeHTTP 方法,那该结构体就是 Handler 对象。

Go 的 http 服务都是基于 Handler 处理的, Handler 中的 ServeHTTP 方法也是用以处理 request 并构建 response 的核心逻辑。

ServeMux

Go 中的路由(Multiplexer)都是基于 ServeMux 结构,那么 ServeMux 是如何定义的?

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // 从最长到最短排序的条目的切片。
    hosts bool       // 是否有任何模式包含主机名
}

type muxEntry struct {
    h       Handler
    pattern string
}

这里最重要的是 ServeMux 中的 m 字段,它是一个 mapkey 是路由表达式,value 是一个 muxEntry 结构,muxEntry 结构存储了对于的路由表达式和 handler

并且通过源码得知:ServeMux 也实现了 ServeHTTP 方法

// ServeHTTP 将请求分派给其模式与请求 URL 最匹配的处理程序。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
       if r.ProtoAtLeast(1, 1) {
          w.Header().Set("Connection", "close")
       }
       w.WriteHeader(StatusBadRequest)
       return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

从这里就能看出:ServeMux 结构体也是 Handler 对象,只不过 ServeMuxServeHTTP 方法不是用来处理具体的 request 和构建 response,而是用来确定路由注册的 handler

如何启动 Server 服务?

启动服务使用:http.ListenAndServe() 方法

// ListenAndServe 侦听 TCP 网络地址 addr,然后使用处理程序调用 Serve 来处理传入连接上的请求。
// 接受的连接被配置为启用 TCP 保持活动。
//
// 处理程序通常为 nil,在这种情况下使用 DefaultServeMux。
//
// ListenAndServe 总是返回一个非零错误。
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

// ListenAndServe 侦听 TCP 网络地址 srv.Addr,然后调用 Serve 来处理传入连接上的请求。
// 接受的连接被配置为启用 TCP 保持活动。
//
// 如果 srv.Addr 为空,使用“:http”。
//
// ListenAndServe 总是返回一个非零错误。
// 在 Shutdown 或 Close 之后,返回的错误为 ErrServerClosed。
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)
}

这里先创建了一个 Server 对象,传入了地址和 handler 参数,然后调用 Server 对象 ListenAndServe() 方法。

这里用到了 Server 结构体, 咱们看一下这个结构体,Server 结构体中字段比较多,可以先大致了解一下:

// 服务器定义用于运 行HTTP 服务器的参数。
// 服务器的零值是有效的配置。
type Server struct {
    // Addr可以选择指定要侦听的服务器的TCP地址,格式为“host:port”。如果为空,则使用“:http”(端口80)。
    // 服务名称在 RFC 6335 中定义,并由 IANA 分配。
    // 查看 net.Dial 获取地址格式的详细信息。
    Addr string

    Handler Handler // 要调用的 Handler,http.DefaultServeMux 如果为零

    // DisableGeneralOptionsHandler,如果为true,则将“OPTIONS*”请求传递给Handler,
    // 否则以 200 OK 和 Content-Length:0 进行响应。
    DisableGeneralOptionsHandler bool

    // TLSConfig 可选地提供 TLS 配置,供 ServeTLS 和 ListenAndServeTLS 使用。
    // 请注意,该值是由 ServeTLS 和 ListenAndServeTLS 克隆的,
    // 因此不可能使用 TLS 等方法修改配置:Config.SetSessionTicketKeys。
    // 若要使用 SetSessionTicketKey,请使用 Server.Serve 和 TLS 侦听器
    TLSConfig *tls.Config

    // ReadTimeout 是读取整个请求(包括正文)的最长持续时间。零值或负值表示没有超时。
    //
    // 由于 ReadTimeout 不允许处理程序根据请求决定每个请求主体的可接受截止日期或上传速率
    // 因此大多数用户更喜欢使用 ReadHeaderTimeout。
    // 同时使用它们是有效的。
    ReadTimeout time.Duration

    // ReadHeaderTimeout 是允许读取请求标头的时间量。
    // 连接的读取截止日期在读取标头后重置,处理程序可以决定什么对主体来说太慢。
    // 如果ReadHeaderTimeout为零,则使用ReadTimeout的值。
    // 如果两者都为零,则没有超时。
    ReadHeaderTimeout time.Duration

    // WriteTimeout 是超时写入响应之前的最长持续时间。每当读取新请求的标头时,它都会重置。
    // 与 ReadTimeout 一样,它不允许处理程序在每个请求的基础上做出决定。
    // 零值或负值表示没有超时。
    WriteTimeout time.Duration

    // IdleTimeout 是启用保持活动时等待下一个请求的最长时间。
    // 如果 IdleTimeout 为零,则使用 ReadTimeout 的值。如果两者都为零,则没有超时。
    IdleTimeout time.Duration

    // MaxHeaderBytes 控制服务器解析请求标头的键和值(包括请求行)时读取的最大字节数。
    // 它不限制请求正文的大小。如果为零,则使用 DefaultMaxHeaderBytes。
    MaxHeaderBytes int

    // TLSNextProto 可选择指定一个函数,以便在发生 ALPN 协议升级时接管所提供 TLS 连接的所有权。
    // 映射密钥是协商的协议名称。
    // Handler 参数应用于处理 HTTP 请求,并将初始化请求的 TLS 和 RemoteAddr(如果尚未设置)。
    // 当函数返回时,连接将自动关闭。
    // 如果 TLSNextProto 不是 nil ,则不会自动启用 HTTP/2 支持。
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    // ConnState specifies an optional callback function that is
    // called when a client connection changes state. See the
    // ConnState type and associated constants for details.
    ConnState func(net.Conn, ConnState)

    // ErrorLog 为接受连接的错误、处理程序的意外行为以及潜在的 FileSystem 错误指定了一个可选的记录器。
    // 如果为零,则通过日志包的标准记录器进行日志记录。
    ErrorLog *log.Logger

    // BaseContext 可选地指定一个函数,该函数返回此服务器上传入请求的基本上下文。
    // 提供的侦听器是即将开始接受请求的特定侦听器。
    // 如果 BaseContext 为 nil,则默认为 context.Background()
    // 如果不是nil,则必须返回一个非 nil 上下文。
    BaseContext func(net.Listener) context.Context

    // ConnContext 可选地指定一个函数来修改用于新连接 c 的上下文。
    // 所提供的 ctx 是从基本上下文派生的,并且具有 ServerContextKey 值。
    ConnContext func(ctx context.Context, c net.Conn) context.Context

    inShutdown atomic.Bool // 当服务器处于关闭状态时为true

    disableKeepAlives atomic.Bool
    nextProtoOnce     sync.Once // 保护设置 HTTP2_*init
    nextProtoErr      error     // http2.ConfigureServer 的结果(如果使用)

    mu         sync.Mutex
    listeners  map[*net.Listener]struct{}
    activeConn map[*conn]struct{}
    onShutdown []func()

    listenerGroup sync.WaitGroup
}

在 Server 的 ListenAndServe 方法中,会初始化监听地址 Addr,同时调用 Listen 方法设置监听。最后将监听的 TCP 对象传入 Serve 方法:

// Serve 接受 Listener l上的传入连接,为每个连接创建一个新的服务 goroutine。
// 服务 goroutines 读取请求,然后调用 srv.Handler 回复他们。
//
//只有当侦听器返回 *tls.Conn 连接并且在 tls Config.NextProtos 中使用“h2”配置时,才启用 HTTP/2 支持。
//
//Serve 总是返回一个非零错误并关闭l。
//在 Shutdown 或 Close 之后,返回的错误为 ErrServerClosed。

func (srv *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
       fn(srv, l) // 带有 listener 的 hook 钩子
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := srv.setupHTTP2_Serve(); err != nil {
       return err
    }

    if !srv.trackListener(&l, true) {
       return ErrServerClosed
    }
    defer srv.trackListener(&l, false)

    baseCtx := context.Background()
    if srv.BaseContext != nil {
       baseCtx = srv.BaseContext(origListener)
       if baseCtx == nil {
          panic("BaseContext returned a nil context")
       }
    }

    var tempDelay time.Duration // 失败时要睡多久

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
       rw, err := l.Accept()
       if err != nil {
          if srv.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
             }
             srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
             time.Sleep(tempDelay)
             continue
          }
          return err
       }
       connCtx := ctx
       if cc := srv.ConnContext; cc != nil {
          connCtx = cc(connCtx, rw)
          if connCtx == nil {
             panic("ConnContext returned nil")
          }
       }
       tempDelay = 0
       c := srv.newConn(rw)
       c.setState(c.rwc, StateNew, runHooks) // 在 Serve 返回之前
       go c.serve(connCtx)
    }
}
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    if ra := c.rwc.RemoteAddr(); ra != nil {
       c.remoteAddr = ra.String()
    }
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    var inFlightResponse *response
    defer func() {
       if err := recover(); err != nil && err != ErrAbortHandler {
          const size = 64 << 10
          buf := make([]byte, size)
          buf = buf[:runtime.Stack(buf, false)]
          c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
       }
       if inFlightResponse != nil {
          inFlightResponse.cancelCtx()
       }
       if !c.hijacked() {
          if inFlightResponse != nil {
             inFlightResponse.conn.r.abortPendingRead()
             inFlightResponse.reqBody.Close()
          }
          c.close()
          c.setState(c.rwc, StateClosed, runHooks)
       }
    }()

    if tlsConn, ok := c.rwc.(*tls.Conn); ok {
       tlsTO := c.server.tlsHandshakeTimeout()
       if tlsTO > 0 {
          dl := time.Now().Add(tlsTO)
          c.rwc.SetReadDeadline(dl)
          c.rwc.SetWriteDeadline(dl)
       }
       if err := tlsConn.HandshakeContext(ctx); err != nil {
          // 如果握手失败是因为客户端不说 TLS,
          // 那么假设他们说的是明文 HTTP,并在 TLS conn 的底层网络上写一个400响应 net.Conn。
          if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
             io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
             re.Conn.Close()
             return
          }
          c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
          return
       }
       // 恢复Conn级别的截止日期。
       if tlsTO > 0 {
          c.rwc.SetReadDeadline(time.Time{})
          c.rwc.SetWriteDeadline(time.Time{})
       }
       c.tlsState = new(tls.ConnectionState)
       *c.tlsState = tlsConn.ConnectionState()
       if proto := c.tlsState.NegotiatedProtocol; validNextProto(proto) {
          if fn := c.server.TLSNextProto[proto]; fn != nil {
             h := initALPNRequest{ctx, tlsConn, serverHandler{c.server}}
             // 将新创建的 HTTP/2 标记为活动并防止任何服务器状态挂钩
             // 阻止在这些连接上运行。这样可以防止 closeIdleConns 关闭此类连接。
             // 请参阅问题 https://golang.org/issue/39776.
             c.setState(c.rwc, StateActive, skipHooks)
             fn(c.server, tlsConn, h)
          }
          return
       }
    }

    // HTTP/1.x from here on.

    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for {
       w, err := c.readRequest(ctx)
       if c.r.remain != c.server.initialReadLimitSize() {
          // 如果我们从线路上读取任何字节,我们就是活动的。
          c.setState(c.rwc, StateActive, runHooks)
       }
       if err != nil {
          const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

          switch {
          case err == errTooLarge:
             // Their HTTP client may or may not be
             // able to read this if we're
             // responding to them and hanging up
             // while they're still writing their
             // request. Undefined behavior.
             const publicErr = "431 Request Header Fields Too Large"
             fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
             c.closeWriteAndWait()
             return

          case isUnsupportedTEError(err):
             // Respond as per RFC 7230 Section 3.3.1 which says,
             //      A server that receives a request message with a
             //      transfer coding it does not understand SHOULD
             //      respond with 501 (Unimplemented).
             code := StatusNotImplemented

             // We purposefully aren't echoing back the transfer-encoding's value,
             // so as to mitigate the risk of cross side scripting by an attacker.
             fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s%sUnsupported transfer encoding", code, StatusText(code), errorHeaders)
             return

          case isCommonNetReadError(err):
             return // don't reply

          default:
             if v, ok := err.(statusError); ok {
                fmt.Fprintf(c.rwc, "HTTP/1.1 %d %s: %s%s%d %s: %s", v.code, StatusText(v.code), v.text, errorHeaders, v.code, StatusText(v.code), v.text)
                return
             }
             publicErr := "400 Bad Request"
             fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
             return
          }
       }

       // Expect 100 Continue support
       req := w.req
       if req.expectsContinue() {
          if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
             // Wrap the Body reader with one that replies on the connection
             req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
             w.canWriteContinue.Store(true)
          }
       } else if req.Header.get("Expect") != "" {
          w.sendExpectationFailed()
          return
       }

       c.curReq.Store(w)

       if requestBodyRemains(req.Body) {
          registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
       } else {
          w.conn.r.startBackgroundRead()
       }

       // HTTP cannot have multiple simultaneous active requests.[*]
       // Until the server replies to this request, it can't read another,
       // so we might as well run the handler in this goroutine.
       // [*] Not strictly true: HTTP pipelining. We could let them all process
       // in parallel even if their responses need to be serialized.
       // But we're not going to implement HTTP pipelining because it
       // was never deployed in the wild and the answer is HTTP/2.
       inFlightResponse = w
       serverHandler{c.server}.ServeHTTP(w, w.req)
       inFlightResponse = nil
       w.cancelCtx()
       if c.hijacked() {
          return
       }
       w.finishRequest()
       c.rwc.SetWriteDeadline(time.Time{})
       if !w.shouldReuseConnection() {
          if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
             c.closeWriteAndWait()
          }
          return
       }
       c.setState(c.rwc, StateIdle, runHooks)
       c.curReq.Store(nil)

       if !w.conn.server.doKeepAlives() {
          // We're in shutdown mode. We might've replied
          // to the user without "Connection: close" and
          // they might think they can send another
          // request, but such is life with HTTP/1.1.
          return
       }

       if d := c.server.idleTimeout(); d != 0 {
          c.rwc.SetReadDeadline(time.Now().Add(d))
       } else {
          c.rwc.SetReadDeadline(time.Time{})
       }

       // Wait for the connection to become readable again before trying to
       // read the next request. This prevents a ReadHeaderTimeout or
       // ReadTimeout from starting until the first bytes of the next request
       // have been received.
       if _, err := c.bufr.Peek(4); err != nil {
          return
       }

       c.rwc.SetReadDeadline(time.Time{})
    }
}

首先创建一个上下文对象,然后调用 ListenerAccept() 等待新的连接建立;一旦有新的连接建立,则调用 ServernewConn() 创建新的连接对象,并将连接的状态标志为 StateNew,然后开启一个新的 goroutine 处理连接请求。

至此,Go 实现的 http server 的大致原理介绍完毕!

总结语

本节,通过 http 源码剖析,了解了 http Server 服务端的初始化过程:

  • 如何注册路由?
  • http 是如何解析路由的?
  • 如何开启 Server 服务?

相信通过本节的学习,可以对 http Server 服务的使用有深入的了解!