第六届字节跳动青训营第四课 | 青训营

64 阅读4分钟

第四节课之HTTP协议

HTTP协议

  1. 定义 HTTP的全称为超文本传输协议(Hypertext Transfer Protocol)。 其中,超文本表示的是文本数据的拓展,包括图片、视频等文件;协议则能明确数据流边界,并携带信息,这可以简化为四部分内容:协议开始、协议元数据、文本、协议结束。

  2. 协议内容 首先是请求行或状态行,请求行应包括方法名、URL和协议版本,状态行则有协议版本、状态码和状态描述。常见的方法有 GETHEADPOST 等,状态码则有信息类,成功,重定向等。 其次是请求头或响应头,它们都应有协议约定或业务相关的内容。 最后则是请求体或响应体。 一个完整的请求协议示例如下。首行和末行分别为请求行和请求体,其余则为请求头。

    POST /a/b/c HTTP/1.1
    Who: Alex
    Content-Type: text/plain
    Host: 127.0.0.1:8888
    Content-Length: 28
    
    Let's watch a movie together
    

    对应的响应协议示例如下。

    HTTP/1.1 200 OK
    Server: hertz
    Date: Sun, 20 Aug 2023 22:42:13 GMT
    Content-Type: text/plain; charset=utf-8
    Content-Length: 2
    Upstream-Caught: 1650541592984580
    
    OK
    

    而Go语言开发中,实现这样一个响应可以采用如下代码。

    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("/a/b/c", func(c context.Context, ctx *app.RequestContext) {
            ctx.Data(200, "text/plain; charset=utf-8", []byte("OK"))
        })
    
        h.Spin()
    }
    
  3. 请求流程 流程一般经过五层,由上至下分别为:业务层、服务治理层或中间层、路由层、协议编解码层和传输层。

  4. 问题 HTTP1存在队头阻塞,传输效率低和明文传输导致的不安全问题。 HTTP2解决了部分问题,实现了多路复用,头部压缩和二进制协议。 QUIC则基于UDP实现,进一步解决了队头阻塞,并加密减少握手次数和支持快速启动。

HTTP框架设计

  1. 分层 分层设计的专注性、扩展性和复用性有利于开发,这样的高内聚低耦合系统可以更轻松地开发较大的项目。

  2. 应用层 应用层设计应注重提供合理的API。 该接口需要保持可理解性,简单性,冗余性,兼容性,可测性和可见性。 如 ctx.Body() 就是一个简单易理解的函数;为了保持简单性,我们可以将 ctx.Request.Header.Peek(key) 的调用简化为 ctx.GetHeader(key)

  3. 中间件 中间件应满足需求包括:配合Handler实现一个完整的请求处理生命周期,拥有预处理逻辑与后处理逻辑,可以注册多中间件,对上层模块用户逻辑模块易用。 一个典型的模型设计为洋葱模型,请求分别经过日志、Metrics到达业务处理,响应再从中经过Metrics和日志传出,这实现了核心逻辑与通用逻辑分离。 一个典型的中间件就像调用一个函数。

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

    用户若不主动调用下一处理函数,则可以通过如下方式在任何场景下保证索引递增。

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

    当异常出现需要停止时,我们可以通过设置索引边界的方式作为异常。

    func (ctx *RequestContext) Abort() {
        ctx.index = IndexMax
    }
    

    在调用时,需注意后处理逻辑或需要在同一调用栈的中间件都应调用Next,一般仅初始化逻辑且不需要在同一调用栈时才不调用Next。

  4. 路由 路由实际上就是为URL匹配对应的处理函数。 路由的形式包括形如 /a/b/c 的静态路由和 /a/:id/c/*all 的参数路由。 除此之外,路由还应有路由修复的功能,如将 /a/b/a/b/ 对应起来 为了处理多种路由,一般采用前缀匹配树的数据结构进行路由匹配。 为了匹配HTTP方法,则在外层使用Map结构,根据method进行初步筛选,再进入前缀树匹配。

  5. 协议层 在这一层,可以抽象出合适的接口如下。

    type Server interface {
        Serve(c context.Context, conn network.Conn) error
    }
    
  6. 网络层 网络层的设计一般有两种,首先是会阻塞的BIO。

    go func() {
        for {
            conn, _ := listener.Accept()
            go func() {
                conn.Read(request)
                
                // handle...
    
                conn.Write(response)
            }()
        }
    }()
    

    NIO相比于前者,可以避免读取数据阻塞等待过久的问题。

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