Go 语言标准库 net/http
包提供了非常易用的接口,如下所示,我们可以利用标准库提供的功能快速搭建新的 HTTP 服务:
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Go
上述的 main
函数只调用了两个标准库提供的函数,它们分别是用于注册处理器的 net/http.HandleFunc
函数和用于监听和处理器请求的 net/http.ListenAndServe
,多数的服务器框架都会包含这两类接口,分别负责注册处理器和处理外部请求,这一种非常常见的模式,我们在这里也会按照这两个维度介绍标准库如何支持 HTTP 服务器的实现。
注册处理器 #
HTTP 服务是由一组实现了 net/http.Handler
接口的处理器组成的,处理 HTTP 请求时会根据请求的路由选择合适的处理器:
图 9-11 HTTP 服务与处理器
当我们直接调用 net/http.HandleFunc
注册处理器时,标准库会使用默认的 HTTP 服务器 net/http.DefaultServeMux
处理请求,该方法会直接调用 net/http.ServeMux.HandleFunc
:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
Go
上述方法会将处理器转换成 net/http.Handler
接口类型调用 net/http.ServeMux.Handle
注册处理器:
func (mux *ServeMux) Handle(pattern string, handler Handler) {
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
Go
路由和对应的处理器会被组成 net/http.DefaultServeMux
,该结构会持有一个 net/http.muxEntry
哈希,其中存储了从 URL 到处理器的映射关系,HTTP 服务器在处理请求时就会使用该哈希查找处理器。
ServeMux 的自定义
我们前面小节讲述 conn.server 的时候,其实内部是调用了 http 包默认的路由器,通过路由器把本次请求的信息传递到了后端的处理函数。那么这个路由器是怎么实现的呢?
它的结构如下:
type ServeMux struct {
mu sync.RWMutex // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
m map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
hosts bool // 是否在任意的规则中带有 host 信息
}
下面看一下 muxEntry
type muxEntry struct {
explicit bool // 是否精确匹配
h Handler // 这个路由表达式对应哪个 handler
pattern string // 匹配字符串
}
接着看一下 Handler 的定义
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 路由实现器
}
Handler 是一个接口,但是前一小节中的 sayhelloName 函数并没有实现 ServeHTTP 这个接口,为什么能添加呢?原来在 http 包里面还定义了一个类型 HandlerFunc, 我们定义的函数 sayhelloName 就是这个 HandlerFunc 调用之后的结果,这个类型默认就实现了 ServeHTTP 这个接口,即我们调用了 HandlerFunc (f), 强制类型转换 f 成为 HandlerFunc 类型,这样 f 就拥有了 ServeHTTP 方法。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
处理请求 #
标准库提供的 net/http.ListenAndServe
可以用来监听 TCP 连接并处理请求,该函数会使用传入的监听地址和处理器初始化一个 HTTP 服务器 net/http.Server
,调用该服务器的 net/http.Server.ListenAndServe
方法:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
Go
net/http.Server.ListenAndServe
会使用网络库提供的 net.Listen
监听对应地址上的 TCP 连接并通过 net/http.Server.Serve
处理客户端的请求:
func (srv *Server) ListenAndServe() error {
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
Go
net/http.Server.Serve
会在循环中监听外部的 TCP 连接并为每个连接调用 net/http.Server.newConn
创建新的 net/http.conn
,它是 HTTP 连接的服务端表示:
func (srv *Server) Serve(l net.Listener) error {
l = &onceCloseListener{Listener: l}
defer l.Close()
baseCtx := context.Background()
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
...
return err
}
connCtx := ctx
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}
Go
创建了服务端的连接之后,标准库中的实现会为每个 HTTP 请求创建单独的 Goroutine 并在其中调用 net/http.Conn.serve
方法,如果当前 HTTP 服务接收到了海量的请求,会在内部创建大量的 Goroutine,这可能会使整个服务质量明显降低无法处理请求。
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
for {
w, _ := c.readRequest(ctx)
serverHandler{c.server}.ServeHTTP(w, w.req)
w.finishRequest()
...
}
}
Go
上述代码片段是我们简化后的连接处理过程,其中包含读取 HTTP 请求、调用 Handler 处理 HTTP 请求以及调用完成该请求。读取 HTTP 请求会调用 net/http.Conn.readRequest
,该方法会从连接中获取 HTTP 请求并构建一个实现了 net/http.ResponseWriter
接口的变量 net/http.response
,向该结构体写入的数据都会被转发到它持有的缓冲区中:
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
...
w.written += int64(lenData)
if w.contentLength != -1 && w.written > w.contentLength {
return 0, ErrContentLength
}
if dataB != nil {
return w.w.Write(dataB)
} else {
return w.w.WriteString(dataS)
}
}
Go
解析了 HTTP 请求并初始化 net/http.ResponseWriter
之后,我们就可以调用 net/http.serverHandler.ServeHTTP
查找处理器来处理 HTTP 请求了:
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)
}
Go
如果当前的 HTTP 服务器中不包含任何处理器,我们会使用默认的 net/http.DefaultServeMux
处理外部的 HTTP 请求。
net/http.ServeMux
是一个 HTTP 请求的多路复用器,它可以接收外部的 HTTP 请求、根据请求的 URL 匹配并调用最合适的处理器:
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
return RedirectHandler(p, StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
Go
经过一系列的函数调用,上述过程最终会调用 HTTP 服务器的 net/http.ServerMux.match
,该方法会遍历前面注册过的路由表并根据特定规则进行匹配:
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
Go
如果请求的路径和路由中的表项匹配成功,我们会调用表项中对应的处理器,处理器中包含的业务逻辑会通过 net/http.ResponseWriter
构建 HTTP 请求对应的响应并通过 TCP 连接发送回客户端。
Go 代码的执行流程
通过对 http 包的分析之后,现在让我们来梳理一下整个的代码执行过程。
首先调用 Http.HandleFunc
按顺序做了几件事:
1 调用了 DefaultServeMux 的 HandleFunc
2 调用了 DefaultServeMux 的 Handle
3 往 DefaultServeMux 的 map [string] muxEntry 中增加对应的 handler 和路由规则
其次调用 http.ListenAndServe (":9090", nil)
按顺序做了几件事情:
1 实例化 Server
2 调用 Server 的 ListenAndServe ()
3 调用 net.Listen ("tcp", addr) 监听端口
4 启动一个 for 循环,在循环体中 Accept 请求
5 对每个请求实例化一个 Conn,并且开启一个 goroutine 为这个请求进行服务 go c.serve ()
6 读取每个请求的内容 w, err := c.readRequest ()
7 判断 handler 是否为空,如果没有设置 handler(这个例子就没有设置 handler),handler 就设置为 DefaultServeMux
8 调用 handler 的 ServeHttp
9 在这个例子中,下面就进入到 DefaultServeMux.ServeHttp
10 根据 request 选择 handler,并且进入到这个 handler 的 ServeHTTP
mux.handler(r).ServeHTTP(w, r)
11 选择 handler:
A 判断是否有路由能满足这个 request(循环遍历 ServeMux 的 muxEntry)
B 如果有路由满足,调用这个路由 handler 的 ServeHTTP
C 如果没有路由满足,调用 NotFoundHandler 的 ServeHTTP
参考:
https://draveness.me/golang/docs/part4-advanced/ch09-stdlib/golang-net-http/