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

105 阅读4分钟

分层设计

一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的,从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始 -- 盖尔定律

专注性、扩展性、复用性

分层设计.png

高内聚低耦合、易复用、高扩展性

Http分层设计.png

HTTP分层.png

应用层设计

提供合理的API 不要试图在文档中说明,很多用户不看文档

  • 可理解性:
    • ctx.GetBody(√)
    • ctx.Body()ctx.BodyA()(×)
  • 简单性
    • ctx.Request.Header.Peek(key)->ctx.GetHeader(key)
  • 冗余性
  • 兼容性
  • 可测性
  • 可见性

中间件设计

中间件需求

  • 配合Handler实现一个完整的请求处理生命周期
  • 拥有预处理逻辑与后处理逻辑
  • 可以注册多中间件
  • 对上层模块用户逻辑模块易用

洋葱模型

核心逻辑与通用逻辑分离

洋葱模型.png

适用场景:

  • 日志记录
  • 性能统计
  • 安全控制
  • 事务处理
  • 异常处理

代码示例:打印每个请求的request和response

//没有使用中间件
func main() {
	h := server.New()

	h.POST("/login", func(c context.Context, ctx *app.RequestContext) {
		//print request
		logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
		//some biz logic
		ctx.JSON(200, "OK")
		//print response
		logs.Infof("Send RawResponse: %s", ctx.Response.RawResponse())
	})

	h.POST("/logout", func(c context.Context, ctx *app.RequestContext) {
		//print request
		logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
		//some biz logic
		ctx.JSON(200, "OK")
		//print response
		logs.Infof("Send RawResponse: %s", ctx.Response.RawResponse())
	})

	h.Spin()
}
//没有使用中间件
func main() {
	h := server.New()

	h.Use(func(c context.Context, ctx *app.RequestContext) {
		//print request
		logs.Infof("Received RawRequest: %s", ctx.Request.RawRequest())
		//next handler
		ctx.Next(c)
		//print response
		logs.Infof("Send RawResponse: %s", ctx.Response.RawResponse())
	})

	h.POST("/login", func(c context.Context, ctx *app.RequestContext) {
//some biz logic
		ctx.JSON(200, "OK")
	})

	h.POST("/logout", func(c context.Context, ctx *app.RequestContext) {
//some biz logic
		ctx.JSON(200, "OK")
	})

	h.Spin()
}

设计

  1. 既然要实现预处理和后处理,那这个就像调用了一个函数
func Middleware(some param) {
	//some logic for pre-handle
	...
	nextMiddleware() / bizLogiz()
	...
	//some logic after-handle
	...
}
  1. 路由上可以注册多Middleware,同时也可以满足请求级别有效,只需要将Middleware设计为和业务和Handler相同即可
func Middleware(some param) {
	//some logic for pre-handle
	...
	Next()
	//some logic after-handle
	...
}
  1. 用户如果不主动调用下一个处理函数怎么办
//核心:在任何场景下index保证自增
func (ctx *RequestContext) Next() {
	ctx.index++
	for ctx.index < int8(len(ctx.handlers)) {
		ctx.handlers[ctx.index]()
		ctx.index++
	}
}
  1. 出现异常想停止怎么办
func (ctx *RequestContext) Abort() {
	ctx.index = IndexMax
}

调用链

中间件调用链.png 适用场景:

  • 不调用Next:初始化逻辑且不需要在同一调用栈
  • 调用Next:后处理逻辑需要在同一调用栈上

路由设计

框架路由实际上就是为URL匹配对应的处理函数(Handlers)

  • 静态路由:/a/b/c、/a/b/d
  • 参数路由:/a/:id/c (/a/b/c 、/a/d/c)、/*all
  • 路由修复:/a/b <-> /a/b/
  • 冲突路由以及优先级:/a/b、/:id/c
  • 匹配HTTP方法
  • 多处理函数:方便添加中间件

如何匹配路由

青铜:map[string]handlers

  • 优势:快、简单
  • 劣势:仅对静态路由有效

黄金:前缀匹配树

  • 静态路由 前缀匹配树静态路由.png
  • 参数路由 前缀匹配树参数路由.png

如何匹配http方法

设计一个路由映射表: 路由映射表.png 外层Map:根据method进行初步筛选

如何实现添加多处理函数

在每个节点上使用一个list存储handler

node struct {
	prefix string
	parent *node
	children children
	handlers app.HandlersChain 
}

协议层设计

抽象出合适的接口:

type Server interface {
	Serve(c context.Context, conn network.Conn) error
}
  1. Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter
  2. 需要在连接上读写数据

网络层设计

BIO(Block IO)

go func() {
	for {
		conn, _ := listener.Accept()
		go func() {
			conn.Read(request)
			handle...
			conn.Write(response)	
		}()
	}
}()

NIO

go func() {
	for {
		readableConns, _ := Monitor(conns)
		for conn := range readableConns {
			go func() {
				conn.Read(request)
				handle...
				conn.Write(response)
			}()
		}
	}
}()

注册一个监听器,当监听到有足够数据时再去唤醒func,避免卡在read上

网络库

go net(golang标准库)

type Conn interface {
	//n表示读取和写入的长度
	//error返回错误
	Read(b []byte) (n int, err error)
	Write(b []byte) (n int, err error)
	...
}
  • 如果没有数据,但调用了Read/Write方法,就会卡住等待数据,属于阻塞IO,即"BIO"
  • 两个接口都是由用户传入数据,属于用户管理buffer

netpoll

type Reader interface {
	//希望底层返回长度为n的数据,到了这个量才会唤醒
	Peek(n int) ([]byte, error)
	...
}

//由于是NIO,不确定数据发出时间,所以需要将数据写入底层,保证数据是不变的
type Writer interface {
	Malloc(n int) (buf []byte, err error)
	Flush() error
	...
}
  • 采用NIO
  • 网络库管理buffer

网络层设计

type Conn interface {
	net.Conn
	Reader
	Writer
}

如何做设计

  1. 明确需求:考虑清楚要解决什么问题,有哪些需求
  2. 业界调研:业界都有哪些解决方案可供参考
  3. 方案权衡:思考不同方案的取舍
  4. 方案评审:相关同学对不同方案做评审
  5. 确定开发:确定最合适的方案进行开发
  • API设计:可理解性、简单性……
  • 中间件设计:洋葱模型
  • 路由设计:前缀匹配树
  • 协议层设计:抽象出合适的接口
  • 网络层设计:网络模型