01.再谈 HTTP 协议
HTTP 协议是什么?为什么需要协议?
HTTP:超文本传输协议(Hypertext Transfer Protocol)
需要明确的边界
- 开始
- 结束
能够携带信息
- 什么消息
- 消息类型
协议开始、协议元数据、Text、协议结束
一个常见的POST请求在协议层做的事?
协议里有什么
常见方法名
- GET
- HEAD
- POST
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
请求行/状态行
- 方法名
- URL
- 协议版本
- 状态码
- 状态码描述
请求头/响应头
- 协议约定
- 业务相关
请求体/响应体
状态码:
- 1xx:信息类
- 2xx:成功
- 3XX:重定向
- 4xx:客户端错误
- 5xx:服务端错误
请求流程
从上到下分别是:业务层、服务治理层、中间件层、路由层、协议编码层、传输层
不足与展望
HTTP1
- 队头阻塞
- 传输效率低
- 明文传输不安全
HTTP2
- 多路复用
- 头部压缩
- 二进制协议
QUIC
- 基于UDP实现
- 解决队头阻塞
- 加密减少握手次数
- 支持快速启动
02.HTTP 框架的设计与实现
分层设计
专注性、扩展性、复用性
高内聚、低耦合、易复用、高扩展性
可将此图和上图对比。
一个切实可行的复杂系统势必是从一个切实可行的简单系统发展而来的。从头开始设计的复杂系统根本不切实可行,无法修修补补让它切实可行。你必须由一个切实可行的简单系统重新开始。——盖尔定律
应用层设计
提供合理的 API
-
可理解性: 如 ctx.Body 0,ctx. GetBody(),
- 不要用 ctx.BodyA()
-
简单性: 如 ctx.Request.Header.Peek (key)/ctx. GetHeader (key)
-
冗余性
-
兼容性
-
可测性
-
可见性
中间件设计
中间件需求:
- 配合 Handler 实现一个完整的请求处理生命周期
- 拥有预处理逻辑与后处理逻辑
- 可以注册多中间件
- 对上层模块用户逻辑模块易用
洋葱模型:
核心逻辑与通用逻辑分离
适用场景:
- 日志记录
- 性能统计
- 安全控制
- 事务处理
- 异常处理
调用链:
有没有什么坑呢?不在一个调用栈上
适用场景:
- 不调用 Next: 初始化逻辑且不需要在同一调用栈
- 调用 Next: 后处理逻辑或需要在同一调用栈上
路由设计
框架路由实际上就是为 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 如何处理带参数的路由注册?(处理形如: /a/:id/b类型的路由)
/a/b/c /a/b/d /a/:b/d /a/:c/f
如何做设计?
- 明确需求:考虑清楚要解决什么问题、有哪些需求
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡: 思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最合适的方案进行开发
协议层设计
抽象出合适的接口:
- 不要将上下文存储在结构类型中:相反,将上下文显式地传递给需要它的每个函数。Context应该是第一个参数。
- 需要在连接上读写数据
网络层设计
netpoll NIO 网络库管理 buffer
netpoll 地址:https://github. com/cloudwego/netpoll
03.性能修炼之道
针对网络库的优化
go net 存下全部 Header 减少系统调用次数 能够复用内存 能够多次读
net with bufio 绑定一块缓冲区
netpoll 存下全部 Header拷贝出完整的 Body
netpoll with nocopy peek 分配足够大的 buffer 限制最大 buffer size
不同网络库的优势:
go net 流式友好 小包性能高
netpoll 中大包性能高 时延低
针对协议的优化——Headers解析:
针对协议相关的 Headers 快速解析:
- 通过 Header key 首字母快速筛除掉完全不可能的 key1.
- 解析对应 value 到独立字段
- 使用 byte slice 管理对应 header 存储,方便复用
请求体中同样处理的Key: User-Agent、 Content-Type、 Content-Length、 Connection, Transfer-Encoding
取 核心字段快速解析 使用byte slice存储 额外存储到成员变量中
舍 普通header性能较低 没有map结构
针对协议的优化——Header key规范化 aaa-bbb ——>Aaa-Bbb
取 超高的转换效率 比net。http提高40倍
舍 额外的内存开销 变更困难
热点资源池化
取
- 减少了内存分配
- 提高了内存复用
- 降低了GC压力
- 性能提升
舍
- 额外的 Reset 逻辑
- 请求内有效
- 问题定位难度增加
总结
- 针对网络库的优化: buffer 设计
- 针对协议的优化: header 解析、热点资源池化
04.企业实践
- 追求性能
- 追求易用,减少误用
- 打通内部生态
- 文档建设、用户群建设
Q&A
1. 为什么 HTTP 框架做要分层设计? 分层设计有哪些优势与劣势?
HTTP框架的分层设计有很多优势,其中最重要的是它能够帮助开发者更好地组织和管理代码,提高代码的可维护性和可扩展性。以下是HTTP框架分层设计的优势和劣势:
优势:
-
更好的代码组织:分层设计能够将代码按照功能划分成不同的层级,使得代码结构更加清晰,易于维护和扩展。
-
更高的可维护性:由于代码被划分成了不同的层级,因此开发者能够更容易地识别和解决问题,从而提高代码的可维护性。
-
更好的可扩展性:由于每个层级都有清晰的职责和接口,因此开发者可以更加容易地添加新的功能或修改现有功能,从而提高代码的可扩展性。
-
更高的复用性:分层设计能够使得代码更加模块化,从而提高代码的复用性,使得开发者可以更加高效地开发新的功能。
劣势:
-
增加了代码复杂度:分层设计需要开发者花费更多的时间和精力来设计和实现,因此会增加代码的复杂度。
-
增加了开发时间:分层设计需要开发者对每个层级进行详细设计和实现,因此会增加开发时间。
-
需要更多的协调工作:由于分层设计需要不同层级之间进行协作,因此需要更多的协调工作,可能会增加沟通成本。
总之,HTTP框架的分层设计能够带来很多优势,但也需要开发者在设计和实现过程中进行权衡和取舍。
2. 现有开源社区 HTTP 框架有哪些优势与不足?
现有的开源社区HTTP框架有很多,比如SpringMVC、Express、Flask、Django等。这些框架都有各自的优势和不足,下面是一些常见的优势和不足:
优势:
-
丰富的功能:开源社区HTTP框架通常提供了很多丰富的功能,包括路由、中间件、模板引擎、ORM等,这些功能能够帮助开发者快速构建Web应用程序。
-
易于使用:这些框架通常都有很好的文档和教程,使得开发者可以很容易地上手使用。
-
社区支持:由于是开源社区开发的框架,因此有很多热心的开发者会为框架贡献代码、提供技术支持等,使得框架更加稳定和可靠。
-
跨平台:大多数HTTP框架都是跨平台的,可以在不同的操作系统和开发环境中使用。
不足:
-
学习成本高:虽然这些框架都有很好的文档和教程,但是它们的学习曲线可能比较陡峭,需要花费一定的时间和精力去学习。
-
性能问题:一些HTTP框架可能存在性能问题,尤其是在高并发情况下。开发者需要仔细评估和测试框架的性能,确保其能够满足应用程序的需求。
-
定制性有限:虽然这些框架提供了很多功能,但是在某些情况下,开发者可能需要进行更加定制化的开发,这时候框架的定制性可能会有限。
-
安全问题:由于这些框架广泛使用,因此也成为了攻击者的目标。开发者需要仔细评估和测试框架的安全性,确保应用程序不会受到攻击。
总之,开源社区HTTP框架有很多优势和不足,开发者需要仔细评估和选择适合自己项目需求的框架。
3. 中间件还有没有其他实现方式?可以用伪代码说明。
除了常见的中间件实现方式,比如函数式中间件、类中间件、管道中间件等,还有一些其他的实现方式,例如基于装饰器的中间件。
基于装饰器的中间件可以通过给函数添加装饰器的方式来实现。下面是一个简单的伪代码示例:
def middleware(func):
def wrapper(*args, **kwargs):
# 执行中间件逻辑
print("执行中间件逻辑")
# 调用被装饰的函数
return func(*args, **kwargs)
return wrapper
# 定义一个普通的函数
def hello():
print("Hello World!")
# 使用装饰器给函数添加中间件
hello = middleware(hello)
# 调用函数
hello()
在上面的代码中,middleware 函数是一个中间件,它接受一个函数作为参数,并返回一个新的函数 wrapper。在 wrapper 函数内部,先执行中间件逻辑,然后再调用被装饰的函数 func。
在最后一行代码中,通过 hello = middleware(hello) 的方式给 hello 函数添加了中间件。当调用 hello() 函数时,会先执行中间件逻辑,然后再输出 "Hello World!"。
总之,基于装饰器的中间件是一种简单而灵活的实现方式,可以通过添加、删除或替换装饰器来动态地修改中间件的执行顺序。
4. 完成基于前缀路由树的注册与查找功能? 可以用伪代码说明
基于前缀路由树的注册和查找功能,可以通过 Trie 树来实现。下面是一个简单的伪代码示例:
# 定义 Trie 树的节点类
class TrieNode:
def __init__(self):
self.children = {}
self.handler = None
# 定义前缀路由树类
class PrefixTree:
def __init__(self):
self.root = TrieNode()
# 向前缀路由树中注册路由
def register(self, path, handler):
node = self.root
for char in path:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.handler = handler
# 在前缀路由树中查找路由
def lookup(self, path):
node = self.root
for char in path:
if char not in node.children:
return None
node = node.children[char]
return node.handler
在上面的代码中,TrieNode 类表示 Trie 树的节点,每个节点包含一个子节点字典 children 和一个处理函数 handler。PrefixTree 类表示前缀路由树,它包含一个根节点 root,以及 register 和 lookup 两个方法。
在 register 方法中,首先遍历路径中的每个字符,如果当前字符在子节点字典中不存在,则创建一个新的节点,并将当前节点指向子节点;如果当前字符已经存在,则直接将当前节点指向子节点。最后将当前节点的处理函数设置为指定的处理函数。
在 lookup 方法中,同样遍历路径中的每个字符,如果当前字符在子节点字典中不存在,则说明路径不存在,直接返回 None;如果当前字符存在,则将当前节点指向子节点。最后返回当前节点的处理函数。
通过前缀路由树,可以实现高效的路由注册和查找功能,可以快速地匹配出满足指定路径的路由,并执行相应的处理函数。
5. 路由还有没有其他的实现方式?
除了基于前缀路由树的实现方式,还有一些其他的路由实现方式,例如正则表达式路由、反射路由、中央调度路由等。
- 正则表达式路由
正则表达式路由通过使用正则表达式来匹配路由,可以实现更加灵活的路由匹配。例如,可以使用正则表达式来匹配动态路径参数。
- 反射路由
反射路由通过使用反射来调用指定的处理函数,可以实现更加灵活的路由处理逻辑。例如,可以使用反射来动态调用不同的处理函数,根据参数类型来选择相应的处理逻辑。
- 中央调度路由
中央调度路由通过使用一个中央的路由控制器来分发路由请求,可以实现更加灵活和可扩展的路由控制逻辑。例如,可以使用中央调度路由来实现多级路由控制器,将路由请求分发到不同的子控制器中处理。
总之,不同的路由实现方式各有优劣,可以根据具体的需求和场景来选择合适的实现方式。