HTTP框架修炼之道 | 青训营

35 阅读5分钟

一、HTTP协议是什么

HTTP协议(超文本传输协议)是最广泛使用的协议之一,它是text文本类型资源的扩充。

image.png

1、为什么需要协议

协议是交互双方的约定规则,网络上传输的是01数据流,因此协议首先需要明确信息的边界,即明确信息什么时候开始与结束,其次需要元数据对信息的描述,明确消息的类型、大小等。

image.png

2、协议内容

包括请求行、请求头字段和请求主体。

image.png

3、协议里有什么

  • PUT和PATCH的区别:PUT完整更新,因此PUT幂等;PATCH部分更新,因此PATCH非幂等
  • 状态行:协议版本、状态码、状态码描述

image.png

4、请求流程

完整的请求流程包括:

  • 业务层
    业务相关的逻辑.
  • 服务治理层&中间层
    中间层是常说的熔断、限流等,而服务治理层依托于中间层,它对每个请求可有先处理逻辑和后处理逻辑,是和请求级别绑定的。
  • 路由层
    对客户端来说,经过业务层、服务治理层&中间层两层就进入协议编(解)码层,对协议编码和解析,最后通过传输层完成传输;而对服务器来说,要经过多个路由层,根据URL选择对应执行的handler。
  • 协议编(解)码层
  • 传输层

image.png

5、不足与展望

  • HTTP1基于TCP,TCP有队头阻塞的问题,即后续分片须等待前面的分片的到来才继续发后面的数据,否则将会一直等待;
  • HTTP1传输效率很低,比如只传输一个字节,传输的无效信息非常多,存在很多头部信息;
  • HTTP1不支持多路复用,即一个请求没结束前不能再发送其他请求;
  • HTTP1明文传输不安全;
  • HTTP2可以多路复用,但仍基于TCP,未解决队头阻塞,且握手开销也未优化;
  • QUIC基于UDP,但是应用不广泛

image.png

二、HTTP框架的设计与实现

1、分层设计

  • 分层设计可以简化系统设计,让不同层专注做某一层次的事情;
  • 只需通过接口,专注特定层开发即可,不需关注底层实现;
  • 分层更容易横向扩展;
  • 分层可做到很高的复用

image.png

考虑高内聚低耦合、易复用、高扩展性,http框架设计也应分层设计:

  • 应用层Application:直接跟用户打交道,对请求抽象,包括request response context等,会提供丰富易用API;
  • 中间层middleware:对请求有预处理和后处理的逻辑,如accesslog、recovery中间件捕获panic等;
  • 路由层route:实现类似注册、路由寻址的操作;
  • 协议层codec:Websocket、HTTP2、QUIC协议的支持;
  • 网络层transport
  • 公共逻辑层common

image.png

(1)应用层

不要试图在文档中说明,因为很多用户不看文档,因此需要在应用层序提供合理的API,包括:

  • 可解释性:使用主流的概念方便理解,如ctx.GetBody()或ctx.Body()而不是ctx.BodyA()
  • 简单性:常用API放到上层,易误用/低频AP放下层
  • 可见性:最小暴露原则,不需暴露API就不暴露,可抽象为接口
  • 冗余性:不需要冗余或能通过其他API组合得到的API
  • 兼容性:尽量避免break change做好版本管理

(2)中间层

需配合handler实现完整请求处理生命周期,有预处理和后处理逻辑,可注册多中间件,对上层模块易用。

常见模型:洋葱模型,核心是将核心逻辑与通用逻辑分离

image.png

先经过日志中间件预处理,再经过metrics中间件预处理,之后进行真正业务逻辑;

最后退出业务逻辑得到后处理,先经过metric中间件后处理,其次经过日志中间件后处理,再将响应返回给用户。

适用场景包括:日志记录、性能统计、安全控制、事务处理、异常处理等

func Middleware(some param){
     //some logic for pre-handle
     ...
     Next()
     //some logic for after-handle
     ...
 }

中间件调用有点像函数调用,同时也可满足请求级别有效,只需将Middleware设计为业务和Handler相同即可,就不用区分是中间件还是业务逻辑,统一为直接调用下一个处理函数,抽象为Next()方法,对服务治理易用。

用户如果不主动调用下一个处理函数怎么办?核心:在任何场景下index保证递增。

func (ctx *RequestContext) Next(){
     ctx.index++
     for ctx.index < int8(len(ctx.handlers)){
         ctx.handlers[ctx.index]()
         ctx.index++
     }
}

中间件调用链:

image.png

(3)路由设计

路由实际是为URL匹配的处理函数,包括静态路由和动态路由:

  • 对于静态路由可使用map,key是URL,value是其handler
  • 动态路由则需前缀树,每个节点用list存储handler

image.png

(4)协议层设计

抽象出合适的接口,实现Serve的接口,传入标准context(注意不要将context存储在结构体)和读写的连接,返回error。

type Server interface {
     Serve(c context.Context,conn network.Conn)error
}

(5)网络层设计

  • 阻塞IO:每次accept获取一个连接后,开一个协程单独处理,读完后处理业务逻辑再写会响应,若读数据时读到一半就读到这里啥也干不了。解决办法是引入通知机制,每次accept但拿到连接后,把它加到一个监听器中;
  • 非阻塞IO:轮询监听器,搜索可读连接数并开协程处理
type Conn interface{
    Read(b []byte)(n int,err error)
    Write(b []byte)(n int,err error)
    ...
}
go func(){
    for{
        conn,_:=listener.Accept()
        go func(){
            conn.Read(request)
            handle...
            conn.Write(response)
        }
    }
}
type Reader interface{
    Peek(n int)([]byte,error)
    ...
}
type Writer interface{
    Malloc(n int)(buf []byte,err error)
    Flush() error
    ...
}
type Conn interface(){
    net.Conn
    Reader
    Writer
}

go func(){
    for{
        readableConns,_:=Monitor(conns)
        for conn:=range readableConns{
            go func(){
                conn.Read(request)
                handle...
                conn.Write(response)
            }
        }
    }
}