前言
在《服务计算》的第一堂课上,潘老师就强调: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)
来实现。能够看到,这个函数依据解析后的申请 r
在 mux
中寻找, 返回对应的 Handler
和 pattern
. 这一机制的实现如下:
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/http
与 github.com/gorilla/mux
的实现,我根本了解了 golang 下的 http server 建设,申请解决,和申请路由,一些要点总结如下:
-
http.ListenAndServe
实现以下几个工作:- 建设端口的监听
- 解析发送来的申请
- 保留 Mux/Router, 以便路由操作
-
http.ServeMux
实现以下几个工作:- 寄存 pattern 和 handler
- 建设从 pattern 到 handler 的映射
- 须要实现
handler
办法来寻找到每个申请 url 对应的 handler
-
http.Handler
interface 的作用:- 要求每个该类型的变量实现
ServeHTTP
办法,来解决申请
- 要求每个该类型的变量实现