3

Go 服务中 HTTP 请求的生命周期

 2 years ago
source link: https://studygolang.com/articles/35252
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go 服务中 HTTP 请求的生命周期

dust347 · 8天之前 · 252 次点击 · 预计阅读时间 11 分钟 · 大约8小时之前 开始浏览    

Go 语言对于编写 HTTP 服务来说是一个常见且非常合适的工具。这篇博文通过一个 Go 服务来探讨一个典型 HTTP 请求的路由,涉及路由,中间件以及比如并发之类的相关问题。

为了有具体的代码可以参考,让我们先从这段简单的服务代码开始(来自于 https://gobyexample.com/http-servers

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}

func headers(w http.ResponseWriter, req *http.Request) {
    for name, headers := range req.Header {
        for _, h := range headers {
            fmt.Fprintf(w, "%v: %v\n", name, h)
        }
    }
}

func main() {
    http.HandleFunc("/hello", hello)
    http.HandleFunc("/headers", headers)

    http.ListenAndServe(":8090", nil)
}

我们会通过查看 http.ListenAndServe 函数来开始跟踪一个 HTTP 请求在这个服务中的生命周期:

func ListenAndServe(addr string, handler Handler) error

这张图展示了调用时所发生的简要流程:

http-request-listenandserve.png?raw=true

这是函数和方法调用的实际序列的高度“内联”版本,但是原始的代码并不难理解。

主流程正如你期望的那样:ListenAndServe 监听给定地址的 TCP 端口,之后循环接受新的连接。对于每一个新连接,它都会调度一个 goroutine 来处理这个连接(稍后详细说明)。处理连接涉及一个这样的循环:

  • 从连接中解析 HTTP 请求;产生 http.Request
  • 将这个 http.Request 传递给用户定义的 handler

handler 是一个实现 http.Handler 接口的任意实例:

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

默认的 handler

在我们的实例代码中,ListenAndServe 被调用的时候使用 nil 作为第二个参数,而这个位置本应该使用用户定义的 handler,这是怎么回事?

我们的图简化了一些细节;实际上,当这个 HTTP 包处理一个请求的时候,它并不会直接调用用户的 handler,而是使用这个适配器:

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.ServeHTTP(rw, req)
}

注意高亮的部分(if handler == nil ...),如果 handler == nil,则 http.DefaultServeMux 被用作 handler。这个是默认的 server muxhttp 包中所包含的一个 http.ServeMux 类型的全局实例。顺便一提,当我们的示例代码使用 http.HandleFunc 注册 handler 函数的时候,会在同一个默认的 mux 上注册这些handler。

我们可以如下所示这样重写我们的示例代码,不再使用默认的 mux。只修改 main 函数,所以这里没有展示 helloheaders handler 函数,看是我们可以在这看完整的代码。功能上没有任何变化[^1]:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/hello", hello)
    mux.HandleFunc("/headers", headers)

    http.ListenAndServe(":8090", mux)
}

一个 ServeMux 仅仅是一个 Handler

当看多了 Go 服务的例子后,很容易给人一种 ListenAndServe 函数“需要一个 mux” 作为参数的印象,但是这是不准确的。就像我们之前所见到的那样,ListenAndServe 函数需要的是一个实现了 http.Handler 接口的值。我们可以写下面这样的服务而没有任何 mux:

type PoliteServer struct {
}

func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome! Thanks for visiting!\n")
}

func main() {
    ps := &PoliteServer{}
    log.Fatal(http.ListenAndServe(":8090", ps))
}

由于这里没有路由逻辑;所有到达 PoliteServerServeHTTP 方法的 HTTP 请求都会以同样的信息所回复。试着用不同的路径和方法 curl -ing 这个服务;返回一定是一致的。

我们可以使用 http.HandlerFunc 来进一步简化我们的 polite 服务:

func politeGreeting(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome! Thanks for visiting!\n")
}

func main() {
    log.Fatal(http.ListenAndServe(":8090", http.HandlerFunc(politeGreeting)))
}

HandlerFunc 是这样一个位于 http 包中的巧妙的适配器:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

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

如果你在这篇博文的第一个示例中注意到 http.HandleFunc[^2], 它对具有 HandlerFunc 签名的函数使用同样的适配器。

就像 PoliteServer 一样,http.ServeMux 是一个实现了 http.Handler 接口的类型。如果愿意的话你可以仔细阅读完整代码;这是一个大纲:

  • ServeMux 维护了一个(根据长度)排序的 {pattern, handler} 的切片。
  • HandleHandleFunc 向该切片增加新的 handler。
  • ServeHTTP

    • (通过查找这个排序好的 handler 对的切片)为请求的 path 找到对应的 handler
    • 调用 handler 的 ServeHTTP 方法

因此,mux 可以被看做是一个转发 handler;这种模式在 HTTP 服务开发中极为常见,这就是中间件

http.Handler 中间件

由于中间件在不同的上下文,不同的语言以及不同的框架中意味着不同的东西,所以它很难被准确定义。

让我们回到这篇博文开头的流程图上,对它进行一点简化,隐藏 http 包所执行的细节:

http-request-simplified.png?raw=true

现在,当我们加了中间件的话,流程图看起来是这样的:

http-request-with-middleware.png?raw=true

在 Go 语言中,中间件只是另一个 HTTP handler,它包裹了一个其他的 handler。中间件 handler 通过调用 ListenAndServe 被注册进来;当调用的时候,它可以执行任意的预处理,调用自身包裹的 handler 然后可以执行任意的后置处理。

我们之前已经见过了一个中间件的例子—— http.ServeMux;在这个例子中,预处理是基于请求的 path 来选择正确的用户 handler 来调用。没有后置处理。

再来另一个具体的例子,回到我们的 polite 服务上,新增一些基本的日志中间件。这个中间件记录每个请求的具体日志,包括执行了多长时间:

type LoggingMiddleware struct {
    handler http.Handler
}

func (lm *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    start := time.Now()
    lm.handler.ServeHTTP(w, req)
    log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))
}

type PoliteServer struct {
}

func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome! Thanks for visiting!\n")
}

func main() {
    ps := &PoliteServer{}
    lm := &LoggingMiddleware{handler: ps}
    log.Fatal(http.ListenAndServe(":8090", lm))
}

注意 LoggingMiddleware 本身是一个 http.Handler,它持有一个用户 handler 作为字段。当 ListenAndServe 调用它的 ServeHTTP 方法,它做了如下事情:

  1. 预处理:在用户的 handler 执行之前记录一个时间戳
  2. 使用请求和返回 writer 调用用户 handler
  3. 后置处理:记录请求详细日志,包括耗时

中间件最大的优点是可以组合。被中间件所包裹“用户 handler” 也可以是另一个中间件,依次类推。这是一个互相包裹的 http.Handler 链。事实上,这在 Go 中是一个常见的模式,来看看 Go 中间件的经典用法。还是我们的日志 polite 服务,这次使用了更有识别度的 Go 中间件实现:

func politeGreeting(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "Welcome! Thanks for visiting!\n")
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, req)
        log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))
    })
}

func main() {
    lm := loggingMiddleware(http.HandlerFunc(politeGreeting))
    log.Fatal(http.ListenAndServe(":8090", lm))
}

相对于创建一个带有方法的结构体,loggingMiddleware 利用 http.HandlerFunc 和闭包使代码更加简洁,同时保留了相同的功能。更重要的是这个例子展示了中间件事实上的标准签名:一个函数传入一个 http.Handler,有时还有其他状态,之后返回一个不同的 http.Handler。返回的 handler 现在应该替换掉传入中间件的那个 handler,之后会“神奇地”执行它原有的功能,并且与中间件的功能包装在一起。

比如。标准库包含了以下的中间件:

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler

如果我们的代码中有 http.Handler,像这样包装它:

handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out")

创建了一个新版本的 handler,这个版本内置了2秒的超时机制。

中间件的组合可以像下面这样展示:

handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out")
handler = loggingMiddleware(handler)

经过这样两行代码之后,handler 会带有超时和日志功能。你也许会注意到链路长的中间件编写起来会很繁琐;Go 有很多流行的包可以解决这个问题,不过这不在这篇文章的讨论范围内。

顺便一提,虽然 http 包在内部使用中间件满足自身需要;具体见这篇博文之前关于 serverHandler 适配器的例子。但是它提供了一个清晰的方式以默认行为处理用户 handler 为 nil 的情形(把请求传入默认的 mux)。

希望这样可以让大家明白为什么中间件是一个很吸引人的辅助设计。我们可以专注于我们的“业务逻辑” handler 上,尽管完全正交,我们利用通用的中间件,在许多方面提升我们的 handler。在其他文章中,会进行全面的探讨。

并发和 panic 处理

为了结束我们对于 Go HTTP 服务中 HTTP 请求的探索,来介绍另外两个主题:并发和 panic 处理。

首先是并发。之前简单提到,每个连接由 http.Server.Serve 在一个新的 goroutine 中处理。

这是 Go 的 net/http 的一个强大的功能,它利用了 Go 出色的并发性能,使用轻量的 goroutine 使 HTTP handler 保持了一个非常简单的并发模型。一个 handler 阻塞的时候(比如,读取数据库)不需要担心拖慢其他 handler。但是,编写存在共享数据的 handler 的时候需要格外小心。具体细节参考之前的文章

最后,panic 处理。一个 HTTP 服务通常是一个长期运行的后台进程。假如在用户提供的请求 handler 中发生了什么糟糕的事情,比如,一些导致运行时 panic 的bug。会导致整个服务崩溃,这可不是什么好事情。为了避免这样的惨剧,你也许会考虑在你服务的 main 函数中加上 recover,但是并没什么用,原因如下:

  1. 当控制返还给 main 函数的时候,ListenAndServe 已经执行完毕而不会再提供任何服务。
  2. 由于每个连接在分开的 goroutine 中处理,当 handler 中发送 panic 的时候,甚至不会影响到 main 函数,但是会导致对应进程的崩溃。

为了提供些许的帮助,net/http 包(在 conn.serve 方法中)内置对每个服务 goroutine 有 recovery。我们可以通过简单的例子来看到它的作用:

func hello(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "hello\n")
}

func doPanic(w http.ResponseWriter, req *http.Request) {
    panic("oops")
}

func main() {
    http.HandleFunc("/hello", hello)
    http.HandleFunc("/panic", doPanic)

    http.ListenAndServe(":8090", nil)
}

如果我们运行这个服务,并且 curl /panic 路径,我们可以看到:

$ curl localhost:8090/panic
curl: (52) Empty reply from server

并且服务会在自身 log 中打印这样的信息:

2021/02/16 09:44:31 http: panic serving 127.0.0.1:52908: oops
goroutine 8 [running]:
net/http.(*conn).serve.func1(0xc00010cbe0)
    /usr/local/go/src/net/http/server.go:1801 +0x147
panic(0x654840, 0x6f0b80)
    /usr/local/go/src/runtime/panic.go:975 +0x47a
main.doPanic(0x6fa060, 0xc0001401c0, 0xc000164200)
[... rest of stack dump here ...]

不过,这个服务会保持运行并且我们可以继续访问它!

尽管这个内置的保护机制相比服务崩溃要好,许多开发者还是发现了它的局限。这个保护机制只关闭了连接以及在日志中输出错误;通常来说,向客户端返回某种错误响应(比如 code 500 —— 内置错误)和附加详细信息会有用得多。

阅读了这个博文后,再写实现这个功能的中间件应该是很容易的。将它作为练习!我会在之后的博文中介绍这个用例。

[^1]: 与使用默认的 mux 的版本相比,这个版本有充分的理由更喜欢这一版本。默认的 mux 有着一定的安全风险;作为全局实例,它可以被你工程中引入的任何包所修改。一个恶意的包也行会出于邪恶的目的而使用它。 [^2]: 注意:http.HandleFunchttp.HandlerFunc 是具有不同而有相互关联的角色的不同实体。


via: https://eli.thegreenplace.net/2021/life-of-an-http-request-in-a-go-server/

作者:Eli Bendersky  译者:dust347  校对:polaris1119

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK