一、走进HTTP协议
HTTP:超文本 传输 协议(Hypertext Transfer Protocol)
1.1、为什么需要协议
需要明确知道数据明确的边界。
一个常见的POST请求在协议层做了什么?
请求:
响应:
请求行,是由
请求方法 + URL +协议版本 组成。
状态行,由协议版本 + 状态码 + 状态码描述
请求/响应头:协议约定 + 业务相关:
- 协议相关:
Content-Type、Content-Length - 业务相关:自己定义
请求流程:
1.2、不足与展望:
HTTP1:
- 队头阻塞:
- 传输效率低
- 明文传输不安全
HTTP2:
- 多路复用
- 头部压缩
- 二进制协议
QUIC:
- 基于UDP实现
- 解决队头阻塞
- 加密减少握手次数
- 支持快速启动
二、HTTP 框架的设计与实现
HTTP框架图:
2.1、应用层设计 Application
提供合理的API
- 可理解性:如
ctx.Body(), 不要ctx.BodyA() - 简单性:如
ctx.Request.Header.Peek(key),包装一下:/ctx.GetHeader(key) - 冗余性
- 兼容性
- 可测性
- 可见性
2.2、中间件需求
- 配合
Handler实现一个完整的请求处理生命周期。 - 拥有预处理逻辑与后处理逻辑:可统计业务耗时
- 可以注册多个中间件
- 对上层模块用户逻辑块易用
经典模型:洋葱模型
适用场景:
- 日志记录
- 性能统计
- 安全控制
- 事务处理
- 异常处理
举例:打印每个请求的request和response
优化:
2.3、路由设计
框架路由实际上是为了匹配URL对应的处理函数(Handlers)
- 静态路由:
/a/b/c、/a/b/d - 参数路由:
/a/:id/c、/*all - 路由修复:
/a/b->/a/b/ - 冲突路由以及优先级:
/a/b、/:id/c - 匹配
HTTP方法 - 多处理函数:方便添加中间件
一般路由使用前缀树来设计:
那么,如何匹配HTTP方法呢? 其实可以通过构建多个前缀树:
如何实现为一个路由添加多个处理函数?
在每个节点上使用一个list存储handler
node struct{
prefix string,
parent *node,
children children.
handlers app.HandlersChain
}
2.4、协议层设计
需要抽象出来合适的接口:
type server interface{
Serve(c context.Context, conn network.Conn) err
}
遵循下列规范:
- 不要将
Context存储在接口中,放在第一个参数中传递 - 在
Conn上读写数据
2.5、网络层设计
网络层分为阻塞式IO 与 非阻塞式IO
阻塞式IO:
传统的 Go net是阻塞式的
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
// ...
}
go func() {
for {
conn, _ := listener.Accept()
go gunc() {
conn.Read(request)
// handler
conn.Write(response);
}()
}
}()
阻塞式IO的问题在于,Read的时候,没有数据会阻塞, Write的时候,没有要写的也会阻塞。
非阻塞式IO:
Netpoll就实现了非阻塞式IO,
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 gunc() {
conn.Read(request)
// handler
conn.Write(response);
}()
}
}
}()
三、性能修炼之道
3.1、针对网络库的优化
go net -> Netpoll
为每一个连接都绑定一个buf
采用链表实现无锁化
3.2、针对协议的优化
Header解析
热点资源池化
有点类似于 线程池。