Go Web (Part 1): Handler & ServeMux

1. Begin

使用Go语言的标准库net/http可以快速构建一个简单的web服务。这篇文章从零开始构建一个简单的web服务器,并主要聚焦于处理器Handler和多路复用器ServeMux。

在开始之前,MindMap里一无所有:

2. The Simplest Server

使用net/http包中的ListenAndServe()函数可以快速构建一个最简单的服务器:

package main

import (
    "net/http"
)

func main() {
    http.ListenAndServe("",nil)
}

使用go run命令执行程序,然后在本地浏览器打开http://localhost ,得到404 page not found的错误。

这就是一个最简单的服务器,什么也不做,但确实是一个服务器。

现在MindMap里多了点东西:

3. 发生了什么

调用http.ListenAndServe()函数就可以启动一个服务器,那么发生了什么呢?

查看源码可知这个函数的实现:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

原来是通过构造一个Server并调用这个ServerListenAndServe()函数实现的。

那也就是说,我们可以自己构造一个Server,然后启动服务器:

func main() {
    s := http.Server{
        Addr:    "127.0.0.1:8080",
        Handler: nil,
    }
    s.ListenAndServe()
}

启动之后结果一样(如果端口被占用可以换一个端口)。

使用http.ListenAndServe()函数可以使用两个参数(Addr服务器地址和Handler处理器)就可以启动一个简单的服务。如果需要更加复杂的设置,需要使用Server结构。Server定义如下:

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
    Addr    string  // TCP address to listen on, ":http" if empty
    Handler Handler // handler to invoke, http.DefaultServeMux if nil
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
    BaseContext func(net.Listener) context.Context
    ConnContext func(ctx context.Context, c net.Conn) context.Context
}

这样,MindMap中又多了点东西,也知道了一些关联:

3. 处理器Handler

由于Handler为空,所以这个服务器什么也不做只是返回404。为了让服务器做事情,需要添加处理器Handler:

type MyHandler struct{}

func (mh *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    s := http.Server{
        Addr:    "127.0.0.1:8080",
        Handler: &MyHandler{},
    }
    s.ListenAndServe()
}

执行结果就是得到了一个Hello World

这里定义了一个简单的处理器MyHandler,然后添加到定义的Server中。

所以为了让服务器有事情做,需要定义Handler告诉它做什么。一个Handler其实是一个接口:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任何结构只要有一个签名是这样的函数,都可以作为Server的处理器;同样,任何接受这两个参数的函数,也可以当做一个处理器。

不过这么做有个明显的缺陷就是,服务器不会通过URL来将请求路由到不同的处理器。为了能根据URL处理不同的请求,我们需要多个处理器。

4. 多个Handler

直接对Server指定Handler只能添加一个处理器,为了支持多个处理器,需要别的做法:

type HelloHandler struct{}

func (hh *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

type WorldHandler struct{}

func (wh *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "World")
}

func main() {
    s := http.Server{
        Addr: "127.0.0.1:8080",
    }

    http.Handle("/hello", &HelloHandler{})
    http.Handle("/world", &WorldHandler{})

    s.ListenAndServe()
}

在这里定义了两个Handler,然后通过http.Handle()函数注册这两个Handler而不是直接对Server指定,这样就可以使用两个处理器了。在浏览器中分别访问http://localhost:8080/hellohttp://localhost:8080/world 可以得到不同的结果。

来看看http.Handle()函数做了什么:

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

原来是把具体的Handler注册到了一个叫做DefaultServeMux的变量里,在详细看看具体做了什么之前,先看看处理器函数HandlerFunc

5. 处理器函数HandlerFunc

除了使用Handler结构外,还可以使用具有相同参数的函数,只不过需要使用http.HandleFunc()函数:

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}

func world(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "World")
}

func main() {
    s := http.Server{
        Addr: "127.0.0.1:8080",
    }

    http.HandleFunc("/hello", hello)
    http.HandleFunc("/world", world)

    s.ListenAndServe()
}

效果和使用http.Handle()一样。处理器函数能够完成和处理器一样的功能,代码也更简洁一些。可以在实际使用中根据不同场景使用不同的方式。比如,如果代码中已经有处理的接口,只需要添加一个ServeHTTP函数就可以把这个接口转换成处理器了。

来看看http.HandleFunc()做了什么:

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

又是DefaultServeMux,接下来详细看看这个是什么,以及它是怎么和Server关联起来的。

6. 多路复用器ServeMux和DefaultServeMux

不管使用http.Handle()注册处理器Handler还是使用http.HandleFunc()注册处理器函数HandlerFunc,内部都涉及到了一个变量DefaultServeMux,这个是默认的多路复用器,即ServeMux的一个实例:

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

多路复用器的作用就是根据URL将不同的请求路由到不同的处理器上进行处理:

ServeMux结构里有一个map,用来保存路由和处理器的映射:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry // slice of entries sorted from longest to shortest.
    hosts bool       // whether any patterns contain hostnames
}

ServeMuxHandle()HandleFunc()函数就是将处理器注册到这个map里的。

不过HandleFunc()怎么把一个函数变成一个处理器呢?

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

原来使用了一个名叫HandlerFunc的结构(注意和http.HandleFunc的区别)完成了转换。HandlerFunc定义如下:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

这样我们就理解了不管是http.Handle()还是http.HandleFunc()最终都归结于ServeMux.Handle()。如果不指定ServeMux的话,就使用默认的DefaultServeMux

还需要注意一点就是,ServeMux本身也实现了ServeHTTP()函数,也是可以作为处理器的,这就是DefaultServeMux可以作为处理器的原因。

7. ServeMux是如何路由的

我们已经大致了解了ServeMux是根据内部维护的一个map来找到具体的处理器的。这里来看看具体的实现细节,对于理解整个流程很有帮助。

回到最开始,不管是使用http.ListenAndServe()还是Server.ListenAndServe(),都是需要先Listen然后Serve,主要的过程就是创建一个TCP链接然后监听请求,最终就是到serve()函数里,主要代码如下:

func (c *conn) serve(ctx context.Context) {
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
    ...
}

这里使用了一个叫做serverHandler的代理:

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)
}

从这里可以看到DefaultServeMux就是默认的多路复用器。所以如果给Server指定了一个Handler,那么就会调用这个HandlerServeHTTP()函数;如果没有指定的话,就调用DefaultServeMuxServeHTTP()函数,毕竟我们在前面已经知道DefaultServeMux就是ServeMux的一个实例,而ServeMux实现了ServeHTTP()函数:

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
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)
    h.ServeHTTP(w, r)
}

这个函数所做的就是根据请求找到对应的Handler,然后让这个处理器完成具体的工作。下面就是ServeMux.Handler()函数的主要部分ServeMux.handler()

// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
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
}

里面调用了ServeMux.match()函数,其实就是根据URL到map去找具体的Handler

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // Check for exact match first.
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // Check for longest valid match.  mux.es contains all patterns
    // that end in / sorted from longest to shortest.
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

首先,精确匹配,找到了就返回。如果没有找到,就根据一定的规则来找,最后如果还没有找到,那么返回404。

在后面的那个循环中,来找最长匹配的URL。mux.es是一个切片,这里存储的都是在注册Handler时模式以/结尾的:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    ...
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }
    ...
}

这样就清楚了,路由过程如下:

  1. 精确匹配,找到就返回;
  2. 如果没有精确匹配到,就到所有注册Handler时模式以/结尾的模式中寻找,原则是最长匹配;
  3. 还没有找到的话,就404。

举个例子,比如注册时将hello这个处理器注册到了/hello/下,那么对于/hello/wrong这个请求如果精确匹配没找到的话就可以匹配/hello/,因为这个模式以/结尾;如果注册hello处理器的时候模式是/hello的话,那就不会把/hello/wrong路由到/hello对应的处理器上来了。

ServeMux有一个缺点就是无法使用变量来实现URL模式匹配,比如/post/123这种,可以使用第三方库来避开这个限制。

这篇文章梳理了一下net/http包中下面的几个概念:

  • http.ListenAndServe()
  • Server.ListenAndServe()
  • http.Handle()
  • Handler
  • http.HandleFunc()
  • HandlerFunc
  • DefaultServeMux

而在我们的MindMap中,形成了下面的结构:

To Be Continued…


 Previous
Go Web (Part 2): Request Go Web (Part 2): Request
在了解了使用net/http构建go web服务之后,这篇文章深入了解一下在Go中如何处理一个请求。 1. 请求包含什么HTTP Message有Request和Response两种。这里详细看看Request Message,一个请求
2019-12-24
Next 
What Happens When Using LOAD DATA What Happens When Using LOAD DATA
在之前的文章Files to MySQL中简单介绍了mysqlimport和LOAD DATA的使用,也知道了mysqlimport其实就是使用了LOAD DATA语句。在这篇文章中详细介绍一下LOAD DATA这个语句,来看看当使用这个语
2019-12-18
  You Will See...