HTTP框架修炼之道 | 青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第14篇笔记
01. 再谈HTTP 协议
1.0 HTTP 框架中常见概念
Golang
- sync.Pool 用法
网络库
- C10K Problem
- Select,Poll,Epoll
- Epoll ET、LT 区别
- 字节跳动自研网络库 netpoll,netpoll-examples
1.1 HTTP 协议是什么
HTTP: 超文本传输协议(Hypertext Transfer Protocol)
为什么需要协议
-
需要明确的边界
- 开始
- 结束
-
能够携带信息
- 什么消息
- 消息类型
1.2 HTTP 协议有什么
-
请求行/状态行
-
请求行
-
方法名
- GET
- HEAD
- POST
- PUT(完整更新,幂等)
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH (部分更新)
-
URL
-
协议版本
-
-
状态行
-
协议版本
-
状态码
-
状态码描述
- 1xx : 信息类
- 2xx : 成功
- 3xx : 重定向
- 4xx : 客户端错误
- 5xx : 服务端错误
-
-
-
请求头/响应头
-
请求头
- 协议约定
- 业务相关
-
响应头
- 协议约定
- 业务相关
-
-
请求体/响应体
1.3 请求流程

1.4 不足与展望
-
HTTP1
- 队头阻塞
- 传输效率低
- 明文传输不安全
-
HTTP2
- 多路复用
- 头部压缩
- 二进制协议
-
QUIC
- 基于UDP实现
- 解决队头阻塞
- 加密减少握手次数
- 支持快速启动
02. HTTP框架的设计与实现
2.1 分层设计

- 专注性
- 扩展性
- 复用性

- 高内聚
- 低耦合
- 高扩展性
对比:

一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的。从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始。
2.2 应用层设计
提供合理的API
- 不可理解性 :如
ctx.Body(),ctx.GetBody(), 不要用ctx. BodyA() - 简单性:如
ctx.Request.Header.Peek(key)/ctx.GetHeader(key) - 冗余性
- 兼容性
- 可测性
- 可见性
2.3 中间件设计
中间件需求:
- 配合Handler实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
洋葱模型:
!
适用场景:
- 日志记录
- 性能统计
- 安全控制
- 事务处理
- 异常处理
中间件设计:
-
既然要实际要实现预处理和后处理,那这个就很像调用了一个函数
-
路由上可以注册多Middleware, 同时也可以满足请求级别有效只需要将Middleware 设计为和业务和Handler 相同即可。
-
用户如果不主动调用下一个处理函数怎 么办?
-
如果出现异常停止怎么办
调用链:

有什么坑? –不在一个调用栈上
适用场景:
- 不调用Next: 初始化逻辑且不需要在同一调用栈
- 调用Next: 后处理逻辑或需要在同一调用栈上
2.4 路由的设计
-
框架路由:根据请求的 URI 选择对应的处理函数。
-
首先匹配 HTTP 方法 – 前缀树
-
静态路由: 精确匹配注册的路由,如:/a/b/c、/a/b/d – Map[string]handlers
-
参数路由: – 前缀树
- 命名参数:形如 **
:name**这类叫做命名参数,命名参数只匹配单个路径段: -
Pattern: /user/:user /user/gordon match user = gordon) /user/you match(user = you) /user/gordon/profile no match /user/ no match - 通配参数:形如 **
*action**这类叫做通配参数,就像名字所暗示的那样,它们匹配所有内容。因此,它们必须始终位于模式的末尾: -
Pattern: /src/*filepath /src/ match(filepath = "") /src/somefile.go match(filepath = somefile.go) /src/subdir/somefile.go match(filepath = subdie/somefile.go)
- 命名参数:形如 **
-
路由修复: 如果只注册了 /a/b,但是访问的 URI 是 /a/b/,那可以提供自动重定向到 /a/b 能力;同样,如果只注册了 /a/b/,但是访问的 URI 是 /a/b,那可以提供自动重定向到 /a/b/ 能力
-
冲突路由:同时注册 /a/b 和 /:id/b,并设定优先级。比如:当请求 URI 为 /a/b 时,优先匹配静态路由 /a/b
-
-
如何做设计
- 明确需求:考虑清楚要解决什么问题、有哪些需求
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最合适的方案进行开发
2.5 协议层的设计
抽象出合适的接口
type Server interface{
Serve(c context.Context, conn network.Conn) error
}
- 不要显示的把Context写在struct 里面,而是显示的把他当做第一个参数传递进来
- 需要在连接上读写数据
2.6 网络层设计
- BIO go net 用户管理buffer
- NIO:注册监听器 netpoll 网络库管理 buffer
2.7 总结
- API设计:可理解性、简单性.
- 中间件设计:洋葱模型
- 路由设计:前缀匹配树
- 协议层设计:抽象出合适的接口
- 网络层设计:网络模型
03 性能修炼之道
3.1 针对网络库的优化
-
go net
- 存下全部Header
- 减少系统调用次数
- 能够复用内存
- 能够多次读
-
go net with bufio
- 绑定-块缓冲区
-
netpoll
- 存下全部的Header
- 拷贝出完整的Body
-
netpoll with nocopy peek
- 分配足够大的buffer
- 限制最大buffer size
-
不同网络库的优势
-
go net
- 流式友好
- 小包性能高
-
netpoll
- 中大包性能高
- 时延低
-
3.2 针对协议的优化 – Headers解析
找到Header Line 边界: \r\n
先找到\n再看它前一个是不是\r
那能不能更快? SIMD
针对协议相关的Headers 快速解析:
- 通过Header key 首字母快速筛除掉完全不可能的key
- 解析对应value到独立字段
- 使用byte slice 管理对应header 存储,方便复用
请求体中同样处理的Key: User-Agent、Content-Type、 Content-L ength、Connection、 Transfer-Encoding
取
- 核心字段快速解析
- 使用byte slice存储
- 额外存储到成员变量中
舍
- 普通header性能较低
- 没有map结构
3.3 针对协议的优化 – Header key 规范化
aaa-bbb → Aaa-Bbb
取
- 超高的转换效率
- 比net.http 提高40倍
舍
- 额外的内存开销
- 变更困难
3.4 热点资源池化

取
- 减少了内存分配
- 提高了内存复用
- 降低了GC压力
- 性能提升
舍
- 额外的Reset 逻辑
- 请求内有效
- 问题定位难度增加
3.5 总结
- 针对网络库的优化: buffer设计
- 针对协议的优化: header解析、热点资源优化
04 企业实践
- 追求性能
- 追求易用、减少误用
- 打通内部生态
- 文档建设、用户群建设
尝试写一个 hello world 服务器
可尝试用 gin 写一个 hello world 程序,达到以下效果
\