HTTP协议与框架 | 青训营

45 阅读8分钟

HTTP框架

HTTP协议

介绍

  1. HTTP(Hypertext Transfer Protocol):超文本传输协议
  2. HTTP协议能够支持传输图片、mp3、avi等格式文件
  3. 协议产生原因:
    • 信息在网络上传输时,需要明确的边界标明信息的开始和结束
    • 还需标注信息以何种方式传播,以何种类型显示

内容

  1. HTTP通常会规定以下几种内容

image.png

请求流程

  1. 请求在客户端和服务端之间的请求流程(从外向内)

image.png

不足&展望

  1. HTTP1
    • 队头阻塞
    • 传输效率低
    • 明文传输不安全
  2. HTTP2
    • 多路复用
    • 头部压缩
    • 二进制协议
  3. QUIC
    • 基于UDP实现
    • 解决队头阻塞
    • 加密减少握手次数
    • 支持快速启动

HTTP框架的设计与实现

分层设计

网络模型与协议

  1. OSI七层网络模型——理想下的网络分层,属于研究模型
  2. 五层网络模型——较为精简的网络分层,利用功能分层,属于教学模型
  3. TCP/IP四层网络模型——通用的商业网络模型,是性价比最高的实践产品

如下图是对应网络层和协议类型(专注性、扩展性、复用性)
image.png

请求在网络中的传输

  1. 过程大致如下

image.png

  1. 如上分层处理的优点:高内聚、低耦合、易复用、高扩展性

应用层

  1. 提供合理的API
    • 简单易理解
    • 冗余性(低)
    • 兼容性(强)
    • 可测性(可测试)
    • 可见性(出于安全性考虑,接口对于用户来说是不可见的)

中间件

  1. 需求:
    • 配合 Handler 实现一个完整的请求处理生命周期
    • 拥有预处理逻辑与后处理逻辑
    • 可以注册多中间件
    • 对上层模块用户逻辑模块易用
  2. 经典模型——洋葱模型

image.png

  1. 中间件处理流程

  1. 路由上可以注册多个 Middleware ,同时也可以满足请求级别有效,只需要将 Middleware 设计为和业务、Handler 相同即可。
  2. next()控制下一处理函数的调用:使用index控制,当index等于ctx的handler数量,则退出next()执行,否则按顺序执行,直到执行完毕代码类似如下
func (ctx *RequestContext) Next(){
    ctx.index++
    for ctx.index < int8(len(ctx.handlers)){
        ctx.handlers[ctx.index]()
        ctx.index++
    }
}
  1. 中间件执行next()时的异常终止设计:将ctx的index设为最大值(ctx的handler数量),使其退出,代码类似如下
func (ctx *RequestContext) Abort(){
    ctx.index = IndexMax
}
  1. 中间件调用链注意事项
    • 仅做初始化逻辑操作,且不需要在同一调用栈上(不存在嵌套调用关系)——不调用Next()
    • 后处理逻辑或需要在同一调用栈上——调用Next

路由

  1. 框架路由实际上就是为 URL 匹配对应的处理函数(对应Server端的Controller层函数)
  2. 路由的划分:
    1. 静态路由(明确,精准匹配):juejin.cn/course/byte…
    2. 参数路由(不明确,模糊匹配):
  3. 路由修复:路由修复就是会为用户补齐url最后的/,比如用户输入 juejin.cn ,路由修复将自动转为juejin.cn/
  4. 路由设计
    1. 路由匹配设计
    • 使用map存储handlers的路由——缺点:只适用于静态路由,无法用于参数路由
    • 使用前缀匹配树

屏幕截图 2023-08-06 154735.png

  1. 路由与方法匹配
  • 路由映射表(k——method,v——前缀树)根据方法找前缀树,若不存在则v为空

image.png

  1. 添加多处理函数
  • list保存handler

协议层

  1. 抽象出合理接口
type Server interface{
    Serve(c context.Context, conn network.Conn) error
}
  1. 分析上述设定
    • 上下文参数应该作为显式的参数供需要的函数传递,通常作为函数第一个参数传入
    • 协议需要在连接上读写数据,因此连接参数很有必要
    • 在读写数据等一系列操作中,可能会出现一些错误,此时需要将其”抛出“

网络层

  1. BIO:阻塞IO,如下,连接先读取请求,然后处理业务,最后写回响应,这一连串的操作都是线性的,如果读数据的操作阻塞整个连接都会阻塞
go func(){
    for {
        conn,_ := listener.Accept()
        go func() {
            conn.Read(request)
            handler()
            conn.Write(response)
        }
    }
}
  1. NIO:较于BIO,多了一个监听操作,监听到足够多的数据才会进行一系列读取请求,然后处理业务,最后写回响应的处理,这样连接在读数据时就不会发生阻塞
go func(){
    for {
        //注册监听器,监听读取数据
        readableConn,_ := Monitor(conns)
        for conn := range readableConn {
            go func() {
            	conn.Read(request)
            	handler()
            	conn.Write(response)
        	}
        }
    }
}
  1. 相关数据结构

    `go net`——BIO    netpoll——NIO
    
    1. BIO——Conn 字节流进,数据长度+err出
type Conn interface {
    Read(b []byte)(n int, err error)
    Write(b []byte)(n int, err error)
    ...
}
  1. NIO——Conn
type Conn interface {
    net.Conn
    Reader
    Writer
}
type Reader interface {
    Peek(n int)(buf []byte, err error)//监听到足够数量的数据才会读入
    ...
}
type Writer interface {
    Malloc(n int)(buf []byte, err error)//底层开辟一个大小为n的空间,把数据写入
    Flush() error//发送error
    ...
}

设计要求

  1. 明确需求:考虑清楚要解决什么问题、有哪些需求
  2. 业界调研:业界都有哪些解决方案可供参考
  3. 方案权衡:思考不同方案的取舍
  4. 方案评审相关同学堆不同方案做评审
  5. 确定开发:确定最适合的方案进行开发

性能修炼之道

针对网络库的优化

go net(BIO)

需求

  1. 能够存下全部 Header(内存空间),Header长度可变,需要保存所有Header才好解析
  2. 减少系统调用次数,系统态/用户态的切换有较大开销
  3. 能够复用内存
  4. 能够多次读,如果解析失败,可以支持多次重新解析请求

优化

  1. 绑定一块缓冲区(据调查,大小通常为4k最佳)
type Reader interface {
    Peek(n int)([]byte, err error)//该方法用来保存读取数据时的指针
    Discard(n int)(discarded int, err error)//该方法用来让指针前移
    Release() error//该方法用来释放绑定的缓冲区内存
    Size() int
    Read(b []byte)(l int, err error)
    ...
}
type Writer interface {
    //拆分  Write(b []byte)(l int, err error)
    Write(b []byte)
    Size() int
    Flush() error//发送error
    ...
}

netpoll(NIO)

需求

  1. 能够存下全部 Header(内存空间),Header长度可变,需要保存所有Header才好解析
  2. 可以拷贝出完整的 Body

优化

  1. 分配足够大的 buffer ,达到满足请求的 Header 和 Body 都在一个buffer里面,并限制最大 buffer size,然后可以直接拷贝

不同网络库的优势

BIO:流式友好,小包性能高
NIO:中大包性能高,时延低

针对协议的优化

Headers解析

第一步:找到 Header Line 的边界——\r\n

  1. 通常情况下,先找到 \n ,然后再看它前一个是否为 \r

缺点:非常慢

  1. 优化一:使用SIMD技术,并行操作找边界,加快解析速度

第二步: 快速解析 Headers

  1. 步骤
    1. 通过 Header key 首字母快速筛除掉完全不可能的 key
    2. 常用高频Header,各开辟单独的空间。解析对应 value 到独立字段
    3. 使用 byte slice 管理对应 header 存储,方便复用
  2. 优缺点
    1. 核心字段快速解析

    2. 使用 byte slice 存储header,方便复用

    3. 对应核心字段的value会额外存储到成员变量中

    4. 对于普通的header 来说,性能较低(高频header开辟空间,普通header没有)

    5. 没有map结构(用户使用不那么简单)

Header key 规范化

  1. Header key的规范是Xxx-Yyy
  2. Header key的规范化:
    1. 用两张表分别存储所有Header key的大写和小写对应ASCII码
    2. 将得到的Header key转成ASCII,然后 在表中查找,找到即返回
  3. 优缺点:
    1. 超高的转换效率

    2. 比 net.http 提高了40倍(使用+/-转化为大写)

    3. 额外的内存开销(两张表)

    4. 变更困难(两张表及底层的匹配都是按照ASCII码表设计的,如果该表弃用/修改,则需要极大变更,但是ASCII码表变更的可能性极小)

热点资源池化

  1. 请求中的热点资源——RequestContext(每个请求和响应都需要,与请求一一对应,贯穿一个请求始终)
  2. 传统情况下,一次请求需要创建一次RequestContext,响应后需要关闭,如下。每次都一开一关,非常浪费该资源。

image.png

  1. 优化——创建一个RequestContext资源池,里面创建一定量的RequestContext,每有一个请求则从资源池中取一个RequestContext,用完将RequestContext放回资源池。

image.png

  1. 优缺点:
    1. 减少了内存分配,提高了内存复用(传统情况,每次创建RequestContext都要分配一次内存)

    2. 降低GC压力(每次关闭RequestContext,要调用GC回收内存)

    3. 性能提升

    4. 额外的 Reset 逻辑(RequestContext被上一个请求使用后放回,需要做一些reset操作,否则,上一请求的脏数据可能会影响到下一请求的使用)

    5. 仅在请求内有效(资源池中申请RequestContext是有时限的,如果超出时限,RequestContext就不再可靠)

    6. 问题定位难度增加(增加资源池后,RequestContext的获取、放回和重置都是额外增加的逻辑,都存在出问题的可能,再加上高并发场景下多个RequestContext不停地被复用,问题产生的概率增加,且难以定位)

企业实践

要求

  1. 追求易用,减少误用
  2. 追求性能
  3. 打通内部生态
  4. 文档建设、用户群建设

使用

  1. 内部 HTTP 框架:Hertz
  2. 1万+服务 3千万+QPS

总结

本次课主要内容有大半是学过的HTTP协议,这点很友好,涉及到网络架构/层级用图来说明最清楚,因此本次笔记图片较多。另外网络库的拓展是之前没有接触过的,加上字节都在自研网络库(netpoll),所以网络库的学习也很重要,需要结合理论去分析。