HTTP/HTTPS
HTTP协议基本概念
HTTP (全称为 “超文本传输协议”) 是一种应用非常广泛的应用层协议。它是两点之间的传输文字、图片、音频、视频等超文本数据的约定和规范。HTTP最突出的优点就是简单、灵活、易于扩展、应用广泛和跨平台。
HTTP状态码的分类
| 分类 | 分类描述 |
|---|---|
| 1** | 信息状态码,服务器收到请求,需要请求者继续执行操作 |
| 2** | 成功状态码,操作被成功接收并处理 |
| 3** | 重定向状态码,需要进一步的操作以完成请求 |
| 4** | 客户端错误状态码,服务器无法处理客户端请求 |
| 5** | 服务器错误状态码,服务器在处理请求的过程中发生了错误 |
HTTP协议的格式
其中请求行主要由方法、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 |
其中比较重要的是GET和POST方法。GET方法的含义请求从服务器获取资源,这个资源可以是静态的文本、页面、图片、视频等;POST方法则是相反操作,它向URL指定的资源提交数据,数据就放在报文的body(请求体)中。
在HTTP协议中,所谓的安全是指请求方法不会破坏服务器上的资源;所谓的幂等,是指多次执行相同的操作,结果都是相同的。因此GET方法是安全且幂等的,因为它是只读操作;而POST方法因为是新增或者提交数据的操作,会对服务器上的数据进行修改,因此是不安全不幂等的。
URL (Uniform Resource Locator 统一资源定位符),互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。
协议头
header 的整体格式是键值对结构,每个键值对占一行,键和值之间使用 冒号+空格 进行分割。请求报头和响应报头的格式基本一致。
| key | value |
|---|---|
| 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.9 | HTML 文件传输 | 确立了客户端请求、服务端响应的通信流程 |
| 1.0 | 不同类型文件传输 | 设立头部字段 |
| 1.1 | 创建/断开 TCP 连接开销大 | 建立长连接进行复用 |
| 2 | 并发数有限 | 二进制分帧 |
| 3 | TCP 丢包阻塞 | 采用 UDP 协议 |
HTTPS概念及特点
HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTP中的信息是明文传输,存在安全风险。而HTTPS则解决了HTTP不安全的缺陷,在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输。HTTPS使用的主要目的是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性。
其特点为:
(1)内容加密:采用混合加密技术,中间者无法直接查看明文内容 ;
(2)验证身份:通过证书认证客户端访问的是自己的服务器
(3)保护数据完整性:防止传输的内容被中间人冒充或者篡改
HTTPS的交互过程如下:
HTTP框架设计与实现
分层设计
分层设计可简化系统设计,让不同层专注做某一层次的事,只需通过接口,专注特定层开发即可,不需关注底层实现。其次分层更容易横向扩展。最后分层可做到很高的复用。
分层设计的目标是满足高内聚、低耦合、易复用和高拓展性。
例如:
- 应用层:主要负责直接面向用户,初步处理用户的请求,提供丰富易用的API(满足可理解性、简单性)
- 中间件层:对请求进行预处理或者后处理,例如常用的消息队列、Token的处理等
- 路由层:实现类似注册、路由寻址等操作
- 协议层:主要部署各种网络协议
- 网络层:根据使用场景变化的网络库
- 公共层:放一些公共逻辑,能够支持上面每一层的使用
中间件设计
中间件的设计需要满足:配合handler实现完整请求处理生命周期,有预处理和后处理逻辑,可注册多中间件,对上层模块易用。
func Middleware(param){
//预处理
Next()
//后处理
}
中间件调用有点像函数调用,同时也可满足请求级别有效,只需将Middleware设计为业务和Handler相同即可,就不用区分是中间件还是业务逻辑,统一为直接调用下一个处理函数,抽象为Next()方法,对服务治理易用
中间件的设计和使用实操:
首先利用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)
}
}
}
}