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 请求流程如下所示:
HTTP 框架的设计与实现
HTTP 框架的设计应该同 HTTP 协议一样是一种分层设计理念。其中包括应用层、中间件、路由、协议编解码、传输层。
应用层设计
HTTP 框架中的应用层指的是与用户交互的框架 API 设计。它应该满足以下特性:
-
可理解性:如 ctx.Body(), ctx.GetBody(), 而不是 ctx.BodyA() 此处 A 没有明确含义。
-
简单性:如 ctx.Request.Header.Peek(key) 调用链太多,可封装为 ctx.GetHeader(key)
中间件设计
如果一个通用的功能(如:日志记录、性能统计、安全控制、异常处理 ... )在很多实际业务逻辑处理中所使用。我们可以对齐进行分离,将通用逻辑单独作为一个模块所复用。下图是洋葱模型,展示了中间件的设计理念。
使用实例:
// 定义一个中间件
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 一个字典来保存。但这会带来路径数据的冗余存储和无法解析参数路径的问题。于是,可以想到采用前缀匹配树来实现。下图展示了一个实例:
前缀树的数据接口设计如下,其中 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, 这通常是根据历史请求大小的统计,而计算得来的。
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 时,无需重复创建、释放对象。
优点:
-
减少了内存分配
-
提高了内存复用
-
降低了 GC 压力
-
性能提升
缺点:
-
额外的 Reset 逻辑,使用/释放哪个 RequestContext
-
问题定位难度增加
企业实践总结
-
追求性能,但不仅仅只有性能。
-
追求易用性,减少误用。主要针对框架、库 API 接口方面的设计。
-
打通内部生态。
-
文档建设、用户群建设。
总结
本次课程主要讲述了 HTTP 协议内容, HTTP 框架设计与实现,如何优化 HTTP 框架性能三方面。