关于golang:Golang-源码阅读-nethttp-与-mux

前言

在《服务计算》的第一堂课上,潘老师就强调:golang是为服务而生的语言。现在最风行的服务莫过于 http 服务,而golang官网也用其极其简洁的写法和优良的服务个性(如高并发)向开发者们证实了这一点。这篇博客正是对于不应用第三方库,仅应用官网提供的程序包: net/http, 搭建http服务的原理,即背地的源码和逻辑的剖析。同时,我也会简要的剖析一个很罕用的库 mux 的实现。

从简略的 Http Server 开始

golang 运行一个 http server 非常简单,须要这样几个局部:

  • 申明定义若干个 handler(w http.ResponseWriter, r *http.Request), 每个 handler 也就是服务端提供的每个服务的逻辑载体。
  • 调用 http.HandleFunc 来注册 handler 到对应的路由下。
  • 调用 http.ListenAndServe 来启动服务,监听某个指定的端口。

依照下面的三个步骤,我实现了一个最简略的 http server demo, 如下:

package main

import (
   "fmt"
   "net/http"
 )
func helloHandler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello world")
}
func main(){
   http.HandleFunc("/", helloHandler)
   http.ListenAndServe(":3000", nil)
}

当咱们运行这段代码, 并且用浏览器拜访http://localhost:3000/ 时,就能如愿看到 helloHandler 中写入的 “Hello world”. 每当一个申请达到咱们搭建的http server后,客户端定义的申请体和申请参数是如何被解析的呢?解析之后又是如何找到helloHandler呢?咱们来一步步摸索 ListenAndServe 函数以及 HandleFunc 函数。

http.ListenAndServe 的工作机制

依据源码,ListenAndServe 要做两个工作:

  • 通过 Listen 函数建设对于本地网络端口的 Listener
  • 调用 Server 构造体的 Serve 函数来监听

对于 Listen 函数的实现和 Listener 的定义本篇博客并不探讨,咱们重点来看 Serve 函数的实现。

// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error {
   // ...
   // 这里节选了比拟要害的,与申请相干的实现
   for {
   // step1. 通过listener, 承受了一个申请
      rw, err := l.Accept()
      if err != nil {
         select {
         case <-srv.getDoneChan():
            return ErrServerClosed
         default:
         }
         if ne, ok := err.(net.Error); ok && ne.Temporary() {
            if tempDelay == 0 {
               tempDelay = 5 * time.Millisecond
 } else {
               tempDelay *= 2
 }
            if max := 1 * time.Second; tempDelay > max {
               tempDelay = max
            }
            srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
            time.Sleep(tempDelay)
            continue
 }
         return err
      }
      connCtx := ctx
      if cc := srv.ConnContext; cc != nil {
         connCtx = cc(connCtx, rw)
         if connCtx == nil {
            panic("ConnContext returned nil")
         }
      }
      tempDelay = 0
 // step2. 确认申请未超时之后,创立一个conn 对象
 c := srv.newConn(rw)
      c.setState(c.rwc, StateNew) // before Serve can return
      
 // step3. 独自创立一个gorouting, 负责解决这个申请
 go c.serve(connCtx)
   }
}

通过代码,Serve 的次要工作就是从listener中接管到申请,依据申请创立一个conn, 随后独自发动一个gorouting来进一步解决,解析,响应该申请。依据conn构造体的serve办法,负责解析 request 的函数是func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error), 这个函数将申请头和申请体中的字段放到 Request 构造体中并返回。
以上就是 ListenAndServe 的工作机理。

http.HandleFunc 的工作机制

每个http server都有一个ServerMux构造体类型的实例,该实例负责将申请依据定义好的pattern来将申请转发到对应的 handlerFunc 来解决。ServerMux 的构造体定义如下:

type ServeMux struct {
 mu    sync.RWMutex                 // 锁,负责解决并发
 m     map[string]muxEntry          // 由路由规定到handler的映射构造
 es    []muxEntry // slice of entries sorted from longest to shortest.
 hosts bool // whether any patterns contain hostnames
}

muxEntry的定义如下:

type muxEntry struct {
   h       Handler  // h 是用户定义的handler函数
   pattern string   // pattern 是路由匹配规定
}

Handler 类型是一个 interface 类型,只须要实现 func(w http.ResponseWriter, r *http.Request), 即:

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

当咱们传入 pattern: "/" handlerFunc: helloHandler 后,DefaultMux 调用 HandleFunc, HandleFunc 调用 handle负责将咱们定义的 pattern 和 handlerFunc 转换为 muxEntry, 代码实现如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
   mux.mu.Lock()
   
   // 对于输出的 pattern 和 handler 进行校验
   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)
   }
   
   // 初始化 muxEntry 和模式的映射
   if mux.m == nil {
      mux.m = make(map[string]muxEntry)
   }
   
   // 初始化 muxEntry
   e := muxEntry{h: handler, pattern: pattern}
   mux.m[pattern] = e
   if pattern[len(pattern)-1] == '/' {
      mux.es = appendSorted(mux.es, e)
   }
   if pattern[0] != '/' {
      mux.hosts = true
 }
}

能够看到,对于注册的handler, 传入 ServerMux 之后须要首先进行输出校验:pattern 和 handler 函数皆不能为空,同时不能反复注册同一个 pattern 对应的多个handler函数;实现校验当前,初始化 muxEntry 项,随后依据 pattern 传入 handler 即可。
以上就是注册门路与handler的过程。

http.ListenAndServe 与 http.HandleFunc 的耦合

介绍了申请的解析和handler的注册之后,解析后的 request 是怎么寻找到相应的 handler的呢?依据源码,这一过程通过 ServeMux 的办法:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
来实现。能够看到,这个函数依据解析后的申请 rmux 中寻找, 返回对应的 Handlerpattern. 这一机制的实现如下:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
   // CONNECT requests are not canonicalized.
   // 如果该申请未 CONNECT 办法的申请,则须要额定解决
 if r.Method == "CONNECT" {
      // If r.URL.Path is /tree and its handler is not registered,
 // the /tree -> /tree/ redirect applies to CONNECT requests // but the path canonicalization does not. 
 if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
         return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
      }
      return mux.handler(r.Host, r.URL.Path)
   }
   // All other requests have any port stripped and path cleaned
 // before passing to mux.handler. host := stripHostPort(r.Host)
 // 首先对于原有申请门路进行截取解决
   path := cleanPath(r.URL.Path)
   // If the given path is /tree and its handler is not registered,
 // redirect for /tree/. 
 if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
      return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
   }
   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)
}

能够看到,Handler 的作用是对于申请门路做解决,如果解决之后与申请中的门路不匹配则会间接返回状态
StatusMovedpermanently. 当通过上述验证后会进入 mux.handler 函数。匹配的次要逻辑都写在 handler 函数中,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
}

利用 —- HTTP两头键

读过了源代码之后,咱们能够利用golang中http server的工作个性开发更多古代服务端开发中罕用的组件。在 Matrix 开发团队进行服务端开发的过程中,用到了nodejs的koa框架,这个框架的显著特点就是轻量,并且很不便的应用 中间件 的个性。这里咱们也能够定义golang http开发中的中间件。
要实现两头键,咱们须要满足以下两个个性:

  • 两头键须要定义和应用于 解析申请最终的handler之间
  • 两头键须要可能互相嵌套

给予咱们上述对于golang http 服务的原理探讨,咱们晓得:http.HandleFunc 可能将特定的 handler 绑定在某个或者某一类 URL 上,它承受两个参数,一个参数是pattern, string 类型,另一个参数是一个函数,http.Handler 类型。不难想到,要实现两头键,咱们只须要实现一个函数签名如下的函数作为两头键:

func (http.Handler) http.Handler

其中,任何实现了func(ResponseWriter, *Request) 这个函数签名的函数都是一个 http.Handler 类型的变量,所以咱们的中间件既可能作为接管handler为参数从而在解析申请和传入的handler中实现,又可能嵌套多个两头键,造成调用链。
例如:

package main
import (
 "fmt"
 "log" 
 "net/http"
)

func middleware1(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      log.Println("middleware1")
      next.ServeHTTP(w, r)
   })
}
func middleware2(next http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      log.Println("middleware2")
      next.ServeHTTP(w, r)
   })
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello world")
}
func main(){
   mux := http.NewServeMux()
   finalHandler := http.HandlerFunc(helloHandler)
   mux.Handle("/", middleware1(middleware2(finalHandler)))
   err := http.ListenAndServe(":3000", mux)
   log.Println(err)
}

拓展 —- github.com/gorilla/mux 包简要解析

基于咱们下面的解析能够看到,应用net/http 注册handler, 搭建简略的 http server 并不简单,只须要将每个特定的 pattern 映射到特定的 handler 即可。然而在设计api的过程中,pattern 并不是一个固定的字符串,而是须要匹配一系列具备雷同模式的url(e.g. /api/users/:user_id, 所有合乎这个模式的url, 比方 /api/users/1, /api/users/2, 都须要应用雷同的控制器来解决)。第三方程序包 mux为这个个性提供了良好的反对。
相比于 net/http 包中的多路复用器的 HandlerFunc, mux 的 多路复用器HandlerFunc 进行了一些拓展,首先多路服务器的数据结构定义如下:

type Router struct {
   // Configurable Handler to be used when no route matches.
 NotFoundHandler http.Handler
 // Configurable Handler to be used when the request method does not match the route.
 MethodNotAllowedHandler http.Handler
 // Routes to be matched, in order.
 routes []*Route
 // Routes by name for URL building.
 namedRoutes map[string]*Route
 // If true, do not clear the request context after handling the request.
 // // Deprecated: No effect, since the context is stored on the request itself. KeepContext bool
 // Slice of middlewares to be called after a match is found
 middlewares []middleware
 // configuration shared with `Route`
 routeConf
}

相比 ServeMux, Router 构造减少了中间件成员 middleware 同时新定义了 Route 构造对于 muxEntry 做了拓展,如下:

type Route struct {
 // Request handler for the route.
 // 这里继承了 Handler 接口,所以Route能够作为 http.Handler 类型的参数
 handler http.Handler
 // If true, this route never matches: it is only used to build URLs.
 buildOnly bool
 // The name used to build URLs.
 name string
 // Error resulted from building a route.
 err error
 // "global" reference to all named routes
 // 减少了一个 namedRoutes 成员,从而可能反对嵌套路由
 namedRoutes map[string]*Route
 // config possibly passed in from `Router`
 routeConf
}

下面剖析申请与handler的耦合时,咱们提到了 ServeHTTP 函数,任何实现了这个函数的接口都是一个 http.Handler 类型变量。通过这种设计,第三方的库,比方mux能够很容易的与调用http.ListenAndServe 的服务端进行对接,解决申请。mux 最大的劣势是反对 url 的模式匹配的逻辑实现在 Match 函数,在 ServeHTTP 函数中调用 Match 函数即可依据申请的url进行特定模式的handler寻找。通过浏览源码,net/http库中的 DefaultMux的 ServeHTTP 实现与 mux 中的 Router 的ServeHTTP 实现基本一致,仅更换了 Match 函数,这也进一步印证了这种设计模式。Match 的实现如下:

func (r *Route) Match(req *http.Request, match *RouteMatch) bool {
   if r.buildOnly || r.err != nil {
      return false
 }
   var matchErr error
 // 扫描所遇的 handler, 封装在matchers中,查看是否有匹配
 for _, m := range r.matchers {
      if matched := m.Match(req, match); !matched {
         if _, ok := m.(methodMatcher); ok {
            matchErr = ErrMethodMismatch
            continue
 }
         // Ignore ErrNotFound errors. These errors arise from match call
 // to Subrouters. // // This prevents subsequent matching subrouters from failing to // run middleware. If not ignored, the middleware would see a // non-nil MatchErr and be skipped, even when there was a // matching route. 
 if match.MatchErr == ErrNotFound {
            match.MatchErr = nil
 }
         matchErr = nil
 return false }
   }
   if matchErr != nil {
      match.MatchErr = matchErr
      return false
 }
   if match.MatchErr == ErrMethodMismatch && r.handler != nil {
      // We found a route which matches request method, clear MatchErr
 match.MatchErr = nil
 // Then override the mis-matched handler
 match.Handler = r.handler
   }
   // Yay, we have a match. Let's collect some info about it.
 if match.Route == nil {
      match.Route = r
 }
   if match.Handler == nil {
      match.Handler = r.handler
   }
   if match.Vars == nil {
      match.Vars = make(map[string]string)
   }
   // Set variables.
 r.regexp.setMatch(req, match, r)
   return true
}

至于 mux 如何设计正则表达式来匹配模式,这里咱们不深刻探讨。

总结

通过浏览 net/httpgithub.com/gorilla/mux 的实现,我根本了解了 golang 下的 http server 建设,申请解决,和申请路由,一些要点总结如下:

  • http.ListenAndServe 实现以下几个工作:

    • 建设端口的监听
    • 解析发送来的申请
    • 保留 Mux/Router, 以便路由操作
  • http.ServeMux 实现以下几个工作:

    • 寄存pattern和handler
    • 建设从pattern到handler的映射
    • 须要实现 handler 办法来寻找到每个申请 url 对应的handler
  • http.Handlerinterface的作用:

    • 要求每个该类型的变量实现 ServeHTTP 办法,来解决申请

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理