前言
在《服务计算》的第一堂课上,潘老师就强调: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
方法,来处理请求
- 要求每个该类型的变量实现