HTTP原理及框架实现 | 青训营

139 阅读10分钟

HTTP/HTTPS

HTTP协议基本概念

HTTP (全称为 “超文本传输协议”) 是一种应用非常广泛的应用层协议。它是两点之间的传输文字、图片、音频、视频等超文本数据的约定和规范。HTTP最突出的优点就是简单、灵活、易于扩展、应用广泛和跨平台

HTTP状态码的分类

分类分类描述
1**信息状态码,服务器收到请求,需要请求者继续执行操作
2**成功状态码,操作被成功接收并处理
3**重定向状态码,需要进一步的操作以完成请求
4**客户端错误状态码,服务器无法处理客户端请求
5**服务器错误状态码,服务器在处理请求的过程中发生了错误

HTTP协议的格式

image.png

其中请求行主要由方法、URL、版本号组成。常见方法有:

方法说明适用版本号
GET获取资源HTTP 1.0、HTTP 1.1
POST传输实体主体HTTP 1.0、HTTP 1.1
PUT传输文件HTTP 1.0、HTTP 1.1
HEAD获得报文首部HTTP 1.0、HTTP 1.1
DELETE删除文件HTTP 1.0、HTTP 1.1
OPTIONS访问支持的方法HTTP 1.1
TRACE追踪路径HTTP 1.1
CONNECT要求用隧道协议连接代理HTTP 1.1
LINK建立和资源之间的联系HTTP 1.1
UNLINE断开连接关系HTTP 1.1

其中比较重要的是GETPOST方法。GET方法的含义请求从服务器获取资源,这个资源可以是静态的文本、页面、图片、视频等;POST方法则是相反操作,它向URL指定的资源提交数据,数据就放在报文的body(请求体)中。

在HTTP协议中,所谓的安全是指请求方法不会破坏服务器上的资源;所谓的幂等,是指多次执行相同的操作,结果都是相同的。因此GET方法是安全且幂等的,因为它是只读操作;而POST方法因为是新增或者提交数据的操作,会对服务器上的数据进行修改,因此是不安全不幂等的。

URL (Uniform Resource Locator 统一资源定位符),互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。

image.png

协议头

header 的整体格式是键值对结构,每个键值对占一行,键和值之间使用 冒号+空格 进行分割。请求报头和响应报头的格式基本一致。

keyvalue
Host表示服务器主机的地址和端口
Content-Length表示 body 的数据长度,长度单位是字节
Content-Type表示 body 的数据格式
User-Agent表示浏览器或者操作系统的属性
Referer表示这个页面是从哪个页面跳转过来的
Cookie是浏览器提供的一种让程序员在本地存储数据的能力

各版本HTTP协议特点

HTTP1.0:

  • 浏览器每发一个请求都会与服务器建立一个TCP请求,当请求结束后该TCP连接就会断开。(无连接
  • 服务器不会跟踪每一个客户端也不会记录过去的请求。(无状态

HTTP1.1:

  • 在HTTP1.0中默认使用Connection:close。在HTTP1.1中已经默认使用Connection:keep-alive,避免了连接建立和释放的开销。一个TCP默认不关闭,可以被多个请求复用。只有当设定的时间过了该连接才会断开。(长连接
  • 引入了管道机制,一个TCP连接可以同时发送多个请求。
  • 对一个域名的请求允许分配多个长链接(缓解了长连接中对头阻塞的问题)。

HTTP2.0:

  • 二进制协议:在HTTP1.1版本的头部信息是文本,数据部分可以是文本也可以是二进制。在HTTP2版本的头部和数据部分都是二进制,且统称为帧。
  • 多路复用:废弃了HTTP1.1中的管道,同一个TCP连接里面,客户端和服务器可以同时发送多个请求和多个响应,且不用按照顺序来,这样避免了队头阻塞的问题。
  • 头部信息压缩:使用专用算法压缩头部,减少数据传输量,主要是通过服务器和客户端共同维护一张头部信息表,所有的头部信息在表里面都会有对应的记录,并且会有一个索引号,这样后面只需要发送索引号即可。
  • 服务器主动推送:允许服务器主动向客户推送数据。
  • 数据流:HTTP2中每一个请求或者响应的所有数据包,成为一个数据流,并且每一个数据流都有一个唯一ID,请求数据流的ID为奇数,响应数据流的ID是偶数。每个数据包在发送的时候带上对应数据流的ID,这样服务器和客户端就能分区是属于哪一个数据流。

HTTP3.0:

  • HTTP3底层是基于UDP实现的,而UDP不需要三次握手、四次挥手的过程,所以天生比TCP快。
  • QUIC不再以四元组标识,而是以一个 64 位的随机数作为 ID 来标识,而且 UDP 是无连接的,所以当 IP 或者端口变化的时候,只要 ID 不变,就不需要重新建立连接。
协议版本解决的核心问题解决方式
0.9HTML 文件传输确立了客户端请求、服务端响应的通信流程
1.0不同类型文件传输设立头部字段
1.1创建/断开 TCP 连接开销大建立长连接进行复用
2并发数有限二进制分帧
3TCP 丢包阻塞采用 UDP 协议

HTTPS概念及特点

HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTP中的信息是明文传输,存在安全风险。而HTTPS则解决了HTTP不安全的缺陷,在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输。HTTPS使用的主要目的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性。

其特点为:

(1)内容加密:采用混合加密技术,中间者无法直接查看明文内容 ;

(2)验证身份:通过证书认证客户端访问的是自己的服务器

(3)保护数据完整性:防止传输的内容被中间人冒充或者篡改

HTTPS的交互过程如下: image.png

HTTP框架设计与实现

分层设计

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

image.png

分层设计的目标是满足高内聚低耦合易复用高拓展性

image.png

例如:

  • 应用层:主要负责直接面向用户,初步处理用户的请求,提供丰富易用的API(满足可理解性、简单性)
  • 中间件层:对请求进行预处理或者后处理,例如常用的消息队列、Token的处理等
  • 路由层:实现类似注册、路由寻址等操作
  • 协议层:主要部署各种网络协议
  • 网络层:根据使用场景变化的网络库
  • 公共层:放一些公共逻辑,能够支持上面每一层的使用

中间件设计

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

func Middleware(param){
    //预处理
    
    Next()
    
    //后处理
}

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

中间件的设计和使用实操:

首先利用GO创建一个新的类型 Middleware。中间件只将 http.HandlerFunc作为其参数,在中间件里将其包装并返回新的 http.HandlerFunc供服务器服务复用器调用。

type Middleware func(http.HandlerFunc) http.HandlerFunc

然后再接住中间件通用代码模板编写中间件,后续只需要往模板中添加核心代码逻辑。中间件是使用装饰器模式实现的。

func createNewMiddleware() Middleware {
    // 创建一个新的中间件
    middleware := func(next http.HandlerFunc) http.HandlerFunc {
        // 创建一个新的handler包裹next
        handler := func(w http.ResponseWriter, r *http.Request) {
            // 中间件的处理逻辑
                        ......
            // 调用下一个中间件或者最终的handler处理程序
            next(w, r)
        }
        // 返回新建的包装handler
        return handler
    }
    // 返回新建的中间件
    return middleware
}

创建两个中间件,一个用于记录程序执行的时长,另外一个用于验证请求用的是否是指定的 HTTPMethod,创建完后再用定义的 Chain 函数把 http.HandlerFunc 和应用在其上的中间件链起来,中间件会按添加顺序依次执行,最后执行到处理函数。完整的代码如下:

package main
import (
    "fmt"
    "log"
    "net/http"
    "time"
)
type Middleware func(http.HandlerFunc) http.HandlerFunc
// 记录每个URL请求的执行时长
func Logging() Middleware {
    // 创建中间件
    return func(f http.HandlerFunc) http.HandlerFunc {
        // 创建一个新的handler包装http.HandlerFunc
        return func(w http.ResponseWriter, r *http.Request) {
            // 中间件的处理逻辑
            start := time.Now()
            defer func() { log.Println(r.URL.Path, time.Since(start)) }()
            // 调用下一个中间件或者最终的handler处理程序
            f(w, r)
        }
    }
}
// 验证请求用的是否是指定的HTTP Method,不是则返回 400 Bad Request
func Method(m string) Middleware {
    return func(f http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            if r.Method != m {
                http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
                return
            }
            f(w, r)
        }
    }
}
// 把应用到http.HandlerFunc处理器的中间件
// 按照先后顺序和处理器本身链起来供http.HandleFunc调用
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
    for _, m := range middlewares {
        f = m(f)
    }
    return f
}
// 最终的处理请求的http.HandlerFunc 
func Hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}
func main() {
    http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))
    http.ListenAndServe(":8080", nil)
}

运行程序后会打开浏览器访问 http://localhost:8080会有如下输出:

2020/02/07 21:07:52 / 359.503µs
2020/02/07 21:09:17 / 34.727µs

以上主要是探究实现原理,那么下面我们考虑在项目中依靠Gin框架实现中间件,最终完成一个用户Token的加密解析功能。这里给出部份实现代码:

import (  
"github.com/gin-gonic/gin"  
)  
  
func Automiddleware() gin.HandlerFunc {  
    return func(c *gin.Context) {  
        tokenstring := c.Query("token") // 获取加密的鉴权token  
        if tokenstring == "" {  
            tokenstring = c.PostForm("token")  
        }  
        token, claim, err := ParseToken(tokenstring) // 解析token  
        if err != nil || !token.Valid {  
            c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "权限不足"})  
            c.Abort() // 确保这个请求的其他函数不会被调用,例如router中的第二个handlefunc  
            return  
        }  

        c.Set("uid", claim.UserId)  
        c.Next()  
    }  
}

网络层设计

阻塞IO是指每次accept获取一个连接后,开一个goroutine单独处理,读完后处理业务逻辑再写会响应,若读数据时读到一半就读到这里啥也干不了。

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

非阻塞IO引入通知机制,每次accept但拿到连接后,把它加到一个监听器中,另外一部分去轮询monitor即监听器,搜索可读连接数并开协程处理,就不会出现阻塞。

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