乐趣区

关于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 办法,来解决申请
退出移动版