1. http服务端总览和web应用注册

296 阅读5分钟

写在前面

本次开个新源码解析专题,聚焦go语言的 http 标准库,同样是基于过往的学习笔记整理。我们知道,go语言的原生 http 标准库由于其底层基于IO多路复用和gocorutine调度两大利器,带来了十分优秀的性能。并且,诸如 gin 这样的 web 应用框架,都是基于 http 标准库对外提供服务的。无论从学习go语言的角度还是学习go语言web开发,http 标准库都值得一读。

本次专题的 http 标准库源码基于go1.17.2版本进行分析。

http server框架

go http server-导出 (2).png

以上是一个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 服务层中。

企业微信截图_8df29d2c-6e44-489e-9a8b-d896b15348c5.png 这个 Handler 成员变量是一个 Handler Interface。其定义中仅有一个 ServeHttp 方法。http.Server 就是通过 Handler Interface 中定义的 ServeHttp 方法,去调用应用层来完成一次 http 请求。

企业微信截图_a63fda58-e50a-4793-b1ba-68ce72820687.png 下面以 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 成员变量来注册服务。


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。

image.png 每当 http 服务层调用 http.ServeMux.ServeHttp 方法时,该方法内会去调用 http.ServeMux.Handler 方法,通过 http.Request 实例中携带的 host和path 等信息,查找到相应的请求处理 Handler 来处理请求。 企业微信截图_f178623d-1764-4d3a-b4da-9f2972f6363d.png

gin.Engine

以下代码摘抄自 gin 的官方文档。代码很简单,其首先通过 gin.Default 返回一个 gin.Engine 实例。在完成gin的路由注册后,调用 gin.Engine 的 Run 方法。gin应用的注册就是在这个 Run 方法中完成的。

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。 企业微信截图_e221aa7d-488a-4946-8216-89e5b276a0de.png gin.Engine 实例会通过它的 Handler 方法,会根据配置,返回一个指向自己的指针。

因为 gin.Engine 实例本身就实现了 ServeHTTP 方法,所以它也实现了 Handler Interface。因此,在 ListenAndServe 方法中,就将 gin.Engine 实例注册到了 http.Server 实例的 Handler 变量中。至此,gin应用便完成了注册。

企业微信截图_67ec462d-8504-4fc7-bcc5-83e0540f1314.png

企业微信截图_01b70c5c-bd7d-4ad1-907d-4541a094ef75.png

总结

本篇作为 http 标准库源码分析的开篇,介绍了 go http 服务端的大体构成。再者通过介绍原生的 http.ServeMux 应用和 gin 应用的注册过程,让大家了解到不同的web应用是通过 Handler interface 去和 http 服务层交互的。http 标准库对 interface 特性的使用,解耦web应用层/http服务层和网络调度层,方便不同领域的开发者构建构建自己的web应用和网络服务。其对 interface 的使用对平时的开发工作很有参考意义。

后续篇章我们会将重点放在网络调度和http应用层的工作机制。