HTTP框架与实现|青训营笔记

136 阅读6分钟

这是我参与「第三届青训营-后端场」笔记创作活动的第14篇笔记。

1. HTTP协议

image (3).png 1.1 HTTP是什么

HTTP(Hypertext Transfer Protocol)超文本传输协议。超文本就是除了文本以外还能传输图片、音乐、视频的数据。

1.2 协议里面有什么

  1. 需要明确的边界确定协议的开始与结束。
  2. 需要包含消息类型(元数据)、消息内容

http请求包括:

  • 请求行:请求描述(方法名、URL)、版本描述
  • 请求头:元数据(包含body长度)
  • 请求体:body

http应答包括:

  • 状态行:版本描述、状态码+对应状态
  • 响应头:元数据
  • 响应体:回复信息

常见方法名有:GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、ARACE、PATCH等

PUT全局更新,PATCH局部更新 PUT是幂等的,PATCH不是幂等的

1.3 请求流程

  1. 业务层:使用api完成业务逻辑
  2. 服务治理层(依托于中间件层):相当与修饰器
  3. 路由层:仅在服务端需要考虑
  4. 协议编(解)码层
  5. 传输层

1.4 不足与展望

  • http1:(TCP)对头阻塞、传输效率低(报头字段太多)、明文传输不安全、不支持多路复用
  • http2:支持多路复用、利用头部压缩(把重复的头部在两端存储以减少需要传输的头部大小)、使用二进制协议,队头阻塞、TLS握手开销较大的问题未解决。
  • quic:基于UDP实现,解决对头阻塞,加密减少握手次数,支持快速启动

2. HTTP框架的设计与实现

网络分层架构带来了专注性(每个团队只需考虑自己层要做的事)、扩展性(协议更新不用全局改造,而是只改变特定的层)、复用性(可以单独抽出某一层使用)

HTTP内部也是分层的,这使HTTP的结构高内聚、低耦合。易复用。具有高扩展性。

image.png

从上到下分为5层。

  1. 应用层:与用户直接打交道,对请求进行抽象,提供api。
  2. 中间件层:预处理。
  3. 路由层:注册、寻址等。
  4. 协议层:支持不同版本的协议。只要实现对应的抽象接口就可以实现扩展。
  5. 网络库层

common存放公共逻辑,每一层都会使用。

应用层设计

提供合理的API:

  • 可理解性:使用主流意见,让接口命名容易理解
  • 简单性:尽量不形成接口链
  • 冗余性:不要提供功能相同的接口
  • 兼容性:接口能随版本一直使用
  • 可测性:接口可以测试
  • 可见性:将接口暴露,核心代码透明

不要试图在文档中说明,很多用户不看文档

2.3 中间件设计

中间件需求:

  • 配合handler实现一个完整的请求处理生命周期
  • 拥有预处理逻辑与后处理逻辑
  • 可以注册多个中间件
  • 对上层模块用户逻辑易用

洋葱模型(起装饰器作用): 核心逻辑与通用逻辑分离。例如请求进入日志层、监控层、核心层。在核心层处理后,逐步经过监控层(后处理)、日志层(后处理)最后形成响应传出。

适用场景:

  • 日志记录
  • 性能统计
  • 安全控制
  • 事物处理
  • 异常处理

调用链:

中间件自增编号。每个函数有执行到第k个中间件的标识。调用完中间件后调用Next函数将k加1。如果不调用后续中间件,直接将k赋值为最大。

image (1).png

适用场景:

  • 不调用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方法(GET、POST......)
  • 多处理函数
  • ........

version1: map[string]handlers 。使用哈希表实现路由映射。实现简单,但不支持参数路由

version2:前缀匹配树

如何匹配HTTP方法?哈希表。不同方法对应不同前缀树。有的方法不需要路由则无前缀树。

如何实现添加多处理函数?每个节点上使用一个list存储handler。

如何做设计?

  1. 明确需求:考虑清楚问题与需求
  2. 业界调研:参考已有的解决方法
  3. 方案权衡
  4. 方案评审
  5. 确定开发:确定最合适的方案进行开发

2.5 协议层设计

抽象出合适的接口:

  1. 不要将contexts写在函数体里,而是作为第一个参数显示的传入(golang的官方推荐)。
  2. 需要在连接上读写数据。

2.6 网络层设计

BIO:阻塞型IO,go net是BIO,需要用户管理buffer。

NIO:注册监听器,有足够的写入时再执行读取。netpoll是NIO,网络库管理buffer。

3.性能如何提升

3.1 针对网络库的优化

go net:存下全部Header,系统调用次数多,不能复用内存,不能多次读(header过大,一次读不完)

go net with bufio:对每一个连接绑定一块缓冲区

netpoll:存下全部Header,拷贝出完整的Body

netpoll with nocopy peek:分配足够大的buffer(根据历史值计算),限制最大buffer size。

  • go net对于流式友好,小包性能高
  • netpoll:由底层进行buffer管理,所以中大包性能高,时延低

3.2 针对协议的优化

找到Header Line边界:\r\n

可以先找到\n再看前一个是不是\r

能不能更快?SIMD,单指令级多数据技术,一次匹配多个字符

针对协议相关的Headers快速解析:

  1. 通过Header key首字母快速筛选除掉完全不可能的key(本来需要读全字符串进行精确匹配,此处先用首字母初筛,然后对剩余部分精确匹配)
  2. 解析对应value到独立字段,高频属性不放在流中,而是独立字段
  3. 使用byte slice管理对应header存储,方便内存复用

请求体中同样处理的key:User-Agent、Content-Type、Connection、Transfer-Encoding

  • 优点:核心字段快速解析。使用byte slice存储。额外存储到成员变量中。
  • 缺点:普通header性能较低。没有map结构。

3.3 Header Key规范化

文本映射:aaa-bbb -> Aaa-Bbb

使用表映射的方法,比go net计算ASC码快40倍。但需要存两张表,额外的内存开销。表的变更存在困难(一般表不会变更)。

3.4 热点资源池化

  • 优点:减少内存分配,提高内存复用,降低GC压力,提升性能
  • 缺点:额外的reset逻辑(放回池子前需要将里面的值reset)。请求内有效。问题定位难度增加。

4. 企业实践

追求性能-->追求易用、减少误用-->打通内部生态(提高代码复用)--->文档建设、用户群建设