1 HTTP
前后端分离流程图
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 一次请求的完整流程
1.1.4 不足与展望
| HTTP1 | HTTP2 | QUIC |
|---|---|---|
| 队头阻塞 | 多路复用 | 基于UDP实现 |
| 传输效率低 | 头部压缩 | 解决队头阻塞 |
| 明文传输不安全 | 二进制协议 | 加密减少握手次数 |
| 支持快速启动 |
1.2 HTTP 框架的设计与实现
1.2.1 分层设计
1.2.2 应用层设计
提供合理的 API
-
可理解性:如 ctx.Body(),ctx.GetBody(),不要用 ctx.BodyA()
-
简单性:如ctx.Request.Header.Peek(key)/ctx.GetHeader(key)
-
冗余性:相同功能不同名称的两个接口是冗余的,两个接口一起完成一个功能也会有冗余
-
兼容性:开源时间久了之后所有的接口都会被用到,随意删除一个接口会造成麻烦
-
可测性
-
可见性
不要试图在文档中说明,很多用户不看文档
1.2.3 中间件层设计
中间件需求:
- 配合 Handler 实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
- 既然要实现预处理和后处理,那这个就很像调用了一个函数
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
}
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 方法
-
多处理函数:方便添加中间件
如何实现添加多处理函数?在每个节点上使用一个 list 存储handler
node struct {
prefix string
parent *node
children children
handlers app.HandlersChain
...
}
如何做设计
- 明确需求:考虑清楚要解决什么问题、有哪些需求
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最合适的方案进行开发
1.2.5 协议层设计
抽象出合适的接口
type Server interface {
Serve(c context.Context, conn network.Conn) error
}
//不要将Context存储在结构类型中;相反,将Context显式地传递给需要它的每个函数。Context应该是第一个参数。
//需要在连接上读写数据
1.2.6 网络层设计
BIO会堵塞,NIO使用监听器避免了阻塞
type Conn interface {
net.Conn
Reader
Writer
}
API 设计:可理解性、简单性...
中间件设计:洋葱模型
路由设计:前缀匹配树
协议层设计:抽象出合适的接口
网络层设计:网络模型
1.3 性能修炼之道
1.3.1 针对网络库的优化-buffer设计
不同网络库的优势
| go net | netpoll |
|---|---|
| 流式友好 | 中大包性能高 |
| 小包性能高 | 时延低 |
1.3.2 针对协议的优化
Headers解析
| 取 | 舍 |
|---|---|
| 核心字段快速解析 | 普通 header 性能较低 |
| 使用byte slice存储 | 没有 map 结构 |
| 额外存储到成员变量中 |
Headers key规范化
| 取 | 舍 |
|---|---|
| 超高的转换效率 | 额外的内存开销 |
| 比net.http提高40倍 | 变更困难 |
热点资源池化
| 取 | 舍 |
|---|---|
| 减少了内存分配 | 额外的 Reset 逻辑 |
| 提高了内存复用 | 请求内有效 |
| 降低了GC压力 | 问题定位难度增加 |
| 性能提升 |
1.4 企业实践
在工作中的一些感悟:
- 追求性能
- 追求易用,减少误用
- 打通内部生态
- 文档建设、用户群建设
内部HTTP框架:Hertz
1万+服务 3千万+QPS