第一层:应用层(业务层)
第二层:中间件层(服务治理层)
如果没有中间件,需要在每一个接口的开始和结束都加上一句话,如果有中间件就不一样了 (类似只需要写一次,然后复用就OK)
-
配合 Handler 实现一个完整的请求处理生命周期
- 在一个典型的网络应用程序中,当客户端发送一个请求时,服务器需要对这个请求进行处理并返回响应。这个处理过程通常涉及多个步骤,从接收请求到最终返回响应。
- Handler是负责实际处理请求的组件。它接收请求,执行必要的业务逻辑(如查询数据库、执行计算等),然后生成响应。
- 中间件(Middleware) 则是在请求到达 Handler 之前和 Handler 处理完请求之后执行额外逻辑的组件。中间件与 Handler 协同工作,确保请求在整个生命周期内得到完整处理。
- 例如,在一个 Web 应用中,当用户请求一个网页时,请求首先经过中间件(如身份验证中间件),然后到达处理该请求的 Handler(如返回网页内容的函数),最后可能再次经过中间件(如日志记录中间件),整个过程构成了一个完整的请求处理生命周期。
-
拥有预处理逻辑与后处理逻辑
-
预处理逻辑
- 预处理逻辑是指在请求到达实际处理程序Handler之前执行的逻辑。它可以用于检查请求的合法性、验证用户身份、设置请求上下文等。
- 例如,在一个 Web API 中,权限验证中间件可以在请求到达实际的 API 处理函数之前,检查请求中携带的用户令牌是否有效。如果令牌无效,中间件可以直接返回错误响应,阻止请求继续向下传递。
-
后处理逻辑
- 后处理逻辑是指在 Handler 处理完请求之后执行的逻辑。它通常用于记录请求处理结果、清理资源、修改响应内容等。
- 例如,在一个 Web 应用中,日志记录中间件可以在 Handler 返回响应之后,记录请求的处理时间、返回的状态码等信息,以便进行性能分析和故障排查。
-
将中间件和业务逻辑统一合并为一个Next函数,不再区分中间件和业务逻辑,
帮用户主动调用之后的中间件,在任何场景下保持index递增
出现异常想停止怎么办?index设置成一个最大值,直接跳出来即可。
中间件C显示调用业务Handler
Recovery Middleware不能捕获其他协程的panic的, 只能调用本协程或者本调用栈的,如果中间件B是Recovery Middleware,他是捕获不了Handler里的中间件的。所以Recovery Middleware是需要在同一调用栈上的,是必须要调用Next逻辑的。
-
可以注册多中间件
- 在一个复杂的系统中,通常需要多个中间件来处理不同方面的逻辑。系统需要支持注册多个中间件,以便在请求处理过程中依次应用这些中间件。
- 例如,一个 Web 应用可能同时需要身份验证中间件、日志记录中间件和性能监控中间件。当请求到来时,首先经过身份验证中间件进行身份验证,然后经过性能监控中间件记录请求开始处理的时间,接着到达 Handler 进行实际处理,最后经过日志记录中间件记录请求处理结果和性能数据。
- 这种多中间件的设计使得系统可以灵活地组合不同的功能模块,满足复杂的业务需求。
-
对上层模块用户逻辑模块易用
- 中间件的设计应该考虑到上层模块(如用户逻辑模块)的易用性。这意味着中间件应该提供简单、直观的接口,使得上层模块可以方便地使用中间件提供的功能,而不需要复杂的配置或调用过程。
- 例如,在一个基于框架开发的 Web 应用中,开发者应该能够通过简单的配置或装饰器(decorator)来注册和使用中间件,而不需要深入了解中间件的内部实现细节。
- 这种易用性设计可以提高开发效率,降低系统的复杂性,使得开发者可以更专注于业务逻辑的实现。
中间件在系统设计中起到了至关重要的作用。它通过预处理和后处理逻辑,可以增强系统的功能(如权限验证、日志记录等)和安全性(如防止非法访问),同时保持系统的易用性,使得上层模块可以方便地使用这些功能。多个中间件的灵活组合可以满足复杂的业务需求,提高系统的可扩展性和可维护性。
第三层:路由层
这张图片展示了关于框架路由的一些技术要点,主要是 URL 匹配对应的处理函数(Handlers)。
-
静态路由:
- 示例:
/a/b/c、/a/b/d - 这些是固定路径的路由,没有参数。
- 示例:
-
参数路由:
- 示例:
/a/:id/c(/a/b/c、/a/d/c)、/*all - 这些路由包含参数,如
:id,可以匹配不同的值(比如可以是b也可以是d也可以是。。。)。/*all是一个通配符路由,用于匹配所有路径。/*all能够匹配任意形式的路由————比如既能匹配/a/b/c又能匹配/a/d/c
- 示例:
-
路由修复:
- 示例:
/a/b和/a/b/ - 这是处理路径结尾是否带斜杠的情况,确保这两种形式都能正确匹配。
- 示例:
-
冲突路由以及优先级:
-
示例:
/a/b、/:id/c -
这是处理当多个路由可能匹配同一 URL 时,如何确定优先级的问题。
-
- 冲突路由与优先级的概念
-
在 Web 开发中,路由系统用于将传入的 URL 请求映射到相应的处理函数。当多个路由可能匹配同一个 URL 时,就会出现冲突。例如,
/a/b和/:id/c这两个路由,/a/b是一个静态路由,而/:id/c是一个参数路由(:id可以取任意值)。如果有一个 URL 请求是/a/b,它既可以匹配/a/b,也可以匹配/:id/c(当id为b时)。这时就需要确定哪个路由应该优先处理这个请求。 -
- 确定优先级的方法
-
一般来说,在路由系统中,确定优先级有以下几种常见方法:
- 精确匹配优先:静态路由通常比参数路由具有更高的优先级。在上面的例子中,
/a/b是一个精确的路径,而/:id/c是一个带有参数的路径。因此,/a/b应该优先处理/a/b这个 URL 请求。 - 定义顺序优先:在某些路由系统中,路由的定义顺序决定了优先级。先定义的路由会优先被检查。例如,如果
/a/b先被定义,然后是/:id/c,那么/a/b会优先处理/a/b这个 URL 请求。 - 路由权重:有些路由系统允许为每个路由设置权重,权重高的路由会优先处理。这种方法比较灵活,但也增加了配置的复杂性。
- 精确匹配优先:静态路由通常比参数路由具有更高的优先级。在上面的例子中,
-
- 应用场景和意义
-
冲突路由和优先级的处理在实际开发中非常重要。例如,在一个电商网站中,可能有一个静态路由
/product/123用于显示特定产品的详情,同时有一个参数路由/product/:id用于处理所有产品详情页面。如果不处理好优先级,可能会导致错误的页面显示或者数据加载。正确处理冲突路由可以确保网站的路由系统稳定、高效地运行,为用户提供正确的页面和数据。
-
-
匹配 HTTP 方法:
- 这是处理不同 HTTP 方法(如 GET、POST 等)的路由。
-
多处理函数:
-
方便添加中间件
-
这是处理如何在路由中添加多个处理函数,通常用于添加中间件来处理请求。
-
- 多处理函数与中间件的概念
-
多处理函数:在路由系统中,多处理函数指的是可以为一个路由指定多个处理函数。这些处理函数会按照一定的顺序被调用,以处理传入的请求。
-
中间件:中间件是一种在请求到达最终处理函数之前(或响应返回之前)进行处理的函数。它可以用于诸如日志记录、身份验证、请求预处理、响应后处理等操作。中间件函数通常会在请求处理管道中被依次调用。
-
- 多处理函数在添加中间件中的应用
-
当我们想要为路由添加中间件时,多处理函数机制就非常有用。例如,考虑一个简单的 Web 应用程序有一个路由
/user/profile。 -
我们可能希望在处理
/user/profile这个路由的请求之前,进行以下操作:- 首先进行身份验证,确保用户已登录。这可以通过一个身份验证中间件来实现。
- 然后记录请求的日志,记录哪个用户在什么时间访问了这个页面。这可以通过一个日志记录中间件来实现。
-
使用多处理函数机制,我们可以将这些中间件函数和最终处理
/user/profile页面显示的函数都绑定到/user/profile这个路由上。 -
假设我们使用一个类似 Express.js 的框架,代码可能如下:
-
app.get('/user/profile',
// 身份验证中间件
function(req, res, next) {
if (req.user) {
next();
} else {
res.send('Please login first');
}
},
// 日志记录中间件
function(req, res, next) {
console.log(`${req.user.username} accessed /user/profile at ${new Date()}`);
next();
},
// 最终处理函数,显示用户个人资料页面
function(req, res) {
res.render('user/profile', { user: req.user });
}
);
-
在这个例子中,
/user/profile路由有三个处理函数:一个身份验证中间件、一个日志记录中间件和一个最终显示用户资料页面的函数。当有请求到达/user/profile时,这些函数会依次被调用。-
- 多处理函数机制的优势
-
模块化和可维护性:通过将不同的功能(如身份验证、日志记录等)分离到不同的中间件函数中,代码变得更加模块化和易于维护。
-
复用性:中间件函数可以在多个路由中复用。例如,身份验证中间件可能在多个需要用户登录的路由中都可以使用。
-
灵活的请求处理管道:可以根据需要灵活地添加、删除或重新排列中间件函数,以调整请求处理的流程。
-
这些内容通常涉及到 Web 开发中的路由机制,特别是在使用框架进行开发时,如何处理不同的 URL 路径和请求方法。
-
青铜级别:map [string] handlers
-
描述:使用
map[string]handlers来处理路由。这种方法简单直接,通过将 URL 路径与处理函数handlers进行映射来处理请求。 -
示例:
- 静态路由:
/a/b/c、/a/b/d - 参数路由:
/a/:id/c、/*all - 这里
/a/:id/c是参数路由,:id表示路径中的参数部分。/*all是一个通配符路由,用于匹配所有路径。
- 静态路由:
黄金级别:前缀匹配树
-
描述:使用前缀匹配树(Prefix Trie)来处理路由。这种方法更高效,适用于处理大量路由的情况。
-
示例:
-
考虑静态路由
/a/b/c和/a/b/d -
问题:如何处理带参数的路由注册?例如
/a/:id/b类型的路由。 -
图表解释:
-
图表展示了一个前缀匹配树的结构。
-
根节点(Root)的
path是/a/,label是/,fullPath是/a/。 -
从根节点向下,有一个节点的
path是b/,label是b,fullPath是/a/b/。 -
再往下,有两个叶子节点:
- 一个叶子节点的
path是c,label是c,fullPath是/a/b/c。 - 另一个叶子节点的
path是d,label是d,fullPath是/a/b/d。
- 一个叶子节点的
-
-
这种结构可以高效地处理路由匹配,特别是对于带参数的路由,可以通过在树中合适的位置插入节点来处理。
-
对比了两种路由处理方法:一种是简单的映射方法,另一种是更高效的前缀匹配树方法,并通过具体示例和图表展示了前缀匹配树的工作原理。
- 当一个 HTTP 请求到达时,首先通过外层 Map(根据 HTTP 方法作为key)进行初步筛选。
- 初步筛选后,进入前缀树(Trie)结构进行进一步的路由匹配。
- 前缀树的头节点是路由匹配的起点,通过前缀树的结构来精确匹配具体的路由。
- 明确需求:考虑清楚要解决什么问题,有哪些需求
- 业界调研:业界都有哪些解决方案可供参考
- 方案权衡:思考不同方案的取舍
- 方案评审:相关同学对不同方案做评审
- 确定开发:确定最合适的方案进行开发
路由层是URI进行选择执行的Handler(http在第七层,tcp在第四层)
第四层 协议层设计
抽象出合适的接口
-
抽象出合适的接口
- 图片中展示了一段 Go 语言代码,定义了一个名为
Server的接口。
- 图片中展示了一段 Go 语言代码,定义了一个名为
type Server interface {
Serve(c context.Context, conn network.Conn) error
}
-
定义了一个
Server接口,该接口包含一个Serve方法。 -
Serve方法接受两个参数:- 第一个参数是
context.Context类型的c,用于传递上下文信息。 - 第二个参数是
network.Conn类型的conn,用于处理网络连接。
- 第一个参数是
-
Serve方法返回一个error类型的值,表示可能出现的错误。
-
设计原则
- 第一条原则:不要在结构体类型内部存储上下文(Contexts),而是将上下文显式地传递给每个需要它的函数。上下文应该是第一个参数。
- 第二条原则:需要在连接上读写数据。
背景知识
-
在网络编程中,协议层设计至关重要。通过定义接口,可以确保不同的网络服务实现遵循相同的规范。
-
context.Context在 Go 语言中用于处理请求的上下文,例如超时控制、取消操作等。 -
network.Conn是 Go 语言中用于处理网络连接的接口,通过它可以进行数据的读写操作。
这张图片展示了在 Go 语言环境下如何进行协议层设计,通过定义Server接口和遵循特定的设计原则,确保网络服务的高效和可靠。
第五层 网络层设计(传输层)
netpoll:https://github.com/cloudwego/netpoll
针对网络模型进行设计
1. BIO(阻塞式 I/O)
- 在左侧展示了 BIO 的代码示例。
- 代码使用 Go 语言编写。
- 主要结构是一个
for循环,在循环中通过listener.Accept()接受连接,然后使用conn.Read(request)读取请求,接着处理请求(handle...部分被红色椭圆圈住,可能是处理请求的代码被省略了),最后使用conn.Write(response)发送响应。 - 这种模式下,每个连接在读取和写入时都会阻塞,直到操作完成。
2. NIO(非阻塞式 I/O)
- 在右侧展示了 NIO 的代码示例。
- 同样使用 Go 语言编写。
- 主要结构也是一个
for循环,但这里首先通过Monitor(conns)监控连接(conns可能是一个连接集合),有足够的监听器之后唤醒下面的func;然后在循环中遍历可读连接(readableConns),读取请求(conn.Read(request)),read可以读到足够的数据,就不会阻塞了;处理请求(handle...部分被红色椭圆圈住,可能是处理请求的代码被省略了),最后发送响应(conn.Write(response))。 - 这种模式下,连接的读取和写入操作不会阻塞,通过监控机制来处理多个连接。
3. 对比和意义
- 对比 BIO 和 NIO 的代码,展示了两种不同的网络层设计模式。
- BIO 模式简单直接,但在处理多个连接时可能会导致性能问题,因为每个连接都会阻塞。
- NIO 模式通过非阻塞操作和监控机制,可以更高效地处理多个连接,适合高并发场景。
- “go net” :这是指 Go 语言的网络包(net package),Go 语言提供了丰富的网络编程功能。
- 底层没有数据,read一直等待,等到超时或者连接关闭;write如果写数据一直没有写到底层。go net用户态管理,用户管理缓冲区意味着程序员需要手动处理数据的读取和写入操作,确保数据的完整性和正确性。
- 在 Go 语言的网络编程中,
net包提供了许多用于处理网络连接的类型和函数。 - 阻塞式 I/O(BIO)是一种传统的 I/O 处理方式,它在执行 I/O 操作时会阻塞当前线程,直到操作完成。
-
Reader 接口:
- 代码定义了一个名为
Reader的接口。 - 接口中包含一个方法
Peekn(int) ([][]byte, error) - 这个方法的作用是从缓冲区中查看指定长度的数据,返回一个字节切片的切片和一个错误。
- 代码定义了一个名为
-
Writer 接口:
-
代码定义了一个名为
Writer的接口。 -
接口中包含两个方法:
Malloc(n int) (buf []byte, err error):这个方法用于分配指定长度的缓冲区。Flush() error:这个方法用于将缓冲区的数据刷新(写入)出去。
-
-
netpoll 网络库在网络层设计中如何处理非阻塞 I/O 和缓冲区管理。