HTTP相关笔记 | 青训营

56 阅读4分钟

1 HTTP

前后端分离流程图

image-20230808161227328.png

1.1 再谈 HTTP 协议

1.1.1 HTTP 协议是什么

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

1.1.2 协议里有什么

  • 请求行

    • 方法名
      • 常见方法名
      • GET
      • HEAD
      • POST
      • PUT
      • DELETE
      • CONNECT
      • OPTIONS
      • TRACE
      • PATCH
    • URL
    • 协议版本
  • 状态行

    • 协议版本

    • 状态码

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

  • 请求头/响应头

    • 协议约定

    • 业务相关

  • 请求体/响应体

// 一个demo:post请求
//POST /sis HTTP/1.1
//Who: Alex
//Content-Type: text/plain 
//Host: 127.0.0.1:8888
//Content-Length: 28
//
//Let's watch a movie together

//ΗΤΤΡ/1.1 200 OΚ
//Server: hertz 
//Date: Thu, 21 Apr 2022 11:46:32 GMT 
//Content-Type: text/plain; charset=utf-8
//Content-Length: 2
//Upstream-Caught: 1650541592984580
//
//OK

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()
}

1.1.3 一次请求的完整流程

image-20230808164430957.png

1.1.4 不足与展望

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

1.2 HTTP 框架的设计与实现

1.2.1 分层设计

image-20230809145900080.png

image-20230809150023966.png

1.2.2 应用层设计

提供合理的 API

  • 可理解性:如 ctx.Body(),ctx.GetBody(),不要用 ctx.BodyA()

  • 简单性:如ctx.Request.Header.Peek(key)/ctx.GetHeader(key)

  • 冗余性:相同功能不同名称的两个接口是冗余的,两个接口一起完成一个功能也会有冗余

  • 兼容性:开源时间久了之后所有的接口都会被用到,随意删除一个接口会造成麻烦

  • 可测性

  • 可见性

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

1.2.3 中间件层设计

中间件需求:

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

image-20230809151014339.png

  • 既然要实现预处理和后处理,那这个就很像调用了一个函数
func Middleware(some param) {
    //some logic for pre-handle
    nextMiddleware() / bizLogic() ==> Next()
	//some logic after-handle
}
  • 路由上可以注册多Middleware,同时也可以满足请求级别有效,只需要将 Middleware设计为和业务和Handler 相同即可。
  • 用户如果不主动调用下一个处理函数怎么办?
func (ctx *RequestContext) Next() {
    ctx.index++
    for ctx.index < int8(len(ctx.handlers)) {
        ctx.handlers[ctx.index]()
        ctx.index++
    }
}
//核心:在任何场景下index保证递增
  • 出现异常想停止怎么办?
func (ctx *RequestContext) Abort() {
    ctx.index = IndexMax
}

image-20230809152050956.png

1.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 方法

  • 多处理函数:方便添加中间件

image-20230809152502399.png

image-20230809152517825.png

image-20230809152655783.png

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

node struct {
    prefix string
    parent *node 
    children children 
    handlers app.HandlersChain
    ...
}

如何做设计

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

1.2.5 协议层设计

抽象出合适的接口

type Server interface {
    Serve(c context.Context, conn network.Conn) error
}
//不要将Context存储在结构类型中;相反,将Context显式地传递给需要它的每个函数。Context应该是第一个参数。
//需要在连接上读写数据

1.2.6 网络层设计

BIO会堵塞,NIO使用监听器避免了阻塞

image-20230809153316334.png

image-20230809153511483.png

image-20230809153611141.png

type Conn interface {
    net.Conn
    Reader
    Writer
}

API 设计:可理解性、简单性...

中间件设计:洋葱模型

路由设计:前缀匹配树

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

网络层设计:网络模型

1.3 性能修炼之道

1.3.1 针对网络库的优化-buffer设计

image-20230809154025060.png

image-20230809154215785.png

不同网络库的优势

go netnetpoll
流式友好中大包性能高
小包性能高时延低

1.3.2 针对协议的优化

Headers解析

image-20230809155052702.png

image-20230809155234997.png

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

Headers key规范化

image-20230809155551230.png

超高的转换效率额外的内存开销
比net.http提高40倍变更困难

热点资源池化

image-20230809160049642.png

image-20230809160112347.png

减少了内存分配额外的 Reset 逻辑
提高了内存复用请求内有效
降低了GC压力问题定位难度增加
性能提升

1.4 企业实践

在工作中的一些感悟:

  • 追求性能
  • 追求易用,减少误用
  • 打通内部生态
  • 文档建设、用户群建设

内部HTTP框架:Hertz

1万+服务 3千万+QPS