写在前面
本次开个新源码解析专题,聚焦go语言的 http 标准库,同样是基于过往的学习笔记整理。我们知道,go语言的原生 http 标准库由于其底层基于IO多路复用和gocorutine调度两大利器,带来了十分优秀的性能。并且,诸如 gin 这样的 web 应用框架,都是基于 http 标准库对外提供服务的。无论从学习go语言的角度还是学习go语言web开发,http 标准库都值得一读。
本次专题的 http 标准库源码来自基于go1.17.2版本进行分析。
http server框架
以上是一个go语言原生 http 服务端的架构,自底向上,由网络调度层,http服务层,web应用层三个部分组成。
网络调度层
主要由net标准库实现,其作用是生成一个 http 服务端的socket,并将新到来的 TCP 连接封装成为一个 net.conn 结构,交给其上的http服务层进行处理。从图中可以看到,无论是服务端的监听socket还是来自客户端的新 TCP 连接,都是通过 Interface 的方式向上层提供底层的网络服务。这样做的好处就是 http 服务层不会感知到底层实现的具体细节。无论底层采用的网络模型是多线程还是IO多路复用,都不需要关心。http服务层只需要专注于自己的工作。
http服务层
这里就是 http 标准库的主要工作域。http 服务层每次在接收到新的连接(net.conn实例,其中封装了客户端连接的socket)时,都会启动一个 goroutine,来完成本次 http 请求的处理。在这个过程中,会生成 http.response 实例和 http.Request 实例。并交给 http 的应用层进行具体的业务处理,在请求处理完成后,往 net.conn 实例写入 http 响应数据。在这里我们同样可以看到,在封装好请求实例和响应实例后,http 应用层通过实现ServeHTTP(ResponseWriter, *Request)
这个方法的 Handler 向 http 服务层暴露业务端接口。这样做的好处同样也是显而易见的,用户可根据自己的需要编写业务逻辑并向 http 服务层注册应用入口。同样,应用层也屏蔽了服务层的实现细节。
web应用层
无论是自己编写还是使用 gin 这样的web框架作为应用层的实现,他们都是通过 http.Server 实例中的 Handler 成员变量注册到 http 服务中。
这个 Handler 成员变量是一个 Handler Interface。其定义中仅有一个 ServeHttp 方法。http.Server 就是通过 Handler Interface 中定义的 ServeHttp 方法,去调用应用层来完成一次 http 请求。
下面以 http.ServeMux 和 gin.Engine 为例子,通过代码看看应用层如何向 http 服务层注册服务。
http.ServeMux
http.ServeMux 是由go语言的 http 标准库提供的 http 服务层实现。可以把它看成是一个大的路由 map,通过 host 和 path 索引到对应的 Handler函数来处理请求。
下面这段代码是 http.ServeMux 的应用例子,在完成 http.ServeMux 实例的初始化并完成注册请求处理函数的路由注册后,下一段关键代码就是http.Server{ Addr: ":8080", Handler: mux, }
,它将 http.ServeMux 实例赋值给 http.Server 的 Handler 成员变量来注册服务。
go
type HelloHandler struct{}
func (HelloHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
fmt.Fprintln(rw, "xxxx")
}
func main() {
hello := HelloHandler{}
mux := http.NewServeMux()
mux.Handle("/hello", hello)
server := http.Server{
Addr: ":8080",
Handler: mux,
}
err := server.ListenAndServe()
if err != nil {
log.Println(err)
}
}
查看 http.ServeMux 的相关源代码可以看到,NewServeMux 返回的是一个 ServeMux 实例。而 ServeMux 结构体就实现了 Handler Interface。
每当 http 服务层调用 http.ServeMux.ServeHttp 方法时,该方法内会去调用 http.ServeMux.Handler 方法,通过 http.Request 实例中携带的 host和path 等信息,查找到相应的请求处理 Handler 来处理请求。
gin.Engine
以下代码摘抄自 gin 的官方文档。代码很简单,其首先通过 gin.Default 返回一个 gin.Engine 实例。在完成gin的路由注册后,调用 gin.Engine 的 Run 方法。gin应用的注册就是在这个 Run 方法中完成的。
css
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
在 Run 方法中,会去调用 http.ListenAndServe 方法。该方法的入参是 http 服务端的ip:port信息和 应用层的入口 Handler,这个Handler参数的类型同样是 Handler Interface。 gin.Engine 实例会通过它的 Handler 方法,会根据配置,返回一个指向自己的指针。
因为 gin.Engine 实例本身就实现了 ServeHTTP 方法,所以它也实现了 Handler Interface。因此,在 ListenAndServe 方法中,就将 gin.Engine 实例注册到了 http.Server 实例的 Handler 变量中。至此,gin应用便完成了注册。
总结
本篇作为 http 标准库源码分析的开篇,介绍了 go http 服务端的大体构成。再者通过介绍原生的 http.ServeMux 应用和 gin 应用的注册过程,让大家了解到不同的web应用是通过 Handler interface 去和 http 服务层交互的。http 标准库对 interface 特性的使用,解耦web应用层/http服务层和网络调度层,方便不同领域的开发者构建构建自己的web应用和网络服务。其对 interface 的使用对平时的开发工作很有参考意义。
后续篇章我们会将重点放在网络调度和http应用层的工作机制。