Go服务器中HTTP请求的生存期(2017)

2021-02-22 01:06:35

Go是编写HTTP服务器的通用且非常适合的工具。本文讨论了典型的HTTP请求通过Go服务器,touchupon路由器,中间件和其他相关问题(如并发性)所经过的路由。

要查看一些具体代码,让我们从这个琐碎的服务器(摘自https://gobyexample.com/http-servers)开始:

包主要导入(" fmt"" net / http")func hello(w http。ResponseWriter,req * http.Request){fmt。 Fprintf(w," hello \ n")} func头文件(w http。ResponseWriter,req * http。Request){对于名称,头文件:= range req。标头{表示_,h:=范围标头{fmt。 Fprintf(w,"%v:%v \ n",name,h)}}} func main(){http。 HandleFunc(" / hello",你好)http。 HandleFunc(" / headers",标头)http。 ListenAndServe(":8090",nil)}

我们将通过查看http.ListenAndServe函数开始通过该服务器跟踪HTTP请求的生命周期:

下图显示了调用过程的简化流程:

这是一个非常内联的"函数和方法调用的实际序列的版本,但是原始代码不太难遵循。

主要流程几乎是您所期望的:ListenAndServe在TCP端口上侦听给定的地址,然后循环接受新的连接。对于每个新的连接,它会旋转一个goroutine来为其服务(稍后会详细介绍)。连接涉及以下循环:

在我们的示例代码中,使用nil作为第二个参数调用ListenAndServe,它应该是用户定义的处理程序。这是怎么回事?

我们的图简化了一些细节。实际上,当HTTP包提供arequest时,它不会直接调用用户的处理程序,而是使用此适配器:

类型serverHandler struct {srv * Server} func(sh serverHandler)ServeHTTP(rw ResponseWriter,req * Request){handler:= sh。 srv。如果处理程序==零,则处理程序{处理程序= DefaultServeMux}如果需要。 RequestURI ==" *" &&要求方法=="选项" {handler = globalOptionsHandler {}}处理程序。服务HTTP(rw,req)}

请注意突出显示的行:如果handler == nil,则将http.DefaultServeMuxis用作处理程序。这是默认的服务器多路复用器-http包中保存的http.ServeMux类型的全局实例。顺便说一句,当我们的示例代码向http.HandleFunc注册处理程序时,它将为其注册这个非常相同的默认多路复用器。

我们可以按以下方式重写示例服务器,而无需使用默认的mux。仅主要功能发生了变化,因此我不会显示hello和headers处理程序,但是您可以在此处看到完整的代码。功能更改[1]:

在阅读大量Go服务器示例时,很容易相信ListenAndServe会使用多路复用器。作为论点,但这并不精确。如上所述,ListenAndServe所采用的是实现http.Handler接口的值。我们可以编写以下没有任何复用的服务器:

键入PoliteServer struct {} func(ms * PoliteServer)ServeHTTP(w http。ResponseWriter,req * http。Request){fmt。 Fprintf(w,"欢迎光临!\ n")} func main(){ps:=& PoliteServer {}日志。致命(http。ListenAndServe(":8090",ps))}

这里没有路由;所有HTTP请求都转到PoliteServer的ServeHTTP方法,并且对所有请求都返回相同的消息。尝试使用不同的路径和方法来卷曲服务器。如果不一致,那什么也没有。

func politeGreeting(w http.ResponseWriter,req * http.Request) Fprintf(w," Welcome!感谢您的光临!\ n")} func main(){log。致命(http。ListenAndServe(":8090",http。HandlerFunc(politeGreeting)))}

// HandlerFunc类型是一个适配器,允许将//普通函数用作HTTP处理程序。如果f是具有适当签名的函数//,则HandlerFunc(f)是//调用f的处理程序。类型HandlerFunc func(ResponseWriter,* Request)// ServeHTTP调用f(w,r)。 func(f HandlerFunc)ServeHTTP(w ResponseWriter,r * Request){f(w,r)}

如果您在文章[2]的第一个示例中注意到http.HandleFunc,它将对具有HandlerFunc签名的函数使用相同的适配器。

就像PoliteServer一样,http.ServeMux是实现http.Handler接口的类型。如果您喜欢,则可以仔细阅读其完整代码。这是一个大纲:

这样,多路复用器可以看作是转发处理程序;这种模式在HTTP服务器编程中极为常见。这是中间件。

很难精确定义中间件,因为中间件在不同的上下文,语言和框架中意味着稍有不同。

让我们从这篇文章的开头看一下流程图,并简化一点,隐藏http包的详细信息:

在Go中,中间件只是包装了不同处理程序的另一个HTTP处理程序。中间件处理程序注册为由ListenAndServe调用;当被调用时,它可以执行任意预处理,调用包装器处理程序,然后进行任意后处理。

我们已经在上面看到了一个中间件示例-http.ServeMux;在这种情况下,预处理将根据请求的路径选择要调用的正确用户处理程序。没有后处理。

再举一个具体的例子,让我们重新访问礼貌的服务器并添加一些基本的日志记录中间件。该中间件记录每个请求的详细信息,包括执行多长时间:

键入LoggingMiddleware struct {处理程序http。处理程序} func(lm * LoggingMiddleware)ServeHTTP(w http.ResponseWriter,req * http.Request){开始:=时间。 Now()lm。处理程序。服务HTTP(w,req)日志。 Printf("%s%s%s&#34 ;,要求方法,要求RequestURI,时间。自(开始))}类型PoliteServer struct {} func(ms * PoliteServer)ServeHTTP(w http。ResponseWriter, req * http。Request){fmt Fprintf(w,"欢迎光临!\ n")} func main(){ps:=& PoliteServer {} lm:=& LoggingMiddleware {handler:ps}日志。致命(http。ListenAndServe(":8090",lm))}

请注意,LoggingMiddleware本身就是一个http.Handler,将userhandler保留为一个字段。当ListenAndServe调用其ServeHTTP方法时,它会执行以下操作:

中间件的伟大之处在于它是可组合的。中间件包装的"用户处理程序可以是另一个中间件,依此类推。它是一串互相包装的http.Handler值。实际上,这是Go中常见的模式,它使我们了解Go中间件的典型外观。这再次是我们的loggingpolite服务器,具有更易于识别的Go中间件实现:

func politeGreeting(w http.ResponseWriter,req * http.Request) Fprintf(w," Welcome!感谢您的光临!\ n")} func loggingMiddleware(下一个http。Handler)http。处理程序{返回http。 HandlerFunc(func(w http。ResponseWriter,req * http。Request){start:= time。Now()next。ServeHTTP(w,req)log。Printf("%s%s%s", req。方法,req.RequestURI,时间,自(start))}}}} func main(){lm:= loggingMiddleware(http.HandlerFunc(politeGreeting))日志。致命(http。ListenAndServe(":8090",lm))}

loggingMiddleware并未使用方法创建结构,而是利用http.HandlerFunc与闭包相结合来使代码更加简洁,同时保留相同的功能。更重要的是,这演示了中间件的事实上的标准签名:该函数采用http.Handler(有时带有其他状态)并返回不同的http.Handler。现在应该使用返回的处理程序代替传递到中间件的处理程序,并且将“神奇地”使用。执行包含中间件功能的原始功能。

在这两行之后,处理程序将安装超时和日志记录。您可能会注意到,中间件的长链编写起来可能很繁琐。 Go有许多受欢迎的软件包可以解决此问题,但这超出了本文的范围。

顺便说一下,即使http包也可以在内部使用中间件来满足其需求;例如,参见本文前面描述的serverHandler适配器,它提供了一种干净的方法来以默认方式处理nil用户处理程序(将请求传递给默认的mux) 。

我希望这可以弄清楚中间件为什么是有吸引力的设计辅助工具。我们可以根据业务逻辑进行工作处理程序,而我们完全采用正交的中间件,可以通过多种方式增强处理程序。不过,我将在这里继续探讨其他职位的可能性。

为了完成对GoHTTP服务器中HTTP请求通过的内容的探索,让我们介绍另外两个主题:并发和紧急处理。

首先,并发。如上文所述,http.Server.Serve在新的goroutine中处理每个连接。

这是Go的net / http的强大功能,因为它利用了Go的出色的并发功能,廉价的goroutines确保了HTTP处理程序的非常干净的并发模型。处理程序可以阻塞(例如,通过从数据库读取),而不必担心使其他处理程序停顿。但是,这需要在编写具有共享数据的处理程序时要格外小心。阅读我的早期文章了解更多详细信息。

最后,进行紧急处理。 HTTP服务器通常意味着在后台运行。假设在某些用户提供的请求处理程序中发生了一些问题,例如某种导致运行时恐慌的错误。这可能会使整个服务器崩溃,这不是一个很好的方案。为了保护自己免受这种情况的侵扰,您可以考虑在服务器的主要功能中添加恢复功能,但是由于几个原因,该功能无济于事:

等到控件返回main时,ListenAndServe已经完成,因此不再进行任何服务。

由于每个连接都是在单独的goroutine中处理的,因此,来自处理程序的恐慌甚至不会到达main,而是会导致进程崩溃。

为了对此提供保护,net / http为每个服务goroutine内置了恢复功能(在conn.serve方法中)。我们可以通过一个简单的示例看到它:

func你好(w http。ResponseWriter,req * http。Request){fmt。 Fprintf(w," hello \ n")} func doPanic(w http。ResponseWriter,req * http。Request){恐慌(" oops")} func main(){http。 HandleFunc(" / hello",你好)http。 HandleFunc(" / panic",doPanic)http。 ListenAndServe(":8090",nil)}

2021/02/16 09:44:31 http:紧急服务127.0.0.1:52908:oopsgoroutine 8 [正在运行]:net / http。(* conn).serve.func1(0xc00010cbe0)/ usr / local / go / src / net / http / server.go:1801 + 0x147panic(0x654840,0x6f0b80)/usr/local/go/src/runtime/panic.go:975 + 0x47amain.doPanic(0x6fa060,0xc0001401c0,0xc000164200)[...其余部分转储到这里...]

尽管这种内置保护比使服务器崩溃更好,但许多开发人员发现它有一定的局限性。 它所做的只是关闭连接并记录错误; 通常,将某种错误响应(例如代码500-内部服务器错误)返回给客户端并附带其他详细信息会更加有用。 阅读了这篇文章之后,编写实现这一目标的中间件应该很容易。 试试看作为练习! 我将在以后的文章中介绍这个用例。 与使用默认多路复用器的版本相比,有充分的理由偏爱此版本。 默认的多路复用器存在一定的安全风险; 因为globalit可以通过您的项目导入的任何软件包进行修改。 恶意软件包可用于邪恶的需求。