走进 HTTP 协议笔记 | 青训营笔记

103 阅读7分钟

这是我在字节跳动后端青训营的第五篇笔记,包含走进HTTP协议部分的内容。

HTTP 协议

前后端分离

前后端通过 HTTP 进行通讯

HTTP框架负责HTTP请求的解析,根据对应的路由选择对应的后端逻辑

再谈 HTTP 协议

最早大规模使用 HTTP 0.9 从 1991 年开始使用

现在还有更新的版本,如 HTTP/2 和 Quick

HTTP: Hypertext Transfer Protocol 超文本传输协议

超文本,超在哪里?

两台电脑通过网络传输 文本,但是文本没有办法满足传输需求,图片视频超链接,对text 进行了扩充,所以成了超文本。对应的传输协议就是 超文本传输协议

1.1 为什么需要协议

遵照一定的规则才能让传输的信息被理解

协议需要明确的边界,知道什么时候开始,什么收结束

需要能够携带信息,也就是协议元数据,从而知道消息的类型。

一个POST请求究竟在协议层做了什么?

1.2 协议里面有什么

image.png

看图片:

协议第一行 POST 接 URL 空格 HTTP版本 也就是请求行

四行是 键值对,是协议的元数据。 空行之后是body 然后换行符,协议结束。

检测到 请求行,就开始接收协议了,协议的结束时是 body(消息体) 和换行符

元数据也包含了对内容长度的描述,Server可以根据Content-length决定接收多少字节

来自Server的响应包括

HTTP版本 状态码 状态描述 metadata

所以HTTP协议包含的内容有:

请求行/状态行: 请求行包含:

  • 方法名
  • URL
  • 协议版本

状态行包含:

  • 协议版本
  • 状态码
  • 状态码描述

请求头/响应头

请求体/响应体

常见方法名

  • GET
  • HEAD
  • POST
  • PUT 完整更新,幂等
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH 部分更新,不是幂等的

状态码包括: 1xx:信息类 2xx:成功 3xx:重定向 4xx:客户端错误 5xx:服务端错误

实现简单的 HTTP 响应:

package main

import (
    "context"
    
    "code.byted.org/middleware/hertz/pkg/app"
    "code.byted.org/middleware/hertz/pkg/app/server"
)

func main() {
    h := server.New()
    
    h.POST("/sis", func(c context.Context, ctx *app.RequestContext) {
        ctx.Data(200, "text/plain; charset=utf-8", []byte("OK"))
    })
    h.Spin()
}

背后的处理流程:

image.png

业务层:业务方使用框架的API,完成逻辑 完成业务逻辑之后,进入服务治理逻辑。 服务治理层:依托于中间件层,对每个请求可以有一些先后处理的逻辑,比如,在进入业务层之前即使,业务层执行完毕后再次即使,进行上报,就可以知道整个业务逻辑的耗时。 对于Client,之后会进入协议的编解码。根据协议内容编译成Server能理解的协议,之后通过传输层传输给Server。

Server会多一个路由曾,根据 URI 选择对应的 handler,也就是选择对应的服务。

HTTP1:

  • 基于 TCP,存在队头阻塞问题,后续分片必须等待前面分片的到来才能继续发送后面的数据,否则就会一直等待
  • 传输效率低,哪怕只发送一句话,也要附带大量额外消息
  • 不支持多路复用,在上一个请求结束前,不支持在发送其它请求
  • 明文传输,不安全

HTTP2:

  • 支持多路复用
  • 头部压缩,把重复的 header 缓存起来,减少重复数据发送
  • 二进制协议,解析更高效
  • 还是基于TCP,没有解决队头阻塞问题。
  • 为了加密使用TLS,握手的开销没有优化

QUIC 基于UDP,解决TCP队头阻塞 加密算法,减少握手次数 支持快速启动

2.1 分层设计

简化了系统的设计,让不同的人专注于某一层的事情。 HTTP框架聚焦于应用层

分层设计还可以提高扩展性和复用性

HTTP 框架的设计也应该采用分层设计 需要考虑高内聚,低耦合,复用性和扩展性。

框架分为 5 层 之间使用接口解耦

应用层直接和用户打交道,会对请求进行一些抽象

中间件层对用户有一些预处理和后处理的逻辑

路由曾提供路由来提供类似注册,寻址相关的操作

协议层:支持特定的网络协议,包括 HTTP2 Quic

传输层: 提供灵活替换网络库的能力

Common:存放公共库。

盖尔定律:切实可行的复杂系统从切实可行的简单系统发展而来

2,2 应用层设计:

提供合理的 API

  • 可理解性: 如 ctx.Body() ctx.GetBody(),不要用 ctx.BodyA()
  • 简单些:如 ctx.Request.Header.Peek(key) / ctx.getHeader(key),减少用户的工作量
  • 冗余性 ctx.Body 和 ctx.GetBody() 可以做一样的是,不要有两个功能重复的接口
  • 兼容性 接口实现不能轻易改变或弃用
  • 可测性 可测试
  • 可见性 为了安全性,不要暴露随便框架的核心,但对于能力强的用户,也要能够找到该接口如何使用

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

2.3 中间件设计

中间件需求:

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

洋葱模型:

image.png

有请求过来了,通过日志中间件预处理,然后执行业务逻辑,业务逻辑退出,日志进行后处理,再将真正响应返回给用户。

中间件的合金就是它可以将核心逻辑于通用逻辑分离

适用场景包括:

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

具体例子: 打印每个请求的request和response

没有中间件:需要在每个接口开始和结束分别加上打印内容。 有了中间件,只要添加一个中间件就行了。

  1. 预处理和后处理,很像调用函数,可以统一为一个函数 Next(),不需要区分到底是中间件还是业务逻辑,统一为直接调用下一个处理函数。只需要一个同样的函数签名就可以写中间件。
  2. 路由上可以注册多 Middleware,同时也可以满足请求级别有效。只需要将 Middleware 设计为和业务,和 Handler相同即可
  3. 用户如果不主动调用下一个处理函数怎么办?可以帮用户主动去调用之后的中间件,核心是任何场景下 index 保持递增
func (ctx * RequestContext) Next() {
    ctx.index++
    for ctx.index < int8(len(ctx.handlers)) {
        ctx.handlers[ctx.index]()
        ctx.index++
    }
}       
  1. 出现异常想停止怎么办?将index设为最大值,就跳出了循环
func (ctx *RequestContext) Abort() {
    ctx.index = IndexMax
}

调用链:

image.png

有没有什么坑?

对于 Go,它们不再一个调用栈上。

比如 recovery 中间件,只能捕获本携程或者说本调用栈的 handler

适用场景:

  • 不调用 Next: 初始化逻辑且不需要在统一调用栈
  • 调用 Next: 后处理逻辑或者需要在同一调用栈上

2.4 路由设计 框架路由实际上就是为 URL 匹配对于的处理函数 (Handlers)

  • 静态路由 /a/b/c /a/b/d
  • 参数路由 /a/:id/c (/a/b/c, /a/d/c) /*all
  • 路由修复 /a/b <-> /a/b
  • 冲突路由以及优先级: /a/b /:id/c
  • 匹配 HTTP 方法
  • 多处理函数:方便添加中间件
  • ...

例如:

xuexi.cn 是一个页面
xuexi.cn/cc72a........一长串 是另一个页面

这就是一个路由

路由可以根据 URL 选择不同的页面,这个页面可以是一个前端路由,也可以由后端直接把页面返回给前端

路由设计: 青铜:map[string]handlers

很快,很简单,但只对静态路由有效

黄金: 前缀匹配树

对参数路由怎么处理? 对匹配树进行改进,对冒号进行匹配,然后添加fullpath

如何匹配 HTTP 方法:

可以构造很多课路由树,外层直接是一个 Map,根据 Method 进行初步筛选。

如何实现多处理函数: 在每个节点上使用 List处理 Handler

2.5 协议层设计: 抽象出合适的接口:

  1. 根据Golang官方推荐,不要把COntext 写到 Struct 里面,而是显式通过函数的第一个参数传递
  2. 需要在连接上读写数据,需要把连接也传递出来
  3. 如果有 error 需要抛给上层

2.6 网络层设计

BIO 和 NIO

BIO:Block IO:每次接收一个连接回开一个go routing,数据读取完了再response,如果数据都一般,就会卡在这里 NIO:注册一个监听器,监听到有足够数据,再唤醒 func,没有阻塞

go 的标准库 go net 是典型的 BIO,由用户管理 Buffer

如果底层没有数据,还要调用接口,就会卡在这里。

netpoll 是字节自研的网络库,采用NIO编程模式,由网络库管理 Buffer