HTTP 框架的设计与实现 | 豆包MarsCode AI刷题

49 阅读4分钟

前言

在熟练掌握了学习了 HTTP 框架的设计与实现视频课程后的使用后 , 结合查询了其他的相关资料 , 总结一下以下内容

  1. 框架的设计基础
  2. 中间件的设计
  3. 路由的设计

框架的设计基础

1 . 标准库的web调用

以下用net / http 标准库的调用相信大家并不陌生

package main

import (
    "fmt"
    "net/http"
)

// HelloHandler handles the "/" route
func HelloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    // Register the HelloHandler for the root route
    http.HandleFunc("/", HelloHandler)

    // Start the server on port 8080
    fmt.Println("Starting server on :8080...")
    if err := http.ListenAndServe(":8080", nil); err != nil {
        fmt.Printf("Error starting server: %v\n", err)
    }
}

要实现路由的启动, 主要就是 http.ListenAndServe的调用

让我们看一下这个函数

2 .http.Handler接口的实现
package http

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

// The handler is typically nil, in which case [DefaultServeMux] is used.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

关键在于Handler接口 , 只要传入任何实现了 ServerHTTP 接口的实例,所有的HTTP请求,就都交给了该实例处理了 , 从而就能代替原来的默认行为。 如下demo

// Engine is the uni handler for all requests
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.URL.Path {
	case "/":
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	case "/hello":
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":8080", engine))
}

在这个例子中 ,

  1. 我们定义了一个实现了ServeHTTP 的空的结构体 , ServeHTTP 方法接收两个参数:w(http.ResponseWriter) 用于写入 HTTP 响应,req(*http.Request)用于接收 HTTP 请求。根据请求的 URL 路径 (req.URL.Path),它会执行不同的逻辑.
  2. 在main函数中 , 我们传给http.ListenAndServe 刚才创建的engine实例 , 将所有的HTTP请求转向了我们自己的处理逻辑 , 而不再使用内置的 http.HandleFunc 实现的路由

中间件的设计

中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样

那如何设计中间件呢

image.png

绝大数框架(例如: hertz)采用的是Next调用

我们具体看以下这里实现

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
//
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}


func (group *RouterGroup) Use(middleware ...app.HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}



// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
func (ctx *RequestContext) Next(c context.Context) {
    ctx.index++
    for ctx.index < int8(len(ctx.handlers)) {
       ctx.handlers[ctx.index](c, ctx)
       ctx.index++
    }
}


// Abort prevents pending handlers from being called.
//
// Note that this will not stop the current handler.
// Let's say you have an authorization middleware that validates that the current request is authorized.
// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
// for this request are not called.
func (ctx *RequestContext) Abort() {
    ctx.index = rConsts.AbortIndex
}



如图调用链 , 我们能更好的解读核心函数next 函数 , 通过append方法 添加到 group.Handlers
这里递归调用ctx.handlers[ctx.index](c, ctx) , 通过外置的ctx.index++保证在handlers中在使用Next时能够完美的跳到下一个

路由的设计

基数树又称为PAT位树(Patricia Trie or crit bit tree),是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例 : 基数树

应用到具体的路径中如下图:

Priority   Path            
9          \                
3          ├s              
2          |├earch\         
1          |└upport\        
2          ├blog\           
1          |    └:post
1          |         └\     
2          ├about-us\       
1          |        └team\  
1          └more\   
1               └*anything    

1. Priority优先级

在这个路由表中 , 每行左手边的数字 Priority(优先级) 是在子节点(子节点、子子节点等等)中注册的句柄的数量 , gin 框架中引入优先级的概念 , 是为了更好的平衡平均查询时间 ,如果我们先匹配节点少的树 , 那么有些短的路径得匹配得快 , 但有点长的路径匹配得慢 , 所以我们应该选择最长的路径可以被优先匹配 类似于成本补偿

2. 动态参数和通配参数
  1. 动态参数(:param):像 :post 这样的动态参数会匹配一个路径中的单一部分。例如,对于路径 /blog/my-first-post:post 会匹配 my-first-post 并将其存储为参数 post。动态参数仅匹配单一层级,因此 /blog/:post 不会匹配 /blog/2023/posts
  2. 通配参数(*param):通配参数用于匹配路径中的所有剩余部分。例如,路径 /more/*anything 会匹配 /more/photos/nature/more/blog/posts 等。*anything 会将剩余路径 photos/natureblog/posts 等保存为参数 anything。通配符通常用在路径末尾,以处理一类特定路径的子路由。