最近发现七牛开源了自己的 http 模块 github.com/qiniu/http,所以拉下来学习一下。
相对于其他语言的 http server 框架,例如 Node.js 的 Express 和 Python 的 Flask,Go 作为网络C语言提供的 net/http 模块原生支持 http server,因为有协程加持,所以不用太关心性能问题,我们把主要关注点放到怎么实现与业务的适配上。
在分析 github.com/qiniu/http 前,我们先做一下准备工作,了解以下 Go 的 net/http 模块的使用方法。
基于HTTP构建的网络应用包括两个端,即 Client 和 Server。两个端的交互行为包括从客户端发出 Request、服务端接受 Request 进行处理并返回 Response 以及客户端处理 Response。所以 http server 的工作就在于如何接受来自客户端的 Request,并向客户端返回 Response。
典型的http服务端的处理流程可以用下图表示:
服务器在接收到请求时,首先会进入 Router,这是一个 Multiplexer (简称 Mux),Router 的工作在于为每个 Request 找到对应的 Handler,Handler 对 Request 进行处理,并构建Response。Golang 实现的 http server 同样遵循这样的处理流程。
我们来通过实例逐步分析net/http 模块的使用方式。首先实现一个最简单的 v1 版本:
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("httpserver v1"))
})
http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo"))
})
log.Println("Starting httpserver v1 ...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
v1 版本中 func ListenAndServe(addr string, handler Handler) error
中的第二个参数为 nil
,即我们没有传入 Router,这个服务任然可以正常工作,这是因为在 net/http 内部有一个默认的 Router,当调用 http.HandleFunc()
或 http.Handle()
时会将参数作为规则注册到默认的 Router 中。
v2 版本中我们不使用默认的 Router,而是通过 http.NewServerMux()
生成一个新的 Router ,然后注册到 http server 中。
func main() {
mux := http.NewServeMux()
mux.Handle("/", &myHandler1{})
mux.Handle("/foo", &myHandler2{})
log.Println("Starting httpserver v2")
log.Fatal(http.ListenAndServe(":8080", mux))
}
type myHandler1 struct{}
func (*myHandler1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("httpserver v2"))
}
type myHandler2 struct{}
func (*myHandler2) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("foo"))
}
通过 v2 我们发现 http.NewServeMux()
所创建的 mux
,主要是为了提供 mux.Handle
和 mux.HandleFunc
等方法方便我们 Append 路由规则到 Router 中,所以我们进一步分析,可以自己实现一个 Router 来处理。首先分析 func ListenAndServe(addr string, handler Handler) error
中的第二个参数 Handler
,通过源码发现它是一个 interface,只要实现了 ServerHTTP()
方法即可,根据 Duck typing 原则,我们可以自己实现一个 Router。
func main() {
mux := new(myHandler)
log.Println("Starting httpserver v3")
log.Fatal(http.ListenAndServe(":8080", mux))
}
type myHandler struct{}
func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/foo":
w.Write([]byte("foo"))
default:
w.Write([]byte("httpserver v3"))
}
}
通过上面3个实例,可以总结出使用 Golang 创建一个 http server 比较关键的步骤就是注册路由,即提供 URL 模式和 Handler 函数的映射,也就是直接或间接的实现一个 Router 将 URL 和 业务处理关联起来。Go 提供的 ServerMux
提供的功能比较有限,对于正则匹配等高阶的功能还需要使用第三方库来实现,例如 github.com/gorilla/mux。
github.com/qiniu/http
大概也是这个思路,自己实现了一个 Router,来匹配 RESTful API 并形成业务规则。下面是一个使用实例
package main
import (
"github.com/qiniu/http/restrpc"
)
// Service is a business prototype
type Service struct{}
// GetFoo method
func (*Service) GetFoo(env *restrpc.Env) (s string, err error) {
s = "foo"
return
}
func main() {
svr := new(Service)
router := restrpc.Router{}
router.ListenAndServe(":8080", svr)
}
上面这个实例运行起来后浏览器输入 http://localhost:8080/foo 即可看到 GetFoo()
函数的返回值,有木有觉得很神奇,github/qiniu/http 中一定存在了某种映射关系,可以将以指定规则命名的函数与对应 URL 和 http 方法形成映射关系,查看源码在 restful_rpc.go 中发现了一段注释,可以解释上面的疑问。
/* ---------------------------------------------------------------------------
1. 参数规格
注意:以下的 [Arguments] 和 [Return-Info] 与 wsrpc 中的说明一致。
allow POST:
/foo func (rcvr *XXXX) PostFoo([Arguments])([Return-Info])
/foo/ func (rcvr *XXXX) PostFoo_([Arguments])([Return-Info])
/foo/bar func (rcvr *XXXX) PostFooBar([Arguments])([Return-Info])
/foo/bar/ func (rcvr *XXXX) PostFooBar_([Arguments])([Return-Info])
/foo//bar func (rcvr *XXXX) PostFoo_Bar([Arguments])([Return-Info])
/foo//bar/ func (rcvr *XXXX) PostFoo_Bar_([Arguments])([Return-Info])
其余的 Method(GET/PUT/DELETE) 均只需要将函数名前的前缀从 Get 改为 Get/Put/Delete 既可。
2. 参数解析
这里以 PostFoo_Bar_ 为例:
type Args struct {
FormParam1 string `json:"form_param1"`
FormParam2 string `json:"form_param2"`
}
func (rcvr *XXXX) PostFoo_Bar_(args *Args, env *rpcutil.Env) {
...
}
如果请求为:
POST /foo/COMMAND1/bar/COMMAND2
Content-Type: application/x-www-form-urlencoded
form_param1=FORM_PARAM1&form_param2=FORM_PARAM2
那么解析出来的 args 为:
args = &Args{
FormParam1: "FORM_PARAM1",
FormParam2: "FORM_PARAM2",
}
// -------------------------------------------------------------------------*/
这种映射是怎么形成的呢,下面我们来具体分析一下。首先查看一下 func (r *Router) ListenAndServe(addr string, rcvr interface{}) error
函数的实现:
func (r *Router) ListenAndServe(addr string, rcvr interface{}) error {
return http.ListenAndServe(addr, r.Register(rcvr))
}
可以看出这个函数将第二个参数进行 func (r *Router) Register(rcvr interface{}, routes ...[][2]string) Mux
加工后,直接调用了 http.ListenAndServe()
,也就是说 Register
函数返回了一个可以被 http.ListenAndServe()
直接使用的 Handler
,这个不难理解,我们继续分析 Register
函数:
func (r *Router) Register(rcvr interface{}, routes ...[][2]string) Mux {
if r.Mux == nil {
r.Mux = NewServeMux()
}
if r.Default != nil {
r.Mux.SetDefault(r.Default)
}
mux := r.Mux
factory := r.Factory
if factory == nil {
factory = Factory
}
typ := reflect.TypeOf(rcvr)
rcvr1 := reflect.ValueOf(rcvr)
if len(routes) == 0 {
patternPrefix := r.PatternPrefix
if strings.HasPrefix(patternPrefix, "/") {
patternPrefix = patternPrefix[1:]
}
sep := r.Separator
if sep == "" {
sep = "_"
}
// Install the methods
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
prefix, handler, err := factory.Create(rcvr1, method)
if err != nil {
continue
}
pattern := []string{prefix}
if patternPrefix != "" {
pattern = append(pattern, patternPrefix)
}
pattern = append(pattern, patternOf(method.Name[len(prefix):], sep)...)
mux.Handle(strings.Join(pattern, "/"), handler)
log.Println("Install", pattern, "=>", method.Name)
}
} else {
for _, item := range routes[0] {
pattern := item[0]
if r.PatternPrefix != "" {
pos := strings.Index(pattern, "/")
if pos > 0 {
pattern = pattern[:pos] + r.PatternPrefix + pattern[pos:]
}
}
method, ok := typ.MethodByName(item[1])
if !ok {
log.Fatalln("Install", pattern, "=>", item[1], "failed: method not found!")
}
_, handler, err := factory.Create(rcvr1, method)
if err != nil {
log.Fatalln("Install", pattern, "=>", item[1], "failed:", err)
}
mux.Handle(pattern, handler)
log.Println("Install", pattern, "=>", item[1])
}
}
return mux
}
这个函数是整个库的核心,充分的利用了 Golang Reflect 的灵活性,将需要注册的结构所实现的方法进行分析和映射,代替了常规 Mux 繁琐的注册过程。
对于快速实现一个 RESTful 网络服务,使用 github.com/qiniu/http 无疑可以大大的提升开发效率,再配合 https://github.com/qiniu/httptest 可以高效的完成常规的开发任务。对于想要加深对 Golang 使用功力的同学,阅读 github/qiniu/http 源码也是一次不错的体验。