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

Mivinci · · 1410 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

之前出于好奇看了一下 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 这样的框架是如何实现这三个功能的,希望后面能有第二篇总结出现吧。。

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


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:Mivinci

查看原文:Golang ServeMux 是如何实现多路处理的

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1410 次点击  ∙  2 赞  
加入收藏 微博
1 回复  |  直到 2019-11-21 11:03:18
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传