一、走进HTTP协议
前后端分离的流程图
-
HTTP框架负责对HTTP请求的解析,根据对应的路由选择对应的后端逻辑
-
1.1 HTTP协议是什么?
- HTTP: 超文本传输协议(Hypertext Transfer Protocol)
-
为什么需要协议?
- 需要明确信息起始边界,需要描述信息类型
-
1.2 一个POST请求发生了什么?协议里面有什么?
- 请求行(方法名:GET、POST等、url、协议版本)/状态行(协议版本、状态码、状态码描述)
- 状态码1xx:信息类;2xx:成功;3xx:重定向;4xx:客户端错误;5xx:服务端错
- 请求头/响应头
- 请求体/响应体
POST /sis HTTP/1.1 // 请求行:POST url HTTP版本 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 // 200是状态码,OK是对该状态码的回复 Server: hertz Data: Thu, 21 Apr 2022 11:46:32 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 2 Upstream-Caught: ************* OK - 请求行(方法名:GET、POST等、url、协议版本)/状态行(协议版本、状态码、状态码描述)
-
1.3 请求流程
- 在业务层,业务方使用框架提供的API去完成业务逻辑
- 服务治理层(垄断、限流等),服务治理层依托于中间件层
- 路由层
- 协议编解码层
- 传输层
- HTTP不足与展望
- HTTP1:基于TCP,存在队头阻塞问题;传输效率低;不支持多路复用;使用明文传输不安全
- HTTP2:多路复用;头部压缩;二进制协议
- QUIC: 基于UDP实现,解决队头阻塞;优化加密算法,加密减少握手次数;支持快速启动
二、HTTP框架的设计与实现
-
2.1 分层设计(高内聚、低耦合、易复用、高扩展性),下面架构分成5层,层与层之间使用接口解耦,从上往下依次是(应用层:对请求进行抽象,提供丰富应用API;中间件层:对用户预处理和后处理逻辑;路由层:注册、寻址相关操作;协议层;网络层),common中存放通用逻辑。
-
2.2 应用层设计
核心是提供合理的API- 可理解性:使用主流的概念(方便能看出来接口的功能)
- 简单性:对于高频结构在最外层写一个方法
- 冗余性:不要出现实现同样功能的接口
- 兼容性
- 可测性:接口可测试
- 可见性:核心部分不可见,防止被修改
-
2.3 中间件设计
中间件的需求如下:-
配合Handler实现一个完整的请求处理生命周期
-
拥有预处理逻辑和后处理逻辑
-
可以注册多中间件
-
对上层模块用户逻辑模块易用
举例:洋葱模型\
-
请求过来之后先经过日志中间件的预处理,之后经过Metrics中间件的预处理,最后去执行业户逻辑,在业务逻辑退出之后首先经过Metrics中间件的后处理,最后通过日志中间件的预处理,然后将真正的响应返回给用户。中间件的核心是将核心逻辑与通用逻辑分离。使用场景包括:日志记录、性能统计、安全控制、事务处理、异常处理。
* 怎样实现中间件设计?
1. 实现预处理和后处理,很像调用了一个函数
```go
func Middleware(some param){
// some logic for pre-handle
...
nextMiddleware() / bizlogic()
// some logic after-handle
...
}
```
2. 路由上可以注册多Middleware,同时可以满足请求级别有效,只需要将Middleware设计为和业务和Handle相同即可。
```go
func Middleware(some param){
// some logic for pre-handle
...
// 不需要区分业务逻辑和中间件
Next()
// some logic after-handle
...
}
```
3. 用户如果不主动调用下一个处理函数怎么办?
```go
func Middleware(some param){
// some logic
...
}
```
我们需要帮用户主动调用之后的中间件
```go
func (ctx *RequestContext) Next(){
ctx.index++
for ctx.index < int8(len(ctx.handlers)){
ctx.handlers[ctx.index]()
ctx.index++
}
}
// 核心是在任何场景下保证index递增
```
4. 出现异常想停止怎么办?\
我们可以把index设置为最大值让其跳出循环
```go
func (ctx *RequestContext) Abort(){
ctx.index = IndexMax
}
```

-
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方法
- 多处理函数:方便添加中间件
- ……
-
路由设计
- 青铜: map[string]handlers
只适合/a/b/c、/a/b/d这种静态路由,不适合/a/:id/c、/*all这种参数路由 - 黄金:前缀匹配树 /a/b/c、/a/b/d,从前往后依次匹配,如果是参数路由,先匹配到:
- 如何匹配·HTTP方法?\
- 青铜: map[string]handlers
外层Map:根据methord进行初步筛选
* 如何实现添加多处理函数?
在每个节点上使用一个list存储handler
go node struct{ prefix string parent *node children children handlers app.HandlersChain ... }
-
如何做设计?
- 明确需求:考虑清楚要解决什么问题,有哪些需求
- 业界调研:业界有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案进行评审
- 确定开发:确定最合适的方案进行开发
-
2.5 协议层设计
抽象出合适的接口:type Server interface{ Server(c context.Context, conn network.Conn) error }- 不要将contexts放到一个struct里面,而应该将context作为第一个参数传进需要的函数中
- 需要在连接上读写数据
-
2.6 网络层设计
首先了解BIO(阻塞IO)
go func(){
for{
conn,_ := listener.Accept()
go func(){
conn.Read(request)
handle...
conn.Write(response)
}()
}
}()
NIO(非阻塞IO)
go func(){
for{
readableConns,_ := Monitor(conns)
for connn : = range readableConns{
go func(){
conn.Read(request)
handle...
conn.Write(response)
}()
}
}
}()
网络层设计
type Conn interface{
net.Conn
Reader
Writer
}