之前出于好奇看了一下 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 是怎么实现多路处理了,简单概括一下。Handle
和 HandleFunc
方法用来将路由路径与处理函数的映射通过一个 map 记录到当前的 mux 实例里;Handler
方法将接收的请求中的 URL 预处理后拿去和记录的映射匹配,若匹配到,就返回该路由的处理函数和路径;ServeHTTP
方法将请求派遣给匹配到的处理函数处理。
但是 ServeMux 的多路处理实现并不支持请求方法判断,也不能处理路由嵌套和URL变量值提取的功能
所以最近在分析 gin、kratos/blademaster 这样的框架是如何实现这三个功能的,希望后面能有第二篇总结出现吧。。
其实写到一半的时候发现掘金上已经有这部分的源码分析了,于是就看了一下,和自己想的差不多,哈哈