1

前言

在《服务计算》的第一堂课上,潘老师就强调: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

其中,任何实现了ServeHTTP(ResponseWriter, *Request) 这个函数的变量都是一个 http.Handler 类型的变量,所以我们的中间件既能够作为接收handler为参数从而在解析请求和传入的handler中实现,又能够嵌套多个中间键,形成调用链。
可是当我们定义了一个 func(ResponseWriter, *Request) 签名的函数,直接在中间件中返回会遇到类型不匹配的报错。这里就涉及到一个 golang 类型转换的技巧。net/http 包下的 HandlerFunc 是这样定义的:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

首先,Handlerfunc 是一个类型,每一个类型都可以被定义其方法,这里,HandlerFunc 类型下 ServeHTTP 方法已经被实现好。而当我们使用 golang 中的类型转换语法,即 Type(val), 我们可以通过 HandlerFunc(function_define_by_ourselves)来将我们定义的函数转换为 HandlerFunc 类型,又因为 HandlerFunc 实现了 Handler 这个 Interface 要求的 ServeHTTP 函数,因此我们只需要在中间件中返回 return HandlerFunc(func(ResponseWriter, *Request)),就可以巧妙的完成类型转换。

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.Handler interface的作用:

    • 要求每个该类型的变量实现 ServeHTTP 方法,来处理请求
    • 每个签名为 func(ResponseWriter, *Request) 的函数都可以通过 HandlerFunc() 来转换成 http.Handler 类型
  • 第三方包对于 net/http 的拓展实现特点:

    • 对于 ServeMux 的封装,比如 mux 中的 Router 需要确保每个 Route 都实现 ServeHTTP 函数,这样才能对接到 http.ListenAndServe

BOBBAIcl
12 声望2 粉丝

bobbai