初学者的疑问
让我们开门见山吧
package main
import (
"fmt"
"net/http"
)
// 自定义两个handler
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() {
// 将上述两个handler注册到对应的路由地址
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
http.ListenAndServe(":8090", nil)
}
代码逻辑很简单,运行之后的结果就是开启了一个端口为8090的HTTP服务。
初次见到类似的代码,我最大的疑问就是ListenAndServe第二个参数为啥是nil啊!
ListenAndServe第二个参数需要一个实现Handler接口的实例。
// net/http/server.go
func ListenAndServe(addr string, handler Handler) error
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
但是在上面的代码中,居然传递了nil,更神奇的是它居然能够生效。
我们使用curl访问之,发现hello、headers两个handler已经注册到HTTP服务器了。
✘ 🐂🍺 curl -v http://127.0.0.1:8080/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 27 Oct 2022 06:54:09 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 127.0.0.1 left intact
hello world.%
为什么能够生效?
答案当然是很简单的。代码的世界没有神奇一说,更没有魔法,一定是net/http具体的实现帮助我们在某处进行了”连接“。
与其单纯的探究这个问题,我们不妨提出一个更好的问题:Go语言的HTTP服务器的生命周期是什么样的。
具体来说,当一个request请求进入,HTTP服务器是如何将之交给与对应的handler处理,如何进行的匹配?如何返回的response?
HTTP 请求的生命周期
经过对代码的深入阅读,整理一下HTTP请求如何与HTTP服务器交互。
-
http.ListenAndServe首先在某端口上开始监听 -
HTTP client发出HTTP请求之后,需要与HTTP server建立连接,即建立两端的tcp连接 -
HTTP server开启一个goroutine,专门处理这个连接上的请求、响应:
1、在goroutine内部解析出request
2、根据请求路径匹配对应的handler
3、调用handler,处理这个请求,返回response
解决我的疑问
之前我提到的自定义handler是如何神奇的绑定到HTTP server上。根据上一节的「HTTP请求的生命周期」,现在我们很容易就能定位将HTTP server和自定义handler串通起来的连接点 —— 读取req之后的ServeHTTP方法。
重点就是这个『调用handler』的步骤。
// net/http/server.go
// 找到咯,就是在这里
// 这里就是调用handler的入口
serverHandler{c.server}.ServeHTTP(w, w.req)
下面进入这个调用的内部实现
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)
}
啊哈!
因为我们调用http.ListenAndServe(":8090", nil)时第二个参数为nil,所以HTTP服务器默认启用DefaultServeMux作为自己的handler。
if handler == nil {
handler = DefaultServeMux
}
而我们自定义的hello/headers两个handler就是注册在DefaultServeMux上的。我们有代码作证:
// 将hello/headers 绑定
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
// http.HandleFunc的实现
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
这样一切就明了啊,优咔哒,优咔哒!
不过这个是何物?
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
明白了,就是一个零值ServeMux的指针类型。下面我们就来讨论一下这个ServeMux。
说到底,ServeMux 是个啥啊?
所以这篇文章关ServeMux什么关系?到底是如何引入的ServeMux来着?
不要急,是这样的。让我们回顾一下。
// main主函数里面如此调用http.HandleFunc,注册hello自定义handler
http.HandleFunc("/hello", hello)
// http.HandleFunc本质是将传入的handler绑定到DefaultServeMux
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
1、main主函数里面如此调用http.HandleFunc,注册hello自定义handler。
2、http.HandleFunc本质是将传入的handler绑定到DefaultServeMux。
3、DefaultServeMux = &ServeMux{}。
没错,DefaultServeMux就是*ServeMux。
当我们调用http.ListenAndServe(":8080", nil)启动HTTP服务器,且第二个参数为nil时,实际上就是把DefaultServeMux作为第二个参数传递进去了啊。
好吧好吧,你说了这么多,不是第一节就已经说明的事情嘛。干嘛还翻来覆去的说个不停呢?
好,下面进入重点。
http.ListenAndServe的第二个参数是个啥类型?其实上面已经说过了,是一个实现http.Handler接口的实例。
// net/http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
从本质上来说,DefaultServeMux就是一个Handler接口实例而已。
所以为了实现hello handler的逻辑,即用户访问http服务器,返回用户hello字符串信息。
我们完全可以这么写。
type HelloHandler struct {
}
func (*HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello world.")
}
func main() {
helloHandler := &HelloHandler{}
http.ListenAndServe(":8080", helloHandler)
}
我们自定义了HelloHandler类型,其指针类型实现了Handler接口,所以直接传入http.ListenAndServe,即可发挥作用。此时我们curl下,成功返回“hello world."字符串。
但是问题有两个:
1、提供一个handler处理函数,就需要我们自定义一个实现类型,并且该类型需要实现ServeHTTP方法以便满足Handler接口。这也太麻烦了吧。
啊,对对对对,说的没错。所以构建net/http包的大佬,为我们提供了一个http.HandlerFunc类型。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
http.HandlerFunc本质上是一个自定义类型,实现了ServeHTTP方法。
如此一来通过强制转换http.HandlerFunc(hello),就可以将类似hello这样平平无奇的函数,变成满足http.Handler接口的实例。HandlerFunc(hello)类似整数类型的强制转换,比如int32(666)。当调用ServeHTTP的时候,其实就是调用了hello本身嘛。
我们改写代码如下:
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello world.")
}
func main() {
http.ListenAndServe("8080", http.HandlerFunc(hello))
}
真是精彩!
2、虽然一行代码就启动了HTTP服务器。可是这个服务器,好像只有一个handler方法?
http.ListenAndServe("8080", http.HandlerFunc(hello))
不论我们:
curl -v http://127.0.0.1:8080/h
curl -v http://127.0.0.1:8080/
curl -v http://127.0.0.1:8080/hello
curl -v http://127.0.0.1:8080/shit
都会一丝不苟的返回hello world字符串。
咋搞哒!这能叫HTTP服务器?这不就是一个小玩具吗?
现在收敛心神,让我们从头开始。
func main() {
http.HandleFunc("/hello", hello) // 注册到DefaultServeMux上,A ServeMux is just a Handler
http.HandleFunc("/headers", headers)
//
http.ListenAndServe(":8080", nil)
}
http.HandleFunc分明就是把路由注册到了DefaultServeMux上!然后给每个tcp连接开启的goroutine上,通过适配器serverHandler会匹配请求的路由,调用相应的handler。
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{}
}
// 调用DefaultServeMux的ServeHTTP方法
// ServeHTTP方法实现路由匹配机制,匹配之后调用我们自定义的handler处理方法
handler.ServeHTTP(rw, req)
}
DefaultServeMux的ServeHTTP逻辑
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) // 根据r的请求路由,返回相应的下一级handler
h.ServeHTTP(w, r) // 调用我们自定义的handler,比如hello
}
哦,一切明了,没有魔法。
DefaultServeMux本质上就是一个实现http.Handler接口的Handler实例。
DefaultServeMux里面也注册了很多Handler,根据路由选择对应的Handler。
所以我们完全可以将最开始的HTTP服务器代码改造如下
func main() {
//http.HandleFunc("/hello", hello)
//http.HandleFunc("/headers", headers)
smx := http.NewServeMux()
smx.HandleFunc("/hello", hello)
smx.HandleFunc("/headers", headers)
http.ListenAndServe(":8090", smx)
}
我们主动创建了一个ServeMux,然后将两个自定义的handler绑定,最后调用ListenAndServe的时候主动将ServeMux作为第二个参数传递。
这样可比传递nil,内部使用DefaultServeMux的方式更容易让人理解呀。
看到这里,你是否已经晕了?就算晕了也不要紧,在你回看之前。重点记住我下面的论述:
-
Q:当我说handler的时候,我指的是什么? -
A:handler就是实现了http.Handler接口的实例。 -
Q:handler重要吗? -
A:重要。因为在HTTP服务器中哪里都是handler,到处都是handler,我们可以自定义handler,标准库中内也有自己实现好的handler,handler是HTTP服务器非常重要的一部分。
好了,建议你在重新看一遍上面的章节。
让我们继续向更多的 handler 前进
上面说到处都是handler,下面继续深化这个概念,并且狂野一点,让我们开启链式handler模式。
DefaultServeMux本身是个handler,然后上面绑定了我们自定义的handler(比如hello和headers),本质上是外层handler包裹了内层handler。你可能想到了,内层的handler可以继续绑定handler,一直组合一直组合,搞一个链式调用。
通过不断的组合,我们可以增加额外的处理逻辑,这就是传说中的前置处理器和后置处理器。
DefaultServeMux就是有自己的前置处理器。
它的前置处理器其实我们已经接触过了:根据路由选择具体的下一级的Handler。
为了复习和验证上面的知识,我们搞一个多条链路组合的handler。
加什么好呢?
搞一个计时日志吧!
干!
package main
import (
"fmt"
"net/http"
"time"
)
type LogHandler struct {
handler http.Handler
}
func (l LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
now := time.Now()
l.handler.ServeHTTP(w, r)
fmt.Printf("serve this handler cost %v\n", time.Since(now))
}
// 最内层的handler
// 当目前hello只是一个普通的函数
func helloHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func main() {
smx := http.NewServeMux()
logHandler := LogHandler{handler: http.HandlerFunc(helloHandler)}
smx.Handle("/hello", logHandler)
http.ListenAndServe(":8000", smx)
}
当我们发送HTTP请求后
🐂🍺 curl -v http://127.0.0.1:8000/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 30 Nov 2022 13:57:22 GMT
< Content-Length: 6
< Content-Type: text/plain; charset=utf-8
<
hello
* Connection #0 to host 127.0.0.1 left intact
* Closing connection 0
果然打印了
serve this handler cost 56.305µs
这次套了一层,我们还可以继续套娃呢!
func main() {
smx := http.NewServeMux()
timeoutHello := http.TimeoutHandler(http.HandlerFunc(helloHandler), 1*time.Second, "you are time out")
logHandler := LogHandler{handler: timeoutHello}
// defaultServerMux(Handler) > LogHandler > Timeout Handler > Hello
smx.Handle("/hello", logHandler)
http.ListenAndServe(":8000", smx)
}
当我们调用时
🐂🍺 curl -v http://127.0.0.1:8000/hello
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 503 Service Unavailable
< Date: Wed, 30 Nov 2022 14:02:31 GMT
< Content-Length: 16
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 127.0.0.1 left intact
you are time out* Closing connection 0
返回了503的错误信息哈
http服务器打印的日志如下
serve this handler cost 1.005334466s
ps:有没有觉得这种handler的组合内嵌的使用方法,类似于Python里面的装饰器呢!
总结
Go原生的net/http包非常强大,默认就提供了并发请求的支持,给每个TCP连接都开启了各自的goroutine。
http.Handler接口是如此的方便和强大,实现一个优秀的HTTP服务器,我们要做的事情就是构建各种handler。
为此,我们可以:
1、自定义函数,然后通过http.HandlerFunc强制转换为handler。
2、自定义类型,然后通过实现ServeHTTP方法成为handler实例。
3、使用默认的DefaultServeMux或者新建NewServeMux。
4、调用内置的Handler函数,比如TimeoutHandler。
然后综合上述实现Handler接口的方式,进行组合吧。
我们组合的这个行为,就是一个函数(方法),接收一个handler,返回一个新的handler。
这种方式叫做适配器模式。
通过适配器模式组合的handler,在Go中,称之为中间件(middleware)
Go的net/http包可真有意思。