27

Golang ServeMux 是如何实现多路处理的

 4 years ago
source link: https://studygolang.com/articles/24813
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前出于好奇看了一下 Golang net/http 包下的部分源码,今天想还是总结一下吧。由于是第一次写文章且抱着忐忑的心情发表,可能有些语义上的不清楚,谅解一下,或者提出修改的建议!

简介

net/http 包里的 server.go 文件里注释写着:ServeMux is an HTTP request multiplexer. 即 ServeMux 是一个 HTTP 请求的 "多路处理器",因为 ServeMux 实现的功能就是将收到的 HTTP 请求的 URL 与注册的路由相匹配,选择匹配度最高的路由的处理函数来处理该请求。

最简单的栗子:

mux := http.NewServeMux()
mux.HandleFunc("/a/b", ab)
mux.HandleFunc("/a", a)
http.ListenAndServe(":8000", mux)
复制代码

每个路由对应了一个处理函数。

先来看看 NewServeMux 函数

func NewServeMux() *ServeMux { return new(ServeMux) }
复制代码

我们知道 new 函数会为传入的类型分配空间并返回指向该空间首地址的指针,于是我们就获取了一个 ServeMux 实例。

源码分析

ServeMux 结构体

接下来就是 ServeMux 的结构

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry 
    hosts bool  // 标记路由中是否带有主机名
}
复制代码

其中 m 就是用来存储路由与处理函数映射关系的 map, es 按照路由长度从大到小的存放处理函数 (后面会讲为什么要这样),但 ServeMux 为了方便,map存放的值其实是放有处理函数和路由路径的 muxEntry 结构体:

type muxEntry struct {
    h       Handler  // 处理函数
    pattern string   // 路由路径
}
复制代码

ServeMux 暴露的方法主要是下面 4 个:

func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
复制代码

Handle 方法

Handle 方法通过将传入的路由和处理函数存入 ServeMux 的映射表 m 中来实现 "路由注册(register)"

源码具体实现如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    // 检查路由路径是否为空
    if pattern == "" {
    	panic("http: invalid pattern")
    }
    // 检查处理函数是否为空
    if handler == nil {
    	panic("http: nil handler")
    }
    // 检查该路由是否已经注册过
    if _, exist := mux.m[pattern]; exist {
    	panic("http: multiple registrations for " + pattern)
    }
    // 如果还没有任何路由注册,就为 mux.m 分配空间
    if mux.m == nil {
    	mux.m = make(map[string]muxEntry)
    }
    // 实例化一个 muxEntry
    e := muxEntry{h: handler, pattern: pattern}
    // 将该路由与该 muxEntry 的实例存到 mux.m 中
    mux.m[pattern] = e
    // 如果该路由路径以 "/" 结尾,就把该路由按照大到小的路径长度插入到 mux.e 中
    if pattern[len(pattern)-1] == '/' {
    	mux.es = appendSorted(mux.es, e)
    }
    // 如果该路由路径不以 "/" 开始,标记该 mux 中有路由的路径带有主机名
    if pattern[0] != '/' {
    	mux.hosts = true
    }
}
复制代码

HandleFunc 方法

HandleFunc 方法接收一个具体的处理函数将其包装成 Handler:

type HandlerFunc func(ResponseWriter, *Request)

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
    	panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}
复制代码

其中 HandlerFunc(f) 起到的作用就是在 HandlerFunc 中执行 f

Handler 方法

Handler 方法从传入的请求(Request)中拿到 URL 进行匹配, 返回对应的处理函数和路由

在看 Handler 的实现前,先看看它调用的 handler 方法:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // 若当前 mux 中注册有带主机名的路由,就用"主机名+路由路径"去匹配
    // 也就是说带主机名的路由优先于不带的
    if mux.hosts {
    	h, pattern = mux.match(host + path)
    }
    // 所以若没有匹配到,就直接把路由路径拿去匹配
    if h == nil {
    	h, pattern = mux.match(path)
    }
    // 若都没有匹配到,就默认返回 NotFoundHandler,该 Handler 会往
    // 响应里写上 "404 page not found"
    if h == nil {
    	h, pattern = NotFoundHandler(), ""
    }
    // 返回获得的 Handler 和路由路径
    return
}
复制代码

好了,现在是 Handler 方法

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    // 去掉主机名上的端口号
    host := stripHostPort(r.Host)
    // 整理 URL,去掉 ".", ".."
    path := cleanPath(r.URL.Path)

    // redirectToPathSlash 在 mux.m 中查看 path+"/" 是否存在
    // 如果存在,RedirectHandler 就将该请求重定向到 path+"/"
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
    	return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    // 如果整理后的 URL 与请求中的路径不一样,先调用 handler 进行匹配
    // 在将请求里的 URL 改成整理后的 URL
    // 最后将该请求重定向到整理后的 URL
    if path != r.URL.Path {
    	_, pattern = mux.handler(host, path)
    	url := *r.URL
    	url.Path = path
    	return RedirectHandler(url.String(), StatusMovedPermanently), pattern
    }
    // 若以上条件都不满足则返回匹配结果
    return mux.handler(host, r.URL.Path)
}
复制代码

我们有必要看看 match 方法是怎么进行匹配的

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // 若 mux.m 中已存在该路由映射,直接返回该路由的 Handler,和路径
    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, ""
}
复制代码

注意这里是在 mux.es 中进行查找,而不是映射表 mux.m 中,而 mux.es 是存放所有以 "/" 结尾的路由路径的切片。因为只会在以 "/" 结尾的路由路径中才会出现需要选择最长匹配方案

比如注册的路由有

mux.HandleFunc("/a/b/", ab)
mux.HandleFunc("/a/", a)
复制代码

那么当一个请求的 URL 为 /a/b/c 的时候,我们希望是由 ab 来处理这个请求。

另外,为了减少在 mux.es 中的查询时间, mux.es 中元素是按照它们的长度由大到小顺序存放的。

ServeHTTP 方法

我们知道在 Go 中要实现一个处理请求的 handler 结构体需要让该结构体现实 Handler 接口的 ServeHTTP 方法:

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

type myHandler struct {}

func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("This message is from myHandler."))
}

func main() {
    http.Handle("/", &helloHandler{})  // 路由注册
}
复制代码

我们已经通过 Handler 方法拿到了请求(Request)和它对应的处理函数(Handler)

我们的 ServeMux 是一个结构体,它的 ServeHTTP 方法要做的就是将每个请求派遣(dispatch)到它们对应的处理函数上。

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    // 如果请求路径为 "*",告诉浏览器该连接已关闭并返回状态码 400
    if r.RequestURI == "*" {
    	if r.ProtoAtLeast(1, 1) {
    	    w.Header().Set("Connection", "close")
    }
    	w.WriteHeader(StatusBadRequest)
    	return
    }
    // 调用 mux.Handler 方法获取请求和它对应的处理函数
    h, _ := mux.Handler(r)
    // 将 ResponseWriter 和 *Request 类型的参数传给处理函数
    h.ServeHTTP(w, r)
}
复制代码

这样,每收到一个请求就会调用对应的处理函数来处理该请求了。

关于 http.HandleFunc 方法

但我们通常会看到,一些简单的示例代码是下面这样写的:

func helloHandler(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.ListenAndServe(":8000", nil)
}
复制代码

我们可以看一下 http.HandleFunc 方法做了些什么:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}
复制代码

可以看到该方法中使用了一个 DefaultServeMux 来注册传入的路由,继续看:

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
复制代码

可以看到, http.HandleFunc 也是通过实例化一个全局的 ServeMux 来进行路由注册的。

总结

我们已经了解了 ServeMux 是怎么实现多路处理了,简单概括一下。 HandleHandleFunc 方法用来将路由路径与处理函数的映射通过一个 map 记录到当前的 mux 实例里; Handler 方法将接收的请求中的 URL 预处理后拿去和记录的映射匹配,若匹配到,就返回该路由的处理函数和路径; ServeHTTP 方法将请求派遣给匹配到的处理函数处理。

但是 ServeMux 的多路处理实现并不支持 请求方法判断 ,也不能处理 路由嵌套URL变量值提取 的功能

所以最近在分析 ginkratos/blademaster 这样的框架是如何实现这三个功能的,希望后面能有第二篇总结出现吧。。

QB32AfZ.jpg!web

其实写到一半的时候发现掘金上已经有 这部分的源码分析 了,于是就看了一下,和自己想的差不多,哈哈


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK