Go第三方框架--gin框架(三)

5. net/http框架源码-- 多路复用的实现

这块核心功能对应 1.3 的圆圈2,所属代码如下图:
在这里插入图片描述
run代码涉及的操作不是gin框架的核心,还记的我说过gin是在net/http的基础上操作的吗,我们来看下gin和net/http包的关联关系。
gin: 主要建立engine ,生成http方法树和对方法树的查找。剩下的采用多路复用实现的连接等操作都是使用的net/http。怎么复用?我们已经说过了,engine实现了 handler的 ServerHTTP方法。具体见1

好了梳理了大部分流程,我们再在来梳理下Run()方法。其代码如下

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

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}
	// 解析 地址
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)

	// 这里引入 net/http包 直接调用它的方法
	err = http.ListenAndServe(address, engine.Handler())
	return
}

我们看到这里直接调用了 net/http包的方法。
再往下讲述之前我们先来梳理下net包服务器客户端连接的要求:

5.1. 一个服务器对应多个客户端,且新的客户端链接不知道啥时候来,所以需要异步等待 ----windows:IOCP模型,linux:epoll模型
5.1.1. 首先建立套接字,将server_ip:port跟套接字绑定,然后封装成一个tcp的监听器
5.1.2. 当客户端开始连接时,根据服务器监听器,启动一个协程异步阻塞监听端口(IOCP或者epoll, 新建一个tcp监听器 这个tcp连接有 套接字信息 server_ip:port+c;client_ip:port

5.1.1的调用链如下
我们来看下具体代码
在这里插入图片描述
这里忽略了 调用的 结构体 只列出涉及的函数 感兴趣的可以自己追踪下
重点介绍两个节点

socket: socket(…) 函数创建 套接字 并将套接字注册到新创建的文件描述符中

fd.pd.init: (调用链最后一个函数)将文件描述符注册到监听事件中

func (pd *pollDesc) init(fd *FD) error {
	serverInit.Do(runtime_pollServerInit)  //初始化 Go 语言运行时的网络轮询服务器. 对应 epoll_create(如果是linux) 建立红黑树
	ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd)) // 打开一个文件描述符 对应 epoll_ctl 可以对红黑树进行增删操作
	if errno != 0 {
		return errnoErr(syscall.Errno(errno))
	}
	pd.runtimeCtx = ctx
	return nil
}

这里只是简要介绍感兴趣的可以自己追踪研究下,到这里套接字被层层包装 到了 tcp监听器中。

好了到这里 服务器的tcp监听器就建立了。

我们来梳理下 套接字的 嵌套流程

套接字(socket)—>文件描述符(fd:这里有对特定内存的读写,请求和响应都这这里)---->tcp连接(只有
seriver_ip:port)

5.1.2 的调用链为:
在这里插入图片描述
调用完后会生成一个新的 tcp连接 包含服务器客户端的ip,然后启动一个协程来开始处理这个tcp连接,这时tcp握手完成,这个新的协程就开始执行新客户端发来的请求了,大致是这样。
srv.Serve函数结构如下:

func (srv *Server) Serve(l net.Listener) error {
		// ...

	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		// 生成一个新的tcp链接;调用 runtime_pollWait (epoll) ;这里阻塞,等待新的客户端来建立tcp连接
		rw, err := l.Accept()
		 // ... 
		 // 启动新的协程 来处理这个tcp连接 这时 进入5.2
		go c.serve(connCtx)
	}
}
5.2. 一旦某个信道(server_ip:port+c;ient_ip:port)建立,就可以不断从对应的套接字进行接收和发送数据 ---- 套接字介入(套接字其实就是可以操作某特定内存的句柄,特定内存在这里一般指request和reponse请求需要使用的一对 buf)

上面 代码中 go c.serve(connCtx) 对应的5.2的主要功能,我们来简要梳理下:

func (c *conn) serve(ctx context.Context) {
	// ...
	
	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)  // 这里将 网络tcp连接(当然也包括对应的套接字)跟bufr对接 使得网络流可以输入(request请求)到buf中,也可以向buf中写(response响应) 这样整个链条就通了
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) //向buf中写(response响应) 

	for {
		w, err := c.readRequest(ctx)  //采用bufer.ReadLine 方法来同步阻塞 等待某个特定客户端的请求(例如在网页调用某个网址加url),从bufr中获取数据 (request请求),w是响应头 这个w包装了 c(完整的tcp连接)。
		// ... 
		// Expect 100 Continue support
		req := w.req
		// 这里调用 gin的ServeHTTP(gin实现了ServeHTTP函数,这里就是各个HTtp框架和net/http 框架交互的地方)
		serverHandler{c.server}.ServeHTTP(w, w.req)
		// ...
	}
}

到这里我们可以看到tcp连接句柄又被包装了一层 放到了 response(w)中。所以套接字的包装如下:

套接字(socket)—>文件描述符(fd:这里有对特定内存的读写,请求和响应都这这里)---->tcp连接(只有
seriver_ip:port)---->response(w;server_ip:port, client_ip:port)。

可以看到在5.1.2的调用链上,又包装了一层response。
我们只简要介绍下net/http的部分代码,感兴趣的可以自己去看下源码。到这里1.3 的圆圈2(其实还要加上其前面和后面的一步)对应内容基本就简要讲解完毕了。接下来又轮到gin框架的介入,基本流程是这样的:

  1. 启动gin框架,建立gin的方法树
  2. 开始执行run函数,这时run函数调用net/http框架来处理tcp连接(包括建立服务端套接字,阻塞等待新客户端到来然后建立新的完整的套接字)
  3. 等到 连接建立,就开始从对应buf获取请求和响应体,然后将请求和响应结构体转交给gin框架来处理

接下来就是gin框架处理请求和返回响应体了,主要涉及上述代码段最后一行代码

serverHandler{c.server}.ServeHTTP(w, w.req)
5.3. 服务器端需要接收请求(request),处理请求和返回请求(response) ----gin实现的ServerHttp接口可以介入此操作

5.3就进入了gin框架的地界,我们接下来看下gin框架怎么处理请求。

6. gin框架源码–路由匹配(压缩前缀树的查找)

这里来到了 gin的ServeHTTP函数 这个函数主要干两件事,公平,公平,还是他…

  1. 根据url查找对应的树节点
  2. 根据树节点的挂载的函数,来执行请求,返回响应体(这里返回底层又调用的net/http包)
    我们来看下ServeHTTP函数
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

	// 从pool获取context
	c := engine.pool.Get().(*Context)
	//将上次 response的 相关信息清除,并将这次response相应信息存入其中
	c.writermem.reset(w)
	// 将请求信息存入context
	c.Request = req
	// 重置context
	c.reset()
   // 开始执行request请求
	engine.handleHTTPRequest(c)
	// 将context存入池中
	engine.pool.Put(c)
}

这里套接字链条再增加一个 context,如下:

套接字(socket)—>文件描述符(fd:这里有对特定内存的读写,请求和响应都这这里)---->tcp连接(只有
seriver_ip:port)---->response(w;server_ip:port, client_ip:port)---->context。我们层层传递的都是对应的结构体的指针(有显式的指针和隐式指针-----接口)这样可以做到同一个tcp数据流只会有一个套接字来处理,也就是同一套bufer存储r和w,以此类推。

ServerHTTP函数中 engine.handleHTTPRequest©是主逻辑实现,其代码如下:

// handleHTTPRequest 主要来实现 来自客户端的request请求;ServeHTTP 的主要实现
func (engine *Engine) handleHTTPRequest(c *Context) {

	// 获取请求方法和路径
	httpMethod := c.Request.Method
 	// ...

	// Find root of the tree for the given HTTP method
	// 从根节点获取相关http方法对应的树
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		//   根据url获取相关 节点 主要获取 执行函数链
		value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
		if value.params != nil {
			c.Params = *value.params
		}

		// 开始执行函数链 (函数链一般包括中间件函数和对应组添加的额外函数,注册的函数一般最后执行)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}

		// 特殊情况 不做介绍
 		// ...
}

其中 root.getValue用来从树上获取url对应的根节点,然后开始执行根节点上挂载的函数,执行挂载函数主要执行我们构造框架时注册的函数,然后通过context的函数来执行底层net/http方法返回响应。

6.1 获取对应树节点

root.getValue代码如下:

/ getValue;engine 实现获取path对应节点的核心函数;主要思路是 将path按照每层节点的 path 参数进行截断 比较,然后置换参数 for循环 直到找到符合条件的node;采用的算法是树的层次遍历
// ps: 只介绍最通用的正常路径匹配,有通配符等特殊匹配情况不再介绍之列
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
	var globalParamsCount int16

walk: // Outer loop for walking the tree
	for {
		// 获取本节点的path
		prefix := n.path

		// 如果本节点的路径长度 小于 要寻找的路径 则截断 路径 置换 节点;继续下一个层次节点的寻找;eg: n.path=/aa   path=/aa/bb 则 进行截断 path=/bb 节点是 /aa的某子节点。
		if len(path) > len(prefix) {
			if path[:len(prefix)] == prefix {
				path = path[len(prefix):]

				// Try all the non-wildcard children first by matching the indices
				idxc := path[0]
				for i, c := range []byte(n.indices) {
					if c == idxc {
						//  strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
						if n.wildChild {
							index := len(*skippedNodes)
							*skippedNodes = (*skippedNodes)[:index+1]
							(*skippedNodes)[index] = skippedNode{
								path: prefix + path,
								node: &node{
									path:      n.path,
									wildChild: n.wildChild,
									nType:     n.nType,
									priority:  n.priority,
									children:  n.children,
									handlers:  n.handlers,
									fullPath:  n.fullPath,
								},
								paramsCount: globalParamsCount,
							}
						}

						n = n.children[i]
						continue walk
					}
				}

				// ...

		// 如果比配上 则返回本节点对应的 函数链和全路径
		if path == prefix {
			// ...
			// Check if this node has a handle registered.
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
			}
			// ...
}
6.2 执行请求 返回

获得了挂载的树后,开始执行函数链,例如假设 以 2.2 中的 路径“/aa/bb”
浏览器输入 localhost:8080/aa/bb 后,则获得树的对应节点value的函数参数是func(c *gin.Context) { c.JSON(200, gin.H{"route path ": “/benchmark/bb”}) },
我们来添加一些代码:

func(c *gin.Context) {

req:=c.req
// 示例代码
respInfo:= handle(req) // 处理请求
c.writer.xxx=respInfo  // 响应体

 c.JSON(200, gin.H{"route path ": "/benchmark/bb"}) 

}

c是包含req和resp的 context见调用链,执行解析请求后,执行请求后,会执行c.JSON 来返回函数。而c.JSON内部调用 net/http来向客户端返回执行结果,到这里整个请求和返回的链条就闭环了。

7. 收尾

我们可以看到,gin框架只根据url和方法(GET/POST等)构建方法树,然后根据url和方法来找到对应的树节点,最后执行函数,将结果存入返回体。其余的操作都会交给net/http包来实现。

ps: 本人菜鸟 不太专业 如果有错还请各位大侠指出;免责声明:凡是按照本八股文去面试被怼的,本人概不承担责任。
参考文章
https://juejin.cn/post/7263826380889915453

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值