HTTP框架修炼之道 | 豆包MarsCode AI 刷题

72 阅读4分钟

HTTP 协议

HTTP 即超文本传输协议 ( Hypertext Transfer Protocol ), 这里的超文本指的是传输数据不仅局限于传统纯文本数据,还包括图片、音频、视频等多媒体数据。

HTTP 协议大致分为三部分:请求行/状态行、请求头/响应头、请求体/响应体。

  • 请求行/状态行:包含方法名 ( GET, HEAD, POST, PUT ... ),URL,协议版本,状态码 ( 成功、重定向、服务端错误 ... ),状态码描述等内容。

  • 请求头/响应头:协议约束、业务相关,如服数据类型、主机地址、日期 ...

  • 请求体/响应体:传输数据的具体内容。

POST /sis HTTP/1.1
Who: Alex
Content-Type: text/plain
Host: 127.0.0.1:8888
Content-Length: 28


Let's watch a movie together
HTTP/1.1 200 Ok
Server: hertz
Date: Thu, 21 Apr 2022 11:46:32 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 2
Upstream-Caught: 1650541592984580


Ok

整个 HTTP 请求流程如下所示:

2024-11-08-01-54-19-image.png

HTTP 框架的设计与实现

HTTP 框架的设计应该同 HTTP 协议一样是一种分层设计理念。其中包括应用层、中间件、路由、协议编解码、传输层。

应用层设计

HTTP 框架中的应用层指的是与用户交互的框架 API 设计。它应该满足以下特性:

  • 可理解性:如 ctx.Body(), ctx.GetBody(), 而不是 ctx.BodyA() 此处 A 没有明确含义。

  • 简单性:如 ctx.Request.Header.Peek(key) 调用链太多,可封装为 ctx.GetHeader(key)

中间件设计

如果一个通用的功能(如:日志记录、性能统计、安全控制、异常处理 ... )在很多实际业务逻辑处理中所使用。我们可以对齐进行分离,将通用逻辑单独作为一个模块所复用。下图是洋葱模型,展示了中间件的设计理念。

2024-11-08-11-21-34-image.png

使用实例:

// 定义一个中间件
func MyMiddleware() app.HandlerFunc {
  return func(ctx context.Context, c *app.RequestContext) {
    // pre-handle
    // ...
    c.Next(ctx)
    // post-handle
    // ...
  }
}


// 使用中间件
h := server.Default()
h.Use(GlobalMiddleware())
group := h.Group("/group")
group.Use(GroupMiddleware())

路由设计

所谓的路由,就是解析请求路径,找到对应的需要执行的业务逻辑函数。最简单的实现思路是使用 map[string]handlers 一个字典来保存。但这会带来路径数据的冗余存储和无法解析参数路径的问题。于是,可以想到采用前缀匹配树来实现。下图展示了一个实例:

2024-11-08-11-42-20-image.png

前缀树的数据接口设计如下,其中 prefix 表示当前匹配的字符串 parent 表示父节点 children 表示子节点集合 handlers 表示匹配的业务逻辑处理函数列表。

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

此外,不同 HTTP 方法会拥有各自的前缀树头节点。根据 method 进行初步筛选。

HTTP 框架实现的性能优化

针对网络库的优化

go net 优化

将 go net 与一块缓冲区绑定,存下全部 Header , 减少系统调用次数,能够复用内存,能够多次读。

type Reader interface {
    Peek(n int)([]byte, error)
    Discard(n int)(discarded int, err error)
    Release() error
    Size() int
    Read(b []byte)(l int, err error)
    ...
}


type Writer interface {
    Write(p []byte)
    Size() int
    Flush() error
    ...
}

netpoll 优化

存下全部 Header , 拷贝出完整的 Body。分配足够大的 buffer , 同时要限制最大的 buffer size, 这通常是根据历史请求大小的统计,而计算得来的。

2024-11-08-17-44-08-image.png

go net 与 netpoll 对比

  • go net: 流式友好、小包性能高。

  • netpoll: 中大包性能高、时延低。

针对协议的优化

Headers 解析

解析 Header , 实际上就是找到 Header Line 边界: \r\n。最直接简单的思路就是循环遍历数组查找,但实际上有更快的方法:SIMD ( Single Instruction Multiple Data ) 。该方法可以实现空间上的并行性技术,也就是一个指令能够同时处理多个数据, go 底层源码中解析 Header 也是采用该方法实现的。

Header key 规范化

通常我们需要将 header key 的首字母变为大写,例如: aaa-bbb -> Aaa-Bbb。于是,我们可以提前创建一个字符数组,其中小写字母取值位置存储相应的大写字母,其余字符不变。

优点:

  • 超高的转换效率

  • 比 net.http 提高 40 倍。

缺点:

  • 额外的内存开销

  • 变更困难

热点资源池化

如果内次请求都要重复创建 RequestContext 的话,比较耗时,浪费资源。于是,可以先创建一个 RequestContext 池,缓存多个 RequestContext 。当接收到一个 Request 时,无需重复创建、释放对象。

2024-11-08-18-14-47-image.png

2024-11-08-18-15-04-image.png

优点:

  • 减少了内存分配

  • 提高了内存复用

  • 降低了 GC 压力

  • 性能提升

缺点:

  • 额外的 Reset 逻辑,使用/释放哪个 RequestContext

  • 问题定位难度增加

企业实践总结

  • 追求性能,但不仅仅只有性能。

  • 追求易用性,减少误用。主要针对框架、库 API 接口方面的设计。

  • 打通内部生态。

  • 文档建设、用户群建设。

总结

本次课程主要讲述了 HTTP 协议内容, HTTP 框架设计与实现,如何优化 HTTP 框架性能三方面。