HTTP协议
HTTP协议是什么
Hypertext Tranfer Protocal:超文本传输协议。在计算机网络的学习中都非常熟悉了,不熟悉的小伙伴可以看看《图解HTTP》这本书,讲的非常通俗易懂。
为什么需要协议?
- 需要明确的边界:什么时候开始,什么时候结束?
- 需要携带信息:什么消息?消息类型?
一个常见的POST请求在协议层到底做了什么?
POST /sis HTTP/1.1 —— 请求行 Who:Alex —— KV对(元数据) Content-Type:text/plain —— KV对(元数据) Host:127.0.0.1:8888 —— KV对(元数据) Content-Length:28 —— KV对(元数据),表示发送多少个字节 Let’s watch a movie together. —— 请求体
协议内有什么?
- 请求行/状态行 请求行:方法名(GET POST PUT DELETE PATCH TRACE OPTIONS CONNECT)、URL、协议版本
顺带一提PUT和PATCH的区别: PUT是完整更新,幂等。PATCH类似PUT,部分更新,不是幂等。
状态行:协议版本、状态码(1xx/2xx/3xx/4xx/5xx)、状态描述
- 请求头/响应头:协议约定、业务相关
- 请求体/响应体
请求流程
不足和展望
HTTP1:
- 基于Tcp,有队头阻塞问题,后续的分片需要等待前面分片的到来。
- 传输效率低:无用信息非常多、不支持多路复用
- 明文传输不安全
HTTP2:
- 可以多路复用
- 可以头部压缩,减少头部数据量
- 二进制协议,解析起来更高效
- 还是基于TCP,没有解决队头阻塞问题。
- 握手开销没有优化。
QUIC:
- 基于UDP实现
- 解决队头阻塞
- 加密减少握手次数
- 支持快速启动
HTTP框架的设计与实现
分层设计
OSI七层模型:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)
三大特点:专注性、扩展性、复用性
高内聚、低耦合、易复用、高扩展性
应用层:与用户打交道,对请求进行抽象,提供丰富的应用API。
中间件层:预处理和互处理的逻辑
路由层:路径映射等
协议层:支持Websocket、HTTP1、HTTP2、QUIC协议。
网络层:灵活提供网络库的能力。netpoll、go ret
然后就是一些通用的模块
应用层设计
提供合理的API:
- 可理解性。如ctx.Body()或者ctx.GetBody(),而不是ctx.BodyA()
- 简单性。一些高频接口就不要像Java一样搞很多API了。
- 如ctx.Request.Header.Peek(key),就不如ctx.GetHeader(key)
- 冗余性。做同样的事情不需要两个接口,不要有一个接口是由其它接口拼接而成
- 兼容性。
- 可测性。
- 可见性:不能让用户拿到框架很核心的东西。
老师的工作感受:不要试图在文档中说明,很多用户不看文档。
中间件层
中间件可以将核心逻辑和业务逻辑分离。
中间件需求:
- 配合Handler实现一个完整的请求处理生命周期。
- 拥有预处理逻辑与后处理逻辑。
- 可以注册多中间件。
- 对上层模块用户逻辑模块易用。
洋葱模型: 适用场景:日志记录、性能统计、安全控制、事务处理、异常处理
例如,打印每个请求的request和response。
- 既然要实现预处理的和后期处理,那就很像调用一个函数。
- 路由上可以注册多个中间件,同时也可以满足请求级别有效,只需要将中间件设计为和业务和Handler相同即可。统一为Next函数。
- 如果用户不主动调用下一个处理函数?
- 帮用户主动调用,内部封装一个index,核心是:在任何场景下index保持递增。类似下面的代码
- 出现异常想停止怎么办?index = maxIndex,跳出循环即可
调用链:
适用场景: 不调用Next的情况:初始化逻辑并且不需要在同一调用栈。 调用Next的情况:后处理逻辑或者需要在同一个调用栈上。
路由设计
框架路由实际上就是为URL匹配对应的Handlers。 功能大概可以分为:
- 静态路由:
/a/b/c - 参数路由:
/a/:id/c - 路由修复:
/a/b -> /a/b/ - 冲突路由以及优先级:
/a/b/c和/a/:id/c - 匹配HTTP方法
- 多处理函数:方便添加中间件
具体怎么设计呢?
- 青铜:Map存储。
- 只能支持静态路由,不支持通配符。
- 优点是快。
- 黄金:前缀匹配树。
如何匹配HTTP方法: 路由映射表:K为String类型的Method,V为前缀树
如何实现添加多处理函数: 每个节点用一个list存储handler。
如何做设计?
- 明确需求:考虑清楚要解决什么问题,有哪些需求 。
- 业界调研:业界都有哪些解决方案可以参考。
- 方案权衡:思考不同方案的取舍。
- 方案评审:相关同学对不同方案做评审。
- 确定开发:确定最合适的方案进行开发。
协议层
抽象出合适的接口:
网络层
BIO:阻塞IO。占线,卡住。
NIO:注册一个监听器,监听器监听到足够的数据之后,再去进行唤醒func,
go net 是BIO,需要用户管理buffer。 netpoll 字节公司内部自研库,是NIO,网络库管理buffer。
让框架提高性能
针对网络库的优化
go net
需求: 存下全部Header,Header有大有小 减少系统调用次数,系统调用涉及内核态和用户态的切换,开销比较大 能够复用内存 能够多次读
对此,我们可以绑定一块缓冲区:go net with bufio, Peek接口:读的时候让指针不动,下次还能在这里读。 Discard接口:让指针移动的接口。 Release接口:回收内存。
netpoll
存下全部Header 拷贝出完整的Body
netpoll with nocopy peek 分配足够大的buffer 限制最大buffer size
不同网络库的优势
go net
- 流式友好
- 小包性能高
netpoll
- 中大包性能高
- 时延低
针对协议的优化--Headers解析
Header Line边界:\r\n。KMP和BM算法针对的是一般的字符串以及很极端的字符串,针对HTTP这种有特征的数据:先找到\n,再看看其前面是否有\r。
能不能更快呢?SIMD(计算机体系结构这门课的内容,跟汇编有关)
SIMD技术加速后,找到\n的速度大幅提升。
针对协议相关的Headers快速解析:
- 通过Header key首字母快速筛掉完全不可能的key
- 解析对应的value到独立字段
- 使用byte slice管理对应Header存储,方便复用。
取舍:
- 取:核心字段快速解析、使用byte slice存储、额外存储到成员变量中。
- 舍:普通header性能较低、没有map结构
针对协议的优化-Header key规范化
将aaa-bbb改成Aaa-Bbb
取舍:
- 取:超快的转换效率、比net.http提高40倍
- 舍:额外的内存开销、变更困难
热点资源池化
取舍:
- 取:减少内存分配、提高内存复用、降低GC压力、性能提升
- 舍:额外的Reset逻辑、请求内有效、问题定位难度增加
企业实践
- 追求性能
- 追求易用,减少误用:这与性能有矛盾,例如池化会导致生命周期的问题。
- 打通内部生态:有时候为了追求优化,会割裂社区的生态和内部的生态。
- 文档建设、用户群建设——将常见问题沉淀下来。
字节内部Http框架:Hertz。1w+服务,3kw+QPS
总结
- Http协议
- Http框架设计
- Http框架优化
- 企业实践