解读Go框架 - http包(一)
一、从一个简单的例子开始
在《Go语言圣经》的第一章有这么一个例子:
1 | package main |
这是一个基于 HTTP 协议的 WEB 服务,通过监听 9090端口
来响应客户端的请求;从上面可以看出,编写一个 WEB 服务器很简单,只要调用 http 包的两个函数就可以了;并且实际上这个 WEB 服务内部支持高并发,也就是在同一时间可以响应来源多个客户端的请求;Go 大道至简的设计理念,对于长期沉浸在 C/C++ 程序员是一种极大的解放。
那么,Go 是如何实现WEB高并发的?带着这个问题,开始一步步来分析。
二、Go如何使得WEB工作
首先简单绘制一张 Go 实现 Web 服务的工作模式的流程图
inet/http包的执行过程大致分为:
-
创建 Listen Socket,开始监听指定的端口。
-
当 Listen Socket 接收到来自客户端的请求时,经过鉴权放行后,创建 Client Socket,接下来由 Client Socket 与客户端通信。
-
Client Socket 读取客户端 HTTP 请求的协议头,如果是 POST 方法,继续读取 HTTP 请求的 BODY,然后交由 handler 处理。
-
handler 处理完毕准备好客户端需要的数据, 通过 Client Socket 返回给客户端。
整个过程中,比较关注三个核心问题:
- 监听机制
- 如何接收多个请求
- 如何分发
接下来进行逐个源码分析。
三、监听机制
前面的例子中可以看到,Go 实际上通过一个函数 ListenAndServe
来实现对端口的监听,首先分析其实现:
1 | func ListenAndServe(addr string, handler Handler) error { |
在 ListenAndServe
函数中,初始化一个 Server 对象并调用其 ListenAndServe
方法,并且接收了一个 addr 字符串 和 Handler对象;继续看看 Server 这个对象的 ListenAndServe
方法
1 | func (srv *Server) ListenAndServe() error { |
可以看到其调用了 net.Listen("tcp", addr)
,也就是这个方法作用是:用TCP协议搭建了一个监听者服务 net.Listener
,然后监控我们设置的端口,最后将这个监听者传递给了 Server 对象的 Serve
方法。
1 | func (srv *Server) Serve(l net.Listener) error { |
小结一下: Go 通过初始化一个 Server 对象并调用其 ListenAndServe
方法,创建一个监听者服务 net.Listener
,并将它加入到自己的追踪map中,并在接下来的监听过程中轮询 net.Listener
是否接收到来自客户端的请求。上述即是完成了 第二章 中的第一步:创建 Listen Socket。
四、如何接收多个请求
刚刚分析到 Server 对象的 Serve
方法通过服务监听配置,对监听者服务对象进行追踪,那么问题来了:监控之后如何接收客户端的请求呢?找寻这个问题答案之前,可以先看看 Serve
方法的注释
1 | // Serve accepts incoming connections on the Listener l, creating a |
这里简单翻译下,核心是:Serve 方法接收 net.Listener
对象传入的每个连接,为其每个创建一个 goroutine
新服务,每个服务读取请求内容并调用对应的 srv.Handler
来处理请求。
接下来继续分析 Serve 方法:
1 | func (srv *Server) Serve(l net.Listener) error { |
到这里可以发现:用户的每一次请求都是在一个新的goroutine去服务,相互不影响;Go语言借助每个请求基于独立协程的方式,来实现高并发能力。 而 srv.newConn
创建出来的 conn 对象亦是 第二章 中对应的 Client Socket。
五、如何分发
5.1 注册路由
前面阐述的是 Listen Socket
和 Client Socket
的创建和数据转发过程,那么接收到请求后,又是如何具体分配到相应的函数来处理呢?
在前面示例代码中:
1 | http.HandleFunc("/", sayHello) |
可以看到,调用了 HandleFunc 方法,将 sayHello 函数与 “/” 路径进行绑定;具体实现如下:
1 | func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { |
其中 DefaultServeMux
定义为
1 | var DefaultServeMux = &defaultServeMux |
从命名上大致可以猜出,DefaultServeMux
是一个默认提供的服务路由多路复用器,简单说就是一个路由器,用来匹配url跳转到相应的handle方法。上层业务调用 http.HandleFunc
,实际上是将请求/
的路由规则注册到 DefaultServeMux
的 muxEntry
中。
而 sayHello
的方法签名为 func(ResponseWriter, *Request)
,在 HandleFunc
可以看到是对应的;但是到了
DefaultServeMux.HandleFunc
是不是一样的呢?
1 | func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { |
可见,handle
的方法签名依旧是 func(ResponseWriter, *Request)
,这里细心点会发现一处类型转换,即 HandlerFunc(handler)
,每个细节都不能放过,看看这个类型是如何定义的:
1 | type HandlerFunc func(ResponseWriter, *Request) |
HandlerFunc这个类型下,存在着一个 ServeHTTP
方法,这个方法没有做其他事情,只是原封不动的调用了其自己,为什么要如此设计?先暂时带着这个疑问继续往下追查 mux.Handle
:
1 | func (mux *ServeMux) Handle(pattern string, handler Handler) { |
发现没有,第二个参数变成了 Handler
类型,而不再是 func(ResponseWriter, *Request)
,查看 Handler
的定义:
1 | type Handler interface { |
发现它实际上是一个接口类型,声明了 ServeHTTP(ResponseWriter, *Request)
方法,而这个方法就是 HandlerFunc
类型所实现的方法。也就是说,到 ServeMux
这一层,所有的handle方法通过类型转换,实际上都是遵循了 Handler
接口定义,即都是 ServeHTTP
方法。ServeHTTP
方法存在的意义看似为了某些一致性,但是截止现在,还不能理解具体是为了什么?先继续把 Handle
方法读完,可见:转换成 Handler 对象后,生成了对应的 muxEntry 对象,存放于路由配置map中。至此,路由配置完成。
5.2 路由分发
接下来回到 第四章 最后 srv.newConn
创建出来的 conn 对象的地方,用户的每一次请求都是在一个新的goroutine 去服务,那就对 conn 对象一探究竟,延续 第四章 最后的 go c.serve(connCtx)
,看看 conn 的 serve 方法做了什么:
1 | func (c *conn) serve(ctx context.Context) { |
首先会解析 request 内容,生成 response
对象w,并将生成的 Request
对象赋值给 w.req
;接下来通过 c.server
寻址到其所对应的 Server
对象,然后创建 serverHandler
结构体,而存在于 serverHandler.ServeHTTP
接下来被马上执行;通过上面源码可以看出,ServeHTTP
方法首先会获取 server
对象的 Handler
成员,这个对象怎么来的呢?其实就是示例代码中第二句中方法的第二个参数:
1 | err := http.ListenAndServe(":9090", nil) |
然而示例一开始就并未给它赋值,所以接下来会执行 handler == nil
中部分,即取出 DefaultServeMux
,然后调用其 ServeHTTP
方法;等等,这个 DefaultServeMux
不就是刚刚 **5.1 章节 ** 分析过的吗?它负责管理路由规则和handler的绑定。而 DefaultServeMux
本身是 ServeMux
类型,其存在方法 ServeHTTP
:
1 | func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { |
而 ServeHTTP
方法内部通过 Handler
方法有转发调用了 私有handler
方法
1 | func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { |
1 | func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { |
到 handler
方法这里,可以看到 ServeMux
对象通过 match
进行了路由规则匹配,也就通过查找自己的 路由规则map 找到对应的 Handler
对象,然后一路返回到 ServeMux.ServeHTTP
方法中,最后调用 handler.ServeHTTP
:
1 | func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { |
前面 5.1 章节 分析过,HandleFunc
方法在其底层会将传递进来的handle方法通过类型转换成 HandlerFunc
类型,而 HandlerFunc
实现了 Handler
接口的方法 ServeHTTP
, 在方法内调用业务传递进来的方法。所以,ServeMux 通过match找到对应的Handler后,就调用了其对应的handler方法。到这里,路由分发工作就完成了(过程中省略讲述错误判断、异常处理、https、http2.0的处理过程等)。
VI. 总结
HTTP WEB服务的整体流程大致如下:
- 调用
http.HandleFunc
- 创建
DefaultServeMux
- 将上层 handler 方法转换为
HandlerFunc
类型 - 将转换后的
HandlerFunc
类型生成muxEntry
对象,然后存入map中
- 创建
- 调用
ListenAndServe
- 创建
net.Listener
,并将它加入到自己的追踪map中 - 轮询
net.Listener
是否接收到来自客户端的请求
- 创建
net.Listener
收到来自客户端的请求- 开启一个goroutine,执行
conn
对象的serve
方法 - 解析 request 内容,生成
response
对象 c.server
寻址到其所对应的Server
对象,找到Server.handler
对象(默认为DefaultServeMux)- 执行
Server
的Handler
方法,进行match工作(如果找没有路由满足,则调用NotFoundHandler
) - 找到对应的handle方法(实际上是通过
HandleFunc
包装过),执行之 - 处理结束后,返回对应的
response
数据 - 关闭连接
- 开启一个goroutine,执行