这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
这里讲的主要是,HTTP 协议是当今广泛使用的协议之一,本节课作为开场将为大家详细介绍 HTTP 协议出现背景与内涵,帮助同学对该协议建立基本的认知。
再谈HTTP协议
HTTP协议介绍
Hypertext Transfer Protocol
-
什么是超文本?
- JPG, MP3, AVI
- 是文本TEXT的补充嘛
-
why 协议?
- 明确的边界:开始,结束的位置
- 能够携带的信息:什么信息,信息类型
-
常见的 post 在协议层做了什么
-
协议里面有什么?
- 请求行/状态行(方法名,URL,协议版本/协议版本,状态码,状态码描述)
- 请求头/相应头
- 请求体/相应体
- Hertz实现的Demo
- 请求流程:
- 不足与展望:
HTTP框架的设计与实现
-
分层设计:
- 专注性
- 扩展性
- 复用性
- 特点:高内聚,低耦合,易用性,高扩展性
一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的。从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始。 ---盖尔定律
应用层设计
提供合理的API
- 可理解性:如ctx.Body(), ctx.GetBody(),不要用ctx.BodyA(),明明很重要捏!!!
- 简单性:如ctx.Request.Header.Peek(key)/ctx.GetHeader(key)
- 冗余性,兼容性,可测性,可见性
不要试图在文档中说明,很多用户不看文档
中间件设计
-
中间件需求:
- 配合Handler 实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
- 洋葱模型:
核心逻辑与通用逻辑分离
- 🌰:
- 设计:
- 实现预处理和后处理,很像调用了一个函数
func Middleware(some param) {
// some logic for pre-handle
...
Next()
...
// some logic for after-handle
}
- 路由上可以注册多个Middleware,同时可以满足请求级别有效,只需要将Middleware设计为和业务和Handler相同即可。
- 用户如果不主动调用下一个处理函数怎么办?
func Middleware(some param) {
// some logic
...
}
- 我们帮助用户主动调用下一个处理函数
func (ctx *RequestContext) Next() {
ctx.index++
for ctx.index < int8(len(ctx.handlers)) {
ctx.handlers[ctx.index]()
ctx.index++
}
}
核心:在任何场景下,index保证递增
- 出现异常想要停止怎么办?
func (ctx *RequestContext) Abort() {
ctx.index = IndexMax
}
直接等于IndexMax,就不会往下走了昂,直接跳出循环就好了昂!!!
- 调用链
例如:如果B是Recovery()组件,不再C -> Handler的调用链上,是捕捉不到下面的bug的昂!!!
路由设计
-
设计:
- 青铜:Map[string]handlers,简单!静态方便并且快!
- 黄金:前缀匹配树,如何处理带参数的路由注册呢?(处理形如:/a/:id/b的路由)
-
如何匹配HTTP方法
- 如何实现添加多处理函数?
- 思考:如何查找路由
如何做设计
- 明确需求:考虑清楚要解决什么问题、有哪些需
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最合适的方案进行开发
协议层设计
- 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.
- 需要在连接上读写数据
- 错误处理
网络层设计
-
标准库go net:
- 标准的"BIO"
- 用户管理buffer
- Netpoll, NIO, 网络库管理buffer
性能修炼之道
针对网络库的优化
-
问题:
- go net, "BIO"
- 存下全部Header
- 减少系统调用次数(操作系统状态切换开销大)
- 能够复用内存
- 能够多次读
type Conn interface {
Read(b []byte)(n int, err error)
Write(b []byte)(n int, err error)
...
}
- go net with buffo: 绑定一块缓冲区
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 with nocopy peek
- Conn设计:
type Conn interface {
net.Conn
Reader
Writer
}
go net & netpoll
Go net:
- 流式友好
- 小包性能高
Netpoll:
- 中大包性能高
- 时延低
协议优化-Headers解析
- 找到Header Line边界:\r\n
- 先找到\n,看看前一个是不是\r -> O(n)。能不能更快?(SIMD)
SIMD(Single Instruction Multiple Data)即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。
并行加速,编解码超快的!!!Go自己就会用上昂!!!
- 性能提升:
- 快速解析:
-
取舍:
-
取:
- 核心字段快速解析
- 使用byte slice存储
- 额外存储到成员变量中
-
舍:
- 普通header性能较低
- 没有map结构
-
协议优化-Header key 规范化
-
Header key规范化:
- aaa-bbb -> Aaa-Bbb
- ToUpperTable,算法很快
-
取舍:
-
取:
- 超高的转换效率
- 比 net.http 提高40倍
-
舍:
- 额外的内存开销
- 变更困难
-
热点资源池化
-
取舍:
-
取:
- 减少内存分配
- 提高内存复用
- 降低GC压力
- 性能提升
-
舍:
- 额外Reset逻辑
- 请求内有效
- 问题定位难度增加
-
总结
- 针对网络库的优化:buffer设计
- 针对协议的优化:header解析、热点资源池化
企业实践
- 追求性能
- 追求易用,减少误用
- 打通内部生态
- 文档建设、用户群建设
性能很重要,但是对于很多业务来说,易用性和减少误用更重要昂!!!
References
- 七层/四层
Reference: zq99299.github.io/note-book2/…
- OSI 模型分成了七层,部分层次与 TCP/IP 很像,从下到上分别是:
- OSI 在设计之初就参考了 TCP/IP 等多个协议,可以比较容易但不是很精确地实现对应关系。
- 第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等;
- 第二层:数据链路层,它基本相当于 TCP/IP 的链接层;
- 第三层:网络层,相当于 TCP/IP 里的网际层;
- 第四层:传输层,相当于 TCP/IP 里的传输层;
- 第五层:会话层,维护网络中的连接状态,即保持会话和同步;
- 第六层:表示层,把数据转换为合适、可理解的语法和语义;
- 第七层:应用层,面向具体的应用传输数据。
- 七层负载均衡,就是HTTP这一层的负载均衡策略
- 四层负载均衡,就是TCP这一层的负载均衡策略
至此,我们常说的「四层」、「七层」就出现了。
- netpoll 地址: github.com/cloudwego/n…
- SIMD解释:zhuanlan.zhihu.com/p/55327037
-
SIMD在go和sonic中的支持:
- Golang中SIMD支持:github.com/golang/go/b…
- Sonic:github.com/bytedance/s…