github.com/qiniu/http 分析

0 前言

最近发现七牛开源了自己的 http 模块 github.com/qiniu/http,所以拉下来学习一下。
相对于其他语言的 http server 框架,例如 Node.js 的 Express 和 Python 的 Flask,Go 作为网络C语言提供的 net/http 模块原生支持 http server,因为有协程加持,所以不用太关心性能问题,我们把主要关注点放到怎么实现与业务的适配上。

1 net/http

在分析 github.com/qiniu/http 前,我们先做一下准备工作,了解以下 Go 的 net/http 模块的使用方法。

基于HTTP构建的网络应用包括两个端,即 Client 和 Server。两个端的交互行为包括从客户端发出 Request、服务端接受 Request 进行处理并返回 Response 以及客户端处理 Response。所以 http server 的工作就在于如何接受来自客户端的 Request,并向客户端返回 Response。

典型的http服务端的处理流程可以用下图表示:

github.com/qiniu/http 分析_第1张图片
服务器在接收到请求时,首先会进入 Router,这是一个 Multiplexer (简称 Mux),Router 的工作在于为每个 Request 找到对应的 Handler,Handler 对 Request 进行处理,并构建Response。Golang 实现的 http server 同样遵循这样的处理流程。

1.1 http server v1

我们来通过实例逐步分析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 中。

1.2 http server v2

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"))
}

1.3 http server v3

通过 v2 我们发现 http.NewServeMux() 所创建的 mux,主要是为了提供 mux.Handlemux.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"))
	}
}

2 github.com/qiniu/http

通过上面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 源码也是一次不错的体验。


参考资料

  1. 七牛如何做HTTP服务测试
  2. Golang开启http服务的三种方式
  3. https://github.com/speedwheel/awesome-go-web-frameworks
  4. 6 款最棒的 Go 语言 Web 框架简介
  5. 深入理解Golang之http server
  6. Understanding RPC Vs REST For HTTP APIs
  7. golang http server 源码解析与说明
  8. 专业 Golang HTTP 服务器

你可能感兴趣的:(Become,a,Gopher,qiniu,qiniu/http,net/http,golang)