一、HTTP协议是什么
HTTP协议(超文本传输协议)是最广泛使用的协议之一,它是text文本类型资源的扩充。
1、为什么需要协议
协议是交互双方的约定规则,网络上传输的是01数据流,因此协议首先需要明确信息的边界,即明确信息什么时候开始与结束,其次需要元数据对信息的描述,明确消息的类型、大小等。
2、协议内容
包括请求行、请求头字段和请求主体。
3、协议里有什么
- PUT和PATCH的区别:PUT完整更新,因此PUT幂等;PATCH部分更新,因此PATCH非幂等
- 状态行:协议版本、状态码、状态码描述
4、请求流程
完整的请求流程包括:
- 业务层
业务相关的逻辑. - 服务治理层&中间层
中间层是常说的熔断、限流等,而服务治理层依托于中间层,它对每个请求可有先处理逻辑和后处理逻辑,是和请求级别绑定的。 - 路由层
对客户端来说,经过业务层、服务治理层&中间层两层就进入协议编(解)码层,对协议编码和解析,最后通过传输层完成传输;而对服务器来说,要经过多个路由层,根据URL选择对应执行的handler。 - 协议编(解)码层
- 传输层
5、不足与展望
- HTTP1基于TCP,TCP有队头阻塞的问题,即后续分片须等待前面的分片的到来才继续发后面的数据,否则将会一直等待;
- HTTP1传输效率很低,比如只传输一个字节,传输的无效信息非常多,存在很多头部信息;
- HTTP1不支持多路复用,即一个请求没结束前不能再发送其他请求;
- HTTP1明文传输不安全;
- HTTP2可以多路复用,但仍基于TCP,未解决队头阻塞,且握手开销也未优化;
- QUIC基于UDP,但是应用不广泛
二、HTTP框架的设计与实现
1、分层设计
- 分层设计可以简化系统设计,让不同层专注做某一层次的事情;
- 只需通过接口,专注特定层开发即可,不需关注底层实现;
- 分层更容易横向扩展;
- 分层可做到很高的复用
考虑高内聚低耦合、易复用、高扩展性,http框架设计也应分层设计:
- 应用层Application:直接跟用户打交道,对请求抽象,包括request response context等,会提供丰富易用API;
- 中间层middleware:对请求有预处理和后处理的逻辑,如accesslog、recovery中间件捕获panic等;
- 路由层route:实现类似注册、路由寻址的操作;
- 协议层codec:Websocket、HTTP2、QUIC协议的支持;
- 网络层transport
- 公共逻辑层common
(1)应用层
不要试图在文档中说明,因为很多用户不看文档,因此需要在应用层序提供合理的API,包括:
- 可解释性:使用主流的概念方便理解,如ctx.GetBody()或ctx.Body()而不是ctx.BodyA()
- 简单性:常用API放到上层,易误用/低频AP放下层
- 可见性:最小暴露原则,不需暴露API就不暴露,可抽象为接口
- 冗余性:不需要冗余或能通过其他API组合得到的API
- 兼容性:尽量避免break change做好版本管理
(2)中间层
需配合handler实现完整请求处理生命周期,有预处理和后处理逻辑,可注册多中间件,对上层模块易用。
常见模型:洋葱模型,核心是将核心逻辑与通用逻辑分离
先经过日志中间件预处理,再经过metrics中间件预处理,之后进行真正业务逻辑;
最后退出业务逻辑得到后处理,先经过metric中间件后处理,其次经过日志中间件后处理,再将响应返回给用户。
适用场景包括:日志记录、性能统计、安全控制、事务处理、异常处理等
func Middleware(some param){
//some logic for pre-handle
...
Next()
//some logic for after-handle
...
}
中间件调用有点像函数调用,同时也可满足请求级别有效,只需将Middleware设计为业务和Handler相同即可,就不用区分是中间件还是业务逻辑,统一为直接调用下一个处理函数,抽象为Next()方法,对服务治理易用。
用户如果不主动调用下一个处理函数怎么办?核心:在任何场景下index保证递增。
func (ctx *RequestContext) Next(){
ctx.index++
for ctx.index < int8(len(ctx.handlers)){
ctx.handlers[ctx.index]()
ctx.index++
}
}
中间件调用链:
(3)路由设计
路由实际是为URL匹配的处理函数,包括静态路由和动态路由:
- 对于静态路由可使用map,key是URL,value是其handler
- 动态路由则需前缀树,每个节点用list存储handler
(4)协议层设计
抽象出合适的接口,实现Serve的接口,传入标准context(注意不要将context存储在结构体)和读写的连接,返回error。
type Server interface {
Serve(c context.Context,conn network.Conn)error
}
(5)网络层设计
- 阻塞IO:每次accept获取一个连接后,开一个协程单独处理,读完后处理业务逻辑再写会响应,若读数据时读到一半就读到这里啥也干不了。解决办法是引入通知机制,每次accept但拿到连接后,把它加到一个监听器中;
- 非阻塞IO:轮询监听器,搜索可读连接数并开协程处理
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)
}
}
}
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)
}
}
}
}