刚毕业那会儿,从合租大牛的书架上看到一本《Socket 套接字编程》,开始对网络编程有一些了解。后来又和同学一起做过一个电话手表的项目,大概功能是电话手表给 Server 端程序发送心跳包,Server 端会给手表发一些指令,也算实践了一下。再后来移动互联网兴起,投入 iOS 开发大潮,这么多年做的项目与服务端开发基本八竿子打不着。

一. 学习 Gin 框架背景原因

我所在的团队虽然是客户端,由于团队规模和业务规模比较大,需要一个管理研发流程的平台,这个平台麻雀虽小五脏俱全,整个流程客户端、持续集成、服务端、前端都有涉及。而由于服务端主力开发转岗,所以很多技术方案的决策就需要我参与,了解下就非常有必要。

服务端项目用的 Go 语言开发,开发框架用的是公司内部的开源方案,经过 3 人 1 个季度的开发,项目已经有一些依赖,引入了很多框架,想调试、运行、或了解一下框架的运行机制有一定成本。因此在看完两遍 Go 语言的基础知识后,找了业内比较流行 Web 框架 Gin 研究一下,这些框架都是解决同样的问题,它们之间也肯定有很多共性。

二. 带着问题学习:Gin 请求处理的入口在哪里?

Gin 框架确实简练,短短几行代码就可以创建响应客户端请求的代码。

func main() {
	g := gin.Default()
	
	g.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello Wrold!")
	})

	g.Run(":" + "8088")
}

1. 思路起点

简练代码背后,是框架对复杂度的封装。看到这样的代码,我想到的第一个问题是:"Gin 如何处理请求的分发?",由于之前看过《Socket 套接字编程》,自然想到如果自己从头开始写一个这样的服务端处理程序应该怎么写?Gin 框架源码应该从哪里开始看?

服务端和客户端通信模型,图片来自segmentfault

有服务端和客户端通信模型,我们可以看到,服务端处理请求都是在 listen() 这里开启监听。因此首先要找到 listen() 方法,另外从软件开发的分层模型来理解,把应用框架系统框架 的实现区分开,有利于把知识分解分块学习,不关注的一部分暂且当做黑盒子,了解层与层之间如何交互即可。

下面我们来看看示例中 mian 方法调用的几个子方法原型:

func Default() *Engine

gin.Default() 返回一个 *Engine 类型。

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

原型接收对象为 *RouterGroup 的 GET 方法。注意示例里它用的 Default() 方法的返回值去接收的,而Default() 返回值 是 *Engine 类型。因此 *Engine 类型也是 *RouterGroup 类型,这个后面我们结合源码说明 Gin 框架是怎么做到的。

func (engine *Engine) Run(addr ...string) (err error)

Run 方法的接收对象 是 *Engine 类型

2. 从 Run 方法说起 - 寻找请求处理入口

源码:

func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	trustedCIDRs, err := engine.prepareTrustedCIDRs()
	if err != nil {
		return err
	}
	engine.trustedCIDRs = trustedCIDRs
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}

这里面的关键方法是 http.ListenAndServe 方法,它属于 net/http 包里的一个方法,也就是我们前面说的 系统框架 的部分。这个方法就是 Gin 应用框架 和 Go 系统框架 的交互接口。Gin 向 http.ListenAndServe 方法传入了 地址 和 engine 两个参数(地址不是我们关注的重点,先忽略)。我们来看看 http.ListenAndServe 的方法原形:

func ListenAndServe(addr string, handler Handler) error

可以看到 ListenAndServe 要求传入的第二个参数类型为 Handler,而 Gin 传入的是 *Engine,它们之间是什么关系了,我们来看看 Handler 的原型:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

我们可以看到 Handler 是一个接口类型,Go 的接口类型关系比较松散,基本上可以理解是靠 同名方法 维系类型和接口间的关系,这样的好处是可以减少编译时依赖。由此我们知道 应该有一个接收类型为 *EngineServeHTTP 方法。搜索源码印证了这个推断:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

至此,我们找到了 Gin 中请求处理的入口。每个请求的处理都会 走 engine.handleHTTPRequest 方法。原型如下:

func (engine *Engine) handleHTTPRequest(c *Context)

3. GET 方法:RouterGroup

前文我们了解到 GET 方法原型中,接收对象是 *RouterGroup, 而示例中 GET 方法接收对象却是 *Engine,看到 Engine 类型的定义解决了这一疑惑:

type Engine struct {
	RouterGroup
        ....
}

type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

这是一个结构体嵌套的用法,Engine 类型头部是一个 RouterGroup类型,因此Engine 类型在某些情况下可以当做 RouterGroup 来用,RouterGroup 里也有 *Engine 类型,所以它两是互相引用,互相查找。以下是 GET 方法的实现:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

routergroup.go
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	assert1(path[0] == '/', "path must begin with '/'")
	assert1(method != "", "HTTP method can not be empty")
	assert1(len(handlers) > 0, "there must be at least one handler")

	debugPrintRoute(method, path, handlers)

	root := engine.trees.get(method)
	if root == nil {
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)

	// Update maxParams
	if paramsCount := countParams(path); paramsCount > engine.maxParams {
		engine.maxParams = paramsCount
	}
}

gin.go

经过这层层调用,结合 handleHTTPRequest 的处理过程,最终我们知道所有的请求都会被加到 engine.trees 上。有时间我们再来分析这一部分。

很久没读框架源码,没想到这次居然读了一个服务端框架。主动思考、主动分析的过程,感觉有收获。内容虽少,也是管中窥豹吧。

2021 年 9 月 10日(周五),团建下班后,于 19:00~21:16 完成。