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框架的介入,基本流程是这样的:
- 启动gin框架,建立gin的方法树
- 开始执行run函数,这时run函数调用net/http框架来处理tcp连接(包括建立服务端套接字,阻塞等待新客户端到来然后建立新的完整的套接字)
- 等到 连接建立,就开始从对应buf获取请求和响应体,然后将请求和响应结构体转交给gin框架来处理
接下来就是gin框架处理请求和返回响应体了,主要涉及上述代码段最后一行代码
serverHandler{c.server}.ServeHTTP(w, w.req)
5.3. 服务器端需要接收请求(request),处理请求和返回请求(response) ----gin实现的ServerHttp接口可以介入此操作
5.3就进入了gin框架的地界,我们接下来看下gin框架怎么处理请求。
6. gin框架源码–路由匹配(压缩前缀树的查找)
这里来到了 gin的ServeHTTP函数 这个函数主要干两件事,公平,公平,还是他…
- 根据url查找对应的树节点
- 根据树节点的挂载的函数,来执行请求,返回响应体(这里返回底层又调用的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