当前位置: 首页 > 工具软件 > issue9/mux > 使用案例 >

[服务计算] gorilla/mux源码阅读

唐俊英
2023-12-01

概述

gorilla/mux(HTTP request multiplexer,github地址)是一个对请求进行路由的实现。它有以下这些特点:

  • 实现了http.Handler接口,因此可以兼容标准的http.ServeMux
  • 可以匹配URL 主机,路径,路径前缀,头部,查询值,HTTP方法或用户自定义的匹配
  • URL 主机、路径、查询值可以使用正则表达式匹配
  • 可以使用子路由。只有当父路由匹配成功后,子路由才会进行匹配。这种嵌套的路由可以加快匹配速度。
    本文将尝试阅读mux的源代码,理解其中的设计思想并学习GO语言的编程技巧。我们打算以自顶向下的方式来开始我们的阅读。

packet使用

mux包的功能很多,下面列出几个重要的功能,下文将会尝试阅读这些主要功能实现的代码。当然mux的功能还有其他一些强大的功能(如静态文件,获取注册URL,遍历路由等),本文限于篇幅,暂不作更多阐述。

路由

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/", HomeHandler)
  r.HandleFunc("/products", ProductsHandler)
  r.HandleFunc("/articles", ArticlesHandler)
  http.Handle("/", r)
}

带变量的路由

匹配

r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

读取变量

func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
  vars := mux.Vars(r)
  w.WriteHeader(http.StatusOK)
  fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

设置筛选条件

r.Headers("X-Requested-With", "XMLHttpRequest")
r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool {
  return r.ProtoMajor == 0
})
r.HandleFunc("/products", ProductsHandler).
	Host("www.example.com").
	Methods("GET").
	Schemes("http")

子路由

r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()
s.HandleFunc("/products/", ProductsHandler)
s.HandleFunc("/products/{key}", ProductHandler)
s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

中间件

func loggingMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Do stuff here
    log.Println(r.RequestURI)
    // Call the next handler, which can be another middleware in the chain, or the final handler.
    next.ServeHTTP(w, r)
  })
}

r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)

设计思路

代码框架

代码主要部分有下面四个模块:

  • mux.go:主要函数定义的位置,如NewRoute,Match,Handle
  • route.go:定义路由相关的函数,如Subrouter等,还有一些辅助实现的类,如match,headerMatcher等
  • middleware.go:中间件相关的定义
  • regexp.go:正则处理相关的定义

类设置

在整个mux中,核心类是一个叫Router的类,而另一个同样重要,名字类似的类是Route。从名字上看来,前者是一个路由器类型,后者是某一条具体的路由。也就是说,在一个路由器中,应该会包含多个路由(在代码中,这些Route被存放在了Router中一个名叫routes的数组中)。而一条路由记录将会保存一个处理函数句柄Handler,选择正确的句柄处理到达的请求,就是路由的工作。

代码实现

类间关系

在上面的Demo中可以看见,Router在http.Handle("/", r)中,可以作为func Handle(pattern string, handler Handler)的第二个参数传入,因此,Router事实上是Handler的一个实现。注意到,Handler接口的定义

// packet: net/http
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

这个route实现了ServeHTTP(ResponseWriter, *Request)方法,因此可以作为func Handle(pattern string, handler Handler)函数的参数。

方法

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if !r.skipClean {
		path := req.URL.Path
		if r.useEncodedPath {
			path = req.URL.EscapedPath()
		}
		// Clean path to canonical form and redirect.
		if p := cleanPath(path); p != path {

			// Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query.
			// This matches with fix in go 1.2 r.c. 4 for same problem.  Go Issue:
			// http://code.google.com/p/go/issues/detail?id=5252
			url := *req.URL
			url.Path = p
			p = url.String()

			w.Header().Set("Location", p)
			w.WriteHeader(http.StatusMovedPermanently)
			return
		}
	}
	var match RouteMatch
	var handler http.Handler
	if r.Match(req, &match) {
		handler = match.Handler
		req = setVars(req, match.Vars)
		req = setCurrentRoute(req, match.Route)
	}

	if handler == nil && match.MatchErr == ErrMethodMismatch {
		handler = methodNotAllowedHandler()
	}

	if handler == nil {
		handler = http.NotFoundHandler()
	}

	if !r.KeepContext {
		defer contextClear(req)
	}

	handler.ServeHTTP(w, req)
}

我们现在先看看这个ServeHTTP的实现。里面对r.skipClean的判断是用于判断是否跳过对路径进行清洗(比如,去掉不需要的双下滑线:把“/fetch/http://xkcd.com/534/”清洗成“/fetch/http/xkcd.com/534/”),并将重定向至清洗后的链接。
接下来,是利用一个r.Match获取一个合适的匹配器RouteMatch。在经过一系列的正确性确认后,方法将调用返回的handle服务发送到的请求。
这里我们可以研究一下Match的方法:

func (r *Router) Match(req *http.Request, match *RouteMatch) bool {
	for _, route := range r.routes {
		if route.Match(req, match) {
			// Build middleware chain if no error was found
			if match.MatchErr == nil {
				for i := len(r.middlewares) - 1; i >= 0; i-- {
					match.Handler = r.middlewares[i].Middleware(match.Handler)
				}
			}
			return true
		}
	}

	if match.MatchErr == ErrMethodMismatch {
		if r.MethodNotAllowedHandler != nil {
			match.Handler = r.MethodNotAllowedHandler
			return true
		}

		return false
	}

	// Closest match for a router (includes sub-routers)
	if r.NotFoundHandler != nil {
		match.Handler = r.NotFoundHandler
		match.MatchErr = ErrNotFound
		return true
	}

	match.MatchErr = ErrNotFound
	return false
}

注意到,方法里的第一段就是Match的关键代码,程序遍历所有route,尝试找到一个能匹配当前请求的route。当确认匹配成功后,这里有一段有趣的代码,用中间件包装了处理函数:

for i := len(r.middlewares) - 1; i >= 0; i-- {
  match.Handler = r.middlewares[i].Middleware(match.Handler)
}

这里可以把这段代码理解成在handle函数上面逐层包装构造出新的handle函数的过程。

[代码评价]因为将handle和middleware组装的工作可以在注册middleware时完成,mux在收到每一个请求时都重做这个工作显得多余。除非允许中间件的动态增删,否则这种重复工作不可避免地会带来性能的损失

func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,*http.Request)) *Route

位置:mux.go

func (r *Router) HandleFunc(path string, f func(http.ResponseWriter,
*http.Request)) *Route {
	return r.NewRoute().Path(path).HandlerFunc(f)
}

HandleFunc是用于注册的函数,事实上这个函数相当简单,就是在r的routes表里添加新的一项,在设置这一项的Path以及处理函数。像定义Path这类型的函数有一整个系列:例如:

  • func (r *Route) Headers(pairs ...string) *Route:增加头部筛选条件
  • func (r *Route) Host(tpl string) *Route:增加主机筛选条件
  • func (r *Route) Methods(methods ...string) *Route:增加Methods筛选条件(“GET",“POST”,“PUT”)
  • func (r *Route) Queries(pairs ...string) *Route:增加Query参数的筛选条件

  • 注意到下面对Headers的添加采用的是同样的代码设计。这里反映的是一种组合的设计模式,同样的一个Route类型,通过注册不同类型的筛选器,可以组合出不同的筛选结果。

[代码评价]组合优于继承,组合给代码带来极大的灵活性。

这里涉及到的另外一个技巧是链式的代码操作。即我们可以把代码写成这样的形式:

r.NewRoute().Path(path).HandlerFunc(f)

而不是

ro = r.NewRoute()
ro.Path(path)
ro.HandlerFunc(f)

前者显然比后者有着更简洁优雅的形式。我们之所以可以把代码写成这样的链式形式,是因为Path,HandleFunc等函数都返回了它要设置的对象本身。对需要一系列设置的对象,这种链式的设置使得代码更简洁易用。

func (r *Router) Headers(pairs …string) *Route

位置:mux.go

func (r *Router) Headers(pairs ...string) *Route {
	return r.NewRoute().Headers(pairs...)
}

这其实就是一种需要组合的设置,此处不多阐述。

func (r *Route) Subrouter() *Router

位置:route.go

func (r *Route) Subrouter() *Router {
	router := &Router{parent: r, strictSlash: r.strictSlash}
	r.addMatcher(router)
	return router
}

这里就是简单地创建一个新的子路由,挂载到父路由上面去的过程。我们好奇的问题是,下面这个功能是如何实现的:只有在父路由成功匹配后,才会转发给对应的子路由继续匹配?查看addMatcher的代码

func (r *Route) addMatcher(m matcher) *Route {
	if r.err == nil {
		r.matchers = append(r.matchers, m)
	}
	return r
}

有意思的东西出现了,addMatcher接受的是一个matcher参数,我们传入的却是一个router。找到matcher的定义:

type matcher interface {
	Match(*http.Request, *RouteMatch) bool
}

这就非常清楚了:Router实现了一个Match方法,因此可以作为一个matcher对象加入到matchers中。回过头看一下整个运作过程

r := mux.NewRouter()
s := r.Host("www.example.com").Subrouter()
s.HandleFunc("/products/", ProductsHandler)

当有一条请求"www.example.com/products/“到达的时候,r的ServeHTTP会找到Host注册的一个Route,Route在调用自己的Match方法时会遍历自己的matchers表,发现了一个可以处理”/products/"的子路由,并递归式地调用Match方法,直到找到了对应的Handle。

func (r *Router) Use(mwf …MiddlewareFunc)

位置:middleware.go

func (r *Router) Use(mwf ...MiddlewareFunc) {
	for	_, fn	:=	range mwf {
		r.middlewares	=	append(r.middlewares, fn)
	}
}

前面提到,这里注册中间件时,只是简单地把所有中间件放到一个数组里,直到使用的时候才对其进行组装。因此这里没有什么特别的东西。由于中间件只会增加不会取走,因此在注册时完成组装将是更好的选择。

与DefaultServeMux对比

所有路由器的不同都在于他们的ServeHTTP不同,DefaultServeMuxServeHTTP函数如下:

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

这里是从mux里面直接获得了处理函数,并交由函数处理,并没有作其他太多工作了。

 类似资料: