前言
在熟练掌握了学习了 HTTP 框架的设计与实现视频课程后的使用后 , 结合查询了其他的相关资料 , 总结一下以下内容
- 框架的设计基础
- 中间件的设计
- 路由的设计
框架的设计基础
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))
}
在这个例子中 ,
- 我们定义了一个实现了
ServeHTTP的空的结构体 ,ServeHTTP方法接收两个参数:w(http.ResponseWriter)用于写入 HTTP 响应,req(*http.Request)用于接收 HTTP 请求。根据请求的 URL 路径 (req.URL.Path),它会执行不同的逻辑. - 在main函数中 , 我们传给
http.ListenAndServe刚才创建的engine实例 , 将所有的HTTP请求转向了我们自己的处理逻辑 , 而不再使用内置的http.HandleFunc实现的路由
中间件的设计
中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样
那如何设计中间件呢
绝大数框架(例如: 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. 动态参数和通配参数
- 动态参数(
:param):像:post这样的动态参数会匹配一个路径中的单一部分。例如,对于路径/blog/my-first-post,:post会匹配my-first-post并将其存储为参数post。动态参数仅匹配单一层级,因此/blog/:post不会匹配/blog/2023/posts - 通配参数(
*param):通配参数用于匹配路径中的所有剩余部分。例如,路径/more/*anything会匹配/more/photos/nature、/more/blog/posts等。*anything会将剩余路径photos/nature或blog/posts等保存为参数anything。通配符通常用在路径末尾,以处理一类特定路径的子路由。