Go 语言系列之 HTTP 服务器
初学者的疑问
让我们开门见山吧
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