http协议
HTTP:超文本传输协议(Hypertext Transfer Protocol
请求流程
client包括业务层、服务治理层/中间件层、协议编解码层、传输层
server包括业务层、服务治理层/中间件层、路由层、协议编解码层、传输层
http框架
分层设计
分层设计实现专注性,扩展性,复用性
| OSI七层网络模型 | TCP/IP四层概念模型 | 对应网络协议 |
|---|---|---|
| 应用层(Application) | 应用层 | HTTP, TFTP, FTP, NFS, WAIS, SMTP |
| 表示层(Presentation) | Telent, Rlogin, SNMP, Gopher | |
| 会话层(Session) | SMTP, DNS | |
| 传输层(Transport) | 传输层 | TCP, UDP |
| 网络层(Network) | 网络层 | IP, ICMP, ARP, RARP, AKP, UUCP |
| 数据链路层(Data Link) | 数据链路层 |
FDDI, Ethernet, Arpanet, PDN, SLIP, PPP |
| 物理层(Physical) | IEEE 802.1A, IEEE 802.2到IEEE 802.11 |
http框架分层设计
目标:实现高内聚,低耦合,易复用,高扩展性
层与层之间使用接口解耦,分别是:
- 应用层:会对请求进行抽象并提供丰富易用的API
- 中间件层:对用户进行预处理以及后处理的逻辑
- 路由层:路由层负责接收请求并将其映射到对应的处理程序。它根据请求的URL、方法、头信息等来确定该由后端的哪个函数或应用组件来处理该请求。
- 协议层:协议层负责对HTTP协议进行抽象和封装,以简化应用层的开发。其职责包括:
- 解析请求报文,提取关键信息如方法、URL、头、正文等。
- 封装和发送响应报文。
- 封装请求/响应为应用接口或对象。
- 实现协议细节如连接管理、缓存协商、压缩编码、消息编码等。
- 网络层:网络层实现底层的网络通信,对应于TCP/IP模型中的传输层和网络层:
- 建立和结束TCP连接
- 发送和接收TCP报文
- 异常和重传处理
- 端口监听和连接分发 网络层确保了不同主机上的应用可以进行可靠的网络数据传输。
- common:会存放一些公共逻辑,每一层都会使用
应用层
- 提供合理的API
- 可理解性
- 简单性
- 冗余性
- 兼容性
- 可测性
- 可见性
中间件层
需求:
- 配合Handler实现一个完整的请求处理生命周期
- 拥有预处理逻辑以及后处理逻辑
- 可以注册很多的中间件
- 对上层模块用户逻辑模块易用
例子:
洋葱模型把HTTP服务器比作一层层的洋葱,请求从外层进入到内层,然后响应按顺序从内向外通过中间件传递。
具体来说,洋葱模型表示的是:
- 每层中间件可以修改请求对象和响应对象。
- 请求会按顺序从外到内,依次通过每层中间件。
- 每层中间件可以选择是否把请求传递给下一层。
- 响应是按内向外的顺序,逐层传递出去的。
- 每层中间件可以修改或封装响应内容。
这样的层级设计使得不同的中间件功能可以很好地解耦合,每一层只需要关注自身的逻辑,不需要了解其他中间件的实现。
比如日志中间件只记录基本信息,不关心业务逻辑;认证中间件只校验权限,不关心如何响应。
中间件设计
- 实现预处理以及后处理:
由于这样像是调用了一个函数,那么可以把他们统一成为一个函数func MIddleware(some param){ // some logic for pre-handle ... nextMiddleware() / bizLogic() //这里表示或者 // some logic after-handle ... }Next()这样可以实现路由上注册多 Middleware,同时也可以满足请求级别有效,只需要将 Middleware 设计为和业务和 Handler 相同即可。 - 当用户不主动调用下一个处理函数时可以主动调用下一个中间件以保持index递增,出现异常想停止时 可以让index编程最大值以跳出循环
例子:
- A调用B
- 由于B不调用Next,所以它会返回给A
- A调用C
- C调用Handler
- 返回C
- 返回A 使用场景:
- 不调用Next:初始化逻辑且 不需要在同一调用栈
- 调用Next:后处理逻辑或需要在同一调用栈上
路由设计
框架路由实际上就是为URL匹配对应的处理函数(Handlers)
- 静态路由:/a/b/c , /a/b/d
- 参数路由:/a/:id/c (/a/b/c , /a/d/c) 匹配id内容, /*all匹配*后全部内容
- 路由修复:/a/b <-> /a/b/
- 冲突路由以及优先级:/a/b , /:id/c
- 匹配HTTP方法
- 多处理函数:方便添加中间件
例子:前缀匹配树
对于参数路由
匹配http方法
构造许多路由树以及最外层有map
添加多处理函数
在每个节点上使用一个list存储handler
协议层设计
抽象出合适的接口:
type Server interface{
serve(c context.Context, conn network.Conn) error
}
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter
- 需要在连接上写数据
网络层设计
BIO: Block IO
go func(){
for {
conn, _ := listener.Accept()
go func(){
conn.Read(request) // 读取
handle... // 处理业务逻辑
conn.Write(response)// 写回response
}()
}
}
NIO:
go func(){
for {
// 注册一个监听器,监听到足够数据之后运行
readableConns, _ := Monitor(conns)
for conn := range readableConns{
go func(){
conn.Read(request)
handle...
conn.Write(response)
}()
}
}
}()
- 主goroutine中有一个无限循环,持续监听连接的可读事件。
- Monitor函数会使用类似epoll的I/O多路复用机制注册所有的连接,收到系统通知后才返回可读事件。
- 这避免了通过轮询方式不停地检查所有连接的低效操作。
- 当有连接就绪可读时,会通过channel返回可读的连接。
- 对每个可读连接,启动一个独立的goroutine进行处理。
- goroutine通过读请求、业务处理和写响应,实现了对一个连接的完整处理流程。
- 但goroutine仅处理业务,监听等职责还是主goroutine完成。
- goroutine采用高效的复用和调度模型,不需要每次请求都重新创建线程。
- 通过channel传递可读连接,可平滑扩展goroutine数量,充分利用CPU。
- 主goroutine、各个conn goroutine职责明确,采用并发的设计。 所以整个服务可以高效地处理大量连接,充分发挥多核CPU能力。go并发模型使得编写异步、并发程序变得简单明了。
go net :
type Conn interface{
Read(b []byte)(n int, err error)
Write(b []byte)(n int, err error)
...
}
属于BIO,当底层没有数据或数据写不到底层时可能会卡在这里,用户管理buffer
type Reader interface{
Peek(n int)([]byte, error)
...
}
type Writer interface{
Malloc(n int)(buf []byte, err error)
Flush() error
...
}
NIO模式,由网络库管理buffer
最终可以这样设计:
type Conn interface{
net.Conn
Reader
Writer
}