HTTP框架
HTTP协议
介绍
- HTTP(Hypertext Transfer Protocol):超文本传输协议
- HTTP协议能够支持传输图片、mp3、avi等格式文件
- 协议产生原因:
内容
- HTTP通常会规定以下几种内容
请求流程
- 请求在客户端和服务端之间的请求流程(从外向内)
不足&展望
HTTP框架的设计与实现
分层设计
网络模型与协议
- OSI七层网络模型——理想下的网络分层,属于研究模型
- 五层网络模型——较为精简的网络分层,利用功能分层,属于教学模型
- TCP/IP四层网络模型——通用的商业网络模型,是性价比最高的实践产品
请求在网络中的传输
- 过程大致如下
应用层
中间件
- 需求:
- 配合 Handler 实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
- 经典模型——洋葱模型
- 中间件处理流程
- 路由上可以注册多个 Middleware ,同时也可以满足请求级别有效,只需要将 Middleware 设计为和业务、Handler 相同即可。
- next()控制下一处理函数的调用:使用index控制,当index等于ctx的handler数量,则退出next()执行,否则按顺序执行,直到执行完毕代码类似如下
func (ctx *RequestContext) Next(){
ctx.index++
for ctx.index < int8(len(ctx.handlers)){
ctx.handlers[ctx.index]()
ctx.index++
}
}
- 中间件执行next()时的异常终止设计:将ctx的index设为最大值(ctx的handler数量),使其退出,代码类似如下
func (ctx *RequestContext) Abort(){
ctx.index = IndexMax
}
路由
- 框架路由实际上就是为 URL 匹配对应的处理函数(对应Server端的Controller层函数)
- 路由的划分:
- 静态路由(明确,精准匹配):juejin.cn/course/byte…
- 参数路由(不明确,模糊匹配):
- 路由修复:路由修复就是会为用户补齐url最后的/,比如用户输入 juejin.cn ,路由修复将自动转为juejin.cn/
- 路由设计
- 路由匹配设计
- 使用map存储handlers的路由——缺点:只适用于静态路由,无法用于参数路由
- 使用前缀匹配树
- 路由与方法匹配
- 路由映射表(k——method,v——前缀树)根据方法找前缀树,若不存在则v为空
- 添加多处理函数
协议层
- 抽象出合理接口
type Server interface{
Serve(c context.Context, conn network.Conn) error
}
- 分析上述设定
网络层
- BIO:阻塞IO,如下,连接先读取请求,然后处理业务,最后写回响应,这一连串的操作都是线性的,如果读数据的操作阻塞整个连接都会阻塞
go func(){
for {
conn,_ := listener.Accept()
go func() {
conn.Read(request)
handler()
conn.Write(response)
}
}
}
- NIO:较于BIO,多了一个监听操作,监听到足够多的数据才会进行一系列读取请求,然后处理业务,最后写回响应的处理,这样连接在读数据时就不会发生阻塞
go func(){
for {
//注册监听器,监听读取数据
readableConn,_ := Monitor(conns)
for conn := range readableConn {
go func() {
conn.Read(request)
handler()
conn.Write(response)
}
}
}
}
-
相关数据结构
`go net`——BIO netpoll——NIO- BIO——Conn 字节流进,数据长度+err出
type Conn interface {
Read(b []byte)(n int, err error)
Write(b []byte)(n int, err error)
...
}
- NIO——Conn
type Conn interface {
net.Conn
Reader
Writer
}
type Reader interface {
Peek(n int)(buf []byte, err error)//监听到足够数量的数据才会读入
...
}
type Writer interface {
Malloc(n int)(buf []byte, err error)//底层开辟一个大小为n的空间,把数据写入
Flush() error//发送error
...
}
设计要求
性能修炼之道
针对网络库的优化
go net(BIO)
需求
- 能够存下全部 Header(内存空间),Header长度可变,需要保存所有Header才好解析
- 减少系统调用次数,系统态/用户态的切换有较大开销
- 能够复用内存
- 能够多次读,如果解析失败,可以支持多次重新解析请求
优化
- 绑定一块缓冲区(据调查,大小通常为4k最佳)
type Reader interface {
Peek(n int)([]byte, err error)//该方法用来保存读取数据时的指针
Discard(n int)(discarded int, err error)//该方法用来让指针前移
Release() error//该方法用来释放绑定的缓冲区内存
Size() int
Read(b []byte)(l int, err error)
...
}
type Writer interface {
//拆分 Write(b []byte)(l int, err error)
Write(b []byte)
Size() int
Flush() error//发送error
...
}
netpoll(NIO)
需求
- 能够存下全部 Header(内存空间),Header长度可变,需要保存所有Header才好解析
- 可以拷贝出完整的 Body
优化
不同网络库的优势
针对协议的优化
Headers解析
第一步:找到 Header Line 的边界——\r\n
- 通常情况下,先找到 \n ,然后再看它前一个是否为 \r
缺点:非常慢
- 优化一:使用SIMD技术,并行操作找边界,加快解析速度
第二步: 快速解析 Headers
- 步骤
- 通过 Header key 首字母快速筛除掉完全不可能的 key
- 常用高频Header,各开辟单独的空间。解析对应 value 到独立字段
- 使用 byte slice 管理对应 header 存储,方便复用
- 优缺点
Header key 规范化
- Header key的规范是Xxx-Yyy
- Header key的规范化:
- 用两张表分别存储所有Header key的大写和小写对应ASCII码
- 将得到的Header key转成ASCII,然后 在表中查找,找到即返回
- 优缺点:
热点资源池化
- 请求中的热点资源——RequestContext(每个请求和响应都需要,与请求一一对应,贯穿一个请求始终)
- 传统情况下,一次请求需要创建一次RequestContext,响应后需要关闭,如下。每次都一开一关,非常浪费该资源。
- 优化——创建一个RequestContext资源池,里面创建一定量的RequestContext,每有一个请求则从资源池中取一个RequestContext,用完将RequestContext放回资源池。
- 优缺点:
-
减少了内存分配,提高了内存复用(传统情况,每次创建RequestContext都要分配一次内存)
-
降低GC压力(每次关闭RequestContext,要调用GC回收内存)
-
性能提升
-
额外的 Reset 逻辑(RequestContext被上一个请求使用后放回,需要做一些reset操作,否则,上一请求的脏数据可能会影响到下一请求的使用)
-
仅在请求内有效(资源池中申请RequestContext是有时限的,如果超出时限,RequestContext就不再可靠)
-
问题定位难度增加(增加资源池后,RequestContext的获取、放回和重置都是额外增加的逻辑,都存在出问题的可能,再加上高并发场景下多个RequestContext不停地被复用,问题产生的概率增加,且难以定位)
-
企业实践
要求
使用
总结
本次课主要内容有大半是学过的HTTP协议,这点很友好,涉及到网络架构/层级用图来说明最清楚,因此本次笔记图片较多。另外网络库的拓展是之前没有接触过的,加上字节都在自研网络库(netpoll),所以网络库的学习也很重要,需要结合理论去分析。