1. 走进HTTP协议
1.1 HTTP协议概念
说到Http协议我们都几乎很清楚了,我们第一个大规模使用的Http协议版本是0.9。
Http又称超文本传输协议(Hypertext Transfer Protocol)。
什么是超文本呢?顾名思义,就是超越文本或不只是文本,比如图片、音频、视频等。
1.2 协议有什么
在网络传输中,传输的都是0、1这样的数据流,只有按照一定的规则,对方才能识别清楚,这样约定好的规则,就可以称为协议。
协议需要明确的边界:开始、结束。需要清楚数据流是从哪一部分开始的到哪一部分结束的。
协议能够携带信息:消息内容、消息类型等
1.3 请求流程
一个请求需要经过:业务层、服务治理层、中间件层、协议编解码层、传输层。
1.4 不足和展望
下面来对比下Http1、Http2、QUIC都有哪些优缺点:
- Http1:队头阻塞、传输效率低、明文传输不安全
- Http2:多路复用、头部压缩、二进制协议
- QUIC:基于UDP实现、解决队头阻塞问题、加密减少握手次数、支持快速启动
2. HTTP框架设计与实现
2.1 分层设计
OSI是Open System Interconnection的缩写,称为开放式系统互联。
OSI模型把网络通信分为7层,从下到上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
OSI分层太多,增加了网络工作的复杂性,后来进行了见后,将一些层进行合并形成了4层结构,从下到上分别为接口层、网络层、传输层和应用层,也就是TCP/IP模型。
分层设计可以提高专注性、扩展性、复用性。
在分层设计的时候应该考虑高内聚低耦合、易复用、高扩展。
API分层设计:
总结以下API设计:
- 应用层设计:可理解、简单
- 中间件设计:洋葱模型
- 路由设计:前缀匹配树
- 协议层设计:抽象出合适的接口
- 网络层设计:网络模型BIO和NIO
2.2 应用层设计
应用层是和用户打交道的一层,提供合理的API。
在设计API时,需要考虑以下几点:
-
可理解性如:ctx.Body()、ctx.GetBody(),而不是ctx.BodyA()。
-
简单性:如:ctx.Request.Header.Peek(key)封装为ctx.GetHeader(key)
-
冗余性
-
兼容性
-
可测性
-
可见性
不要视图在文档中说明,很多用户不看文档。
2.3 中间件设计
中间件在工作中,常常有以下需求:
- 配合Handler实现一个完整的请求处理声明周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
如下图是一个洋葱模型:
洋葱模型的思想是核心逻辑与通用逻辑分离。
中间件要实现预处理和后处理,很像调用了一个函数,如:
func Middleware(some param) {
// some logic for pre-handler
Next()
// some logic for after-hanler
}
路由上可以注册多个中间件,同时也可以满足请求级别有效只需要将中间件设计为和业务和hanlder相同即可。
如果用户不主动调用下一个处理函数怎么办?
我们可以让用户在使用调用中间件时,先进行初始化,然后中间件处理调用的逻辑,如:
func (ctx *RequestContext) Next() {
ctx.index++
for ctx.index < int8(len(ctx.handlers)) {
ctx.hanlers[ctx.index]()
ctx.index++
}
}
核心思想是在任何场景下index保证递增。
如果出现异常想停止怎么办?
只需要给index赋值一个最大值,跳出循环即可,如:
func (ctx *RequestContext) Abort() {
ctx.index = IndexMax
}
中间件调用链:
不适用Next:初始化逻辑且不需要在同一个调用栈
适用Next:后处理逻辑或者需要在同一个调用栈
2.4 路由设计
框架路由实际上就是为URL匹配对应的处理函数(Handlers)
- 静态路由:/a/b/c、/a/b/d
- 参数路由:/a/:id/c、/*all
- 路由修复:/a/b <-> /a/b/
- 冲突路由以及优先级:/a/b、/:id/c
- 匹配HTTP方法
- 多处理函数:方便添加中间件
路由设计:
青铜:map
黄金:前缀匹配树
如何匹配HTTP方法?外层map根据method进行初步筛选,然后使用前缀树匹配路由。
如何实现添加多处理函数?在每个节点上使用一个list存储handler。
2.5 协议层设计
抽象出合适的接口:
2.6 网络层设计
这里需要预备一个知识点:BIO和NIO
BIO使用一个监听器,中途出现堵塞就卡住了。
NIO使用监控器,监控可以使用的IO连接,用于后续的业务。
go net就是BIO,用户管理buffer。
netpoll是NIO,网络库管理buffer
3. 性能修炼之道
总结:
- 针对网络库的优化:buffer 设计
- 针对协议的优化:header 解析、热点资源池化
3.1 网络库优化
网络库:C10K Problem;Select,Poll,Epoll;Epoll ET、LT 区别;字节跳动自研网络库 netpoll,netpoll-examples
go net:优化方案是为每一个连接绑定一块缓冲区buffer,大小为4k,这样对连接的压力也不大,最后记得回收内收。这样的优化可以存下全部 Header、减少系统调用次数、能够复用内存、能够多次读。
netpoll:在底层分配一个大节点,分配足够大的buffer,这样可以存下全部 Header,拷贝出完整的 Body。
go net:流式友好,小包性能高。
netpoll:中大包性能好,时延低。
3.2 针对协议优化——Headers 解析
方案:
- SIMD加速解码.
- 针对协议相关的 Headers 快速解析
SIMD:即单指令流多数据流,是一种采用一个控制器来控制多个处理器,同时对一组数据(又称“数据向量”)中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。
针对协议相关的 Headers 快速解析:
- 通过 Header key 首字母快速筛除掉完全不可能的 key
- 解析对应 value 到独立字段
- 使用 byte slice 管理对应 header 存储,方便复用
请求体中同样处理的Key:User-Agent、Content-Type、 Content-Length、Connection、 Transfer-Encoding
优点:核心字段快速解析,使用byte slice存储,额外存储到成员变量中。
缺点:普通 header性能较低,没有 map 结构。
3.3 针对协议优化——Header key 规范化
表映射:将字节转化成ascii码,匹配效率可以达到O(1)
优点:超高的转换效率,比 net.http 提高 40倍
缺点:额外的内存开销,变更困难
3.4 热点资源池化
优点:减少了内存分配,提高了内存复用,降低了GC压力,性能提升。
缺点:额外的 Reset 逻辑,请求内有效,问题定位难度增加。
4. 企业实战
追求性能
追求易用,减少误用
打通内部生态
文档建设、用户群建设
字节HTTP框架:Hertz