mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-06-13 23:57:38 +08:00
Compare commits
No commits in common. "8feb31a9903431195f733ae05366df3a9b36ad0b" and "a6e278d458a06fe92da353d291e13bf71855ff2b" have entirely different histories.
8feb31a990
...
a6e278d458
14 changed files with 10 additions and 796 deletions
20
README.md
20
README.md
|
|
@ -2,19 +2,9 @@
|
|||
|
||||
Touka(灯花) 是一个基于 Go 语言构建的多层次、高性能 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。
|
||||
|
||||
## 文档
|
||||
**想深入了解 Touka 吗?请阅读我们的 -> [深度指南 (about-touka.md)](about-touka.md)**
|
||||
|
||||
我们提供了详尽的文档来帮助您快速上手并深入了解 Touka:
|
||||
|
||||
- **[灯花框架简介 (introduction.md)](docs/introduction.md)**
|
||||
- **[快速开始 (quickstart.md)](docs/quickstart.md)**
|
||||
- **[路由系统 (routing.md)](docs/routing.md)**
|
||||
- **[上下文 Context (context.md)](docs/context.md)**
|
||||
- **[中间件 (middleware.md)](docs/middleware.md)**
|
||||
- **[统一错误处理 (error-handling.md)](docs/error-handling.md)**
|
||||
- **[静态文件与资源 (static-files.md)](docs/static-files.md)**
|
||||
- **[Server-Sent Events (sse.md)](docs/sse.md)**
|
||||
- **[高级特性与优化 (advanced.md)](docs/advanced.md)**
|
||||
这份深度指南包含了对框架设计哲学、核心功能(路由、上下文、中间件、错误处理等)的全面剖析,并提供了大量可直接使用的代码示例,帮助您快速上手并精通 Touka。
|
||||
|
||||
### 快速上手
|
||||
|
||||
|
|
@ -82,9 +72,11 @@ func main() {
|
|||
- [jwt](https://github.com/fenthope/jwt)
|
||||
- [带宽限制](https://github.com/fenthope/toukautil/blob/main/bandwithlimiter.go)
|
||||
|
||||
## 贡献
|
||||
## 文档与贡献
|
||||
|
||||
我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。
|
||||
* **深度指南:** **[about-touka.md](about-touka.md)**
|
||||
* **API 文档:** 访问 [pkg.go.dev/github.com/infinite-iroha/touka](https://pkg.go.dev/github.com/infinite-iroha/touka) 查看完整的 API 参考。
|
||||
* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。
|
||||
|
||||
## 相关项目
|
||||
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
# 高级特性与优化
|
||||
|
||||
本章节涵盖了 Touka 的一些深层特性以及在生产环境中的最佳实践。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. Context 池化
|
||||
|
||||
Touka 使用 `sync.Pool` 来重用 `touka.Context` 对象。这极大减少了每个请求产生的内存分配和 GC 压力。
|
||||
- **代价**: 您必须在处理器返回后立即停止对该 `Context` 指针的任何引用。
|
||||
- **解决方案**: 如果需要在后台 Goroutine 中使用请求数据,请预先提取所需数据(如 `c.Query` 的值),或者深拷贝该对象(不推荐)。
|
||||
|
||||
### 2. 预分配参数切片
|
||||
|
||||
在路由匹配过程中,Touka 会预分配路径参数切片,并根据路由深度进行缓存,从而在路由查找时实现几乎零分配。
|
||||
|
||||
## 优雅停机 (Graceful Shutdown)
|
||||
|
||||
在部署新版本时,我们希望服务器停止接收新请求,但能处理完当前正在进行的请求。
|
||||
|
||||
```go
|
||||
r := touka.Default()
|
||||
// ... 注册路由 ...
|
||||
|
||||
// 监听 SIGINT 和 SIGTERM 信号
|
||||
// 如果在 10 秒内未处理完,则强制关闭
|
||||
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
|
||||
log.Fatal("服务器退出异常:", err)
|
||||
}
|
||||
```
|
||||
|
||||
## 与标准库集成
|
||||
|
||||
Touka 遵循 `net/http` 哲学。您可以方便地使用现有的标准库组件。
|
||||
|
||||
### 适配 `http.HandlerFunc`
|
||||
|
||||
```go
|
||||
r.GET("/pprof/*any", touka.AdapterStdFunc(pprof.Index))
|
||||
```
|
||||
|
||||
### 手动注入
|
||||
|
||||
由于 `Engine` 实现了 `http.Handler` 接口,您可以将其挂载到任何地方。
|
||||
|
||||
```go
|
||||
s := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: r, // Engine 实例
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
s.ListenAndServe()
|
||||
```
|
||||
|
||||
## 自定义日志集成
|
||||
|
||||
Touka 默认集成了 `reco` 日志库。您可以自定义其输出行为。
|
||||
|
||||
```go
|
||||
logConfig := reco.Config{
|
||||
Level: reco.LevelInfo,
|
||||
Output: os.Stdout,
|
||||
Async: true, // 异步写入提高性能
|
||||
}
|
||||
r.SetLoggerCfg(logConfig)
|
||||
```
|
||||
|
||||
## 内存读取限制 (MaxReader)
|
||||
|
||||
为了防止恶意的大数据包攻击(如慢速 HTTP 攻击或内存溢出),Touka 内置了 `MaxReader` 机制。
|
||||
|
||||
```go
|
||||
// 设置全局最大读取限制(例如 2MB)
|
||||
r.SetMaxReader(2 << 20)
|
||||
```
|
||||
120
docs/context.md
120
docs/context.md
|
|
@ -1,120 +0,0 @@
|
|||
# 上下文 (Context)
|
||||
|
||||
`touka.Context` 是 Touka 框架中最重要的结构。它携带了关于当前 HTTP 请求的所有必要信息,并提供了一系列方法来解析请求和构建响应。
|
||||
|
||||
## 请求数据解析
|
||||
|
||||
### 查询参数 (Query Parameters)
|
||||
|
||||
```go
|
||||
// /welcome?firstname=Jane&lastname=Doe
|
||||
r.GET("/welcome", func(c *touka.Context) {
|
||||
firstname := c.DefaultQuery("firstname", "Guest")
|
||||
lastname := c.Query("lastname") // 快捷方式,不存在则为空
|
||||
|
||||
c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
|
||||
})
|
||||
```
|
||||
|
||||
### 表单数据 (Form Data)
|
||||
|
||||
```go
|
||||
r.POST("/form_post", func(c *touka.Context) {
|
||||
message := c.PostForm("message")
|
||||
nick := c.DefaultPostForm("nick", "anonymous")
|
||||
|
||||
c.JSON(http.StatusOK, touka.H{
|
||||
"status": "posted",
|
||||
"message": message,
|
||||
"nick": nick,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### JSON 绑定
|
||||
|
||||
Touka 提供了非常便捷的 JSON 绑定功能,它会自动解析请求体并填充到结构体中,同时进行基本的验证。
|
||||
|
||||
```go
|
||||
type LoginRequest struct {
|
||||
User string `json:"user" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
r.POST("/login", func(c *touka.Context) {
|
||||
var json LoginRequest
|
||||
if err := c.ShouldBindJSON(&json); err != nil {
|
||||
c.JSON(http.StatusBadRequest, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if json.User != "admin" || json.Password != "123" {
|
||||
c.JSON(http.StatusUnauthorized, touka.H{"status": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, touka.H{"status": "you are logged in"})
|
||||
})
|
||||
```
|
||||
|
||||
## 响应构建
|
||||
|
||||
### 基础格式
|
||||
|
||||
Touka 支持多种响应格式:
|
||||
|
||||
```go
|
||||
// JSON
|
||||
c.JSON(http.StatusOK, touka.H{"message": "hey"})
|
||||
|
||||
// 字符串 (支持格式化)
|
||||
c.String(http.StatusOK, "welcome %s", name)
|
||||
|
||||
// 纯文本
|
||||
c.Text(http.StatusOK, "just text")
|
||||
|
||||
// HTML 模板
|
||||
c.HTML(http.StatusOK, "index.tmpl", touka.H{"title": "Main website"})
|
||||
```
|
||||
|
||||
### 文件与流
|
||||
|
||||
```go
|
||||
// 服务本地文件
|
||||
c.File("/local/file.go")
|
||||
|
||||
// 将文件内容作为响应体(不触发下载)
|
||||
c.SetRespBodyFile(http.StatusOK, "config.json")
|
||||
|
||||
// 写入数据流
|
||||
c.WriteStream(reader)
|
||||
```
|
||||
|
||||
### 重定向
|
||||
|
||||
```go
|
||||
c.Redirect(http.StatusMovedPermanently, "http://google.com/")
|
||||
```
|
||||
|
||||
## 数据传递 (Keys/Values)
|
||||
|
||||
您可以在中间件和处理器之间共享数据。
|
||||
|
||||
```go
|
||||
// 在中间件中设置
|
||||
c.Set("user_id", 12345)
|
||||
|
||||
// 在处理器中获取
|
||||
id, exists := c.Get("user_id")
|
||||
val := c.MustGet("user_id").(int)
|
||||
```
|
||||
|
||||
## 状态管理
|
||||
|
||||
- `c.Abort()`: 停止执行后续的处理器/中间件。
|
||||
- `c.Next()`: 执行后续的处理链。这常用于中间件中,在执行完某些前置逻辑后,显式调用 `Next`,并在其返回后执行后置逻辑。
|
||||
|
||||
## 对象池化
|
||||
|
||||
为了提高性能,Touka 的 Context 对象是复用的。
|
||||
**重要提示:不要在 Goroutine 中持久化持有 `touka.Context` 指针。如果您需要在 Goroutine 中使用请求数据,请务必在派生 Goroutine 前提取所需的值。**
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
# 统一错误处理
|
||||
|
||||
Touka 的核心优势之一是其**高度统一且自动化**的错误处理机制。
|
||||
|
||||
## 全局错误处理器
|
||||
|
||||
您可以为整个引擎设置一个统一的错误处理器。无论错误来自您的业务代码,还是来自框架内部(如 404/405),甚至是来自标准库的 `http.FileServer`,最终都会流向这个处理器。
|
||||
|
||||
```go
|
||||
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
|
||||
// 您可以在这里定义统一的错误响应格式
|
||||
c.JSON(code, touka.H{
|
||||
"code": code,
|
||||
"message": http.StatusText(code),
|
||||
"detail": err.Error(),
|
||||
})
|
||||
|
||||
// 也可以记录日志
|
||||
c.Errorf("HTTP Error %d: %v", code, err)
|
||||
})
|
||||
```
|
||||
|
||||
## `errorCapturingResponseWriter` (ecw) 的工作原理
|
||||
|
||||
很多时候,我们希望拦截标准库组件(如 `http.FileServer`)产生的错误,以便能够应用我们自定义的 404 页面或 JSON 响应。
|
||||
|
||||
Touka 通过包装标准的 `http.ResponseWriter` 实现了这一点:
|
||||
|
||||
1. **拦截写入**: 当 `http.FileServer` 等组件尝试调用 `WriteHeader(statusCode)` 且 `statusCode >= 400` 时,Touka 的包装器会捕获这个状态码。
|
||||
2. **阻止输出**: 它会阻止组件继续向响应体写入默认的错误消息(如 `404 page not found`)。
|
||||
3. **回调处理**: 包装器随后会调用全局配置的 `ErrorHandler`。
|
||||
|
||||
这意味着您可以像这样轻松地为静态文件服务设置自定义错误处理:
|
||||
|
||||
```go
|
||||
r := touka.New()
|
||||
|
||||
// 设置全局错误处理
|
||||
r.SetErrorHandler(func(c *touka.Context, code int, err error) {
|
||||
if code == http.StatusNotFound {
|
||||
c.String(http.StatusNotFound, "找不到此资源")
|
||||
return
|
||||
}
|
||||
c.String(code, "发生错误: %v", err)
|
||||
})
|
||||
|
||||
// 服务静态目录
|
||||
r.StaticDir("/static", "./public")
|
||||
// 如果用户访问 /static/missing-file.jpg,他将看到 "找不到此资源"
|
||||
```
|
||||
|
||||
## 手动触发错误处理
|
||||
|
||||
您也可以在处理器中通过 `c.ErrorUseHandle` 手动触发此流程:
|
||||
|
||||
```go
|
||||
r.GET("/item/:id", func(c *touka.Context) {
|
||||
item, err := db.GetItem(c.Param("id"))
|
||||
if err != nil {
|
||||
// 调用全局错误处理器
|
||||
c.ErrorUseHandle(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
})
|
||||
```
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Touka (灯花) 简介
|
||||
|
||||
Touka 是一个基于 Go 语言构建的高性能、多层次 Web 框架。其设计目标是为开发者提供**更直接的控制、有效的扩展能力,以及针对特定场景的行为优化**。
|
||||
|
||||
## 为什么选择 Touka?
|
||||
|
||||
在众多的 Go Web 框架中,Touka 致力于在保持类似 Gin 的易用性的同时,提供更深度的底层控制和更强大的统一错误处理机制。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **高性能路由**: 基于基数树(Radix Tree)实现的路由系统,支持高效的路径匹配、参数捕获和通配符路由。
|
||||
- **极致性能优化**:
|
||||
- **Context 复用**: 通过对象池(sync.Pool)管理 `touka.Context`,显著减少 GC 压力。
|
||||
- **最小化内存分配**: 在热点路径上尽可能减少临时对象的产生。
|
||||
- **统一错误处理**: 独创的 `errorCapturingResponseWriter` 机制,能够捕获包括标准库 `http.FileServer` 在内的所有组件产生的错误状态码,并交由全局处理器统一处理。
|
||||
- **无缝集成 SSE**: 内置对 Server-Sent Events 的支持,提供简单易用的回调式 API 和高度灵活的通道式 API。
|
||||
- **静态资源增强**: 针对本地文件、目录以及 Go 嵌入式文件系统(embed.FS)提供了开箱即用的支持。
|
||||
- **标准库兼容**: 提供了适配器,可以轻松将现有的 `http.Handler` 或 `http.HandlerFunc` 集成到 Touka 中。
|
||||
|
||||
## 设计哲学
|
||||
|
||||
1. **直接性**: 框架 API 设计直观,尽可能减少开发者需要记忆的概念。
|
||||
2. **可扩展性**: 每一个核心组件(如日志、错误处理器、渲染器)都是可插拔或可定制的。
|
||||
3. **健壮性**: 内置优雅停机支持,确保在服务器更新或关闭时请求能得到正确处理。
|
||||
|
||||
Touka 不仅仅是一个处理 HTTP 请求的工具,它还是构建现代化、可维护、高可用 Web 应用的坚实基础。
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# 中间件 (Middleware)
|
||||
|
||||
中间件是运行在 HTTP 请求处理链中的函数。它们可以拦截请求、修改数据、控制流向(通过 `c.Next()` 或 `c.Abort()`),并执行通用的前置/后置逻辑。
|
||||
|
||||
## 如何使用中间件
|
||||
|
||||
### 全局中间件
|
||||
|
||||
全局中间件应用于所有注册的路由。
|
||||
|
||||
```go
|
||||
r := touka.New()
|
||||
r.Use(touka.Recovery()) // 崩溃恢复
|
||||
r.Use(MyCustomLogger()) // 自定义日志
|
||||
```
|
||||
|
||||
### 路由组中间件
|
||||
|
||||
仅应用于特定组下的路由。
|
||||
|
||||
```go
|
||||
api := r.Group("/api")
|
||||
api.Use(AuthMiddleware())
|
||||
{
|
||||
api.GET("/user", handleUser)
|
||||
}
|
||||
```
|
||||
|
||||
## 编写自定义中间件
|
||||
|
||||
中间件的函数签名是 `touka.HandlerFunc`。
|
||||
|
||||
### 示例:请求计时器
|
||||
|
||||
```go
|
||||
func TimerMiddleware() touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
// --- 前置逻辑 ---
|
||||
start := time.Now()
|
||||
|
||||
// 执行处理链中的下一个函数
|
||||
c.Next()
|
||||
|
||||
// --- 后置逻辑 ---
|
||||
duration := time.Since(start)
|
||||
log.Printf("Request %s %s took %v", c.Request.Method, c.Request.URL.Path, duration)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:简单的 API 密钥验证
|
||||
|
||||
```go
|
||||
func APIKeyAuth() touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
apiKey := c.GetHeader("X-API-KEY")
|
||||
if apiKey != "secret-token" {
|
||||
// 验证失败,返回错误并中止后续逻辑
|
||||
c.JSON(http.StatusUnauthorized, touka.H{"error": "Invalid API Key"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证通过,继续执行
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内置中间件
|
||||
|
||||
- **Recovery**: 捕获任何发生的 panic,恢复运行并返回 500 错误。它还负责调用全局错误处理器。
|
||||
|
||||
Touka 的设计非常精简,许多扩展功能(如 Gzip, JWT, Sessions)由外部或第三方库提供,您可以轻松通过 `r.Use()` 集成它们。
|
||||
|
||||
## 条件中间件 (Conditional Middleware)
|
||||
|
||||
Touka 支持根据布尔条件动态启用或禁用中间件。这在根据环境配置启用插件时非常有用。
|
||||
|
||||
### `UseIf`
|
||||
|
||||
```go
|
||||
// 仅在 Debug 模式下启用日志
|
||||
r.Use(r.UseIf(config.IsDebug, MyDebugLogger))
|
||||
```
|
||||
|
||||
### `UseChainIf` (条件中间件链)
|
||||
|
||||
如果您有一组相关的中间件需要根据同一条件启用,可以使用 `UseChainIf`。
|
||||
|
||||
```go
|
||||
r.Use(r.UseChainIf(config.EnableMetrics,
|
||||
MetricsMiddleware,
|
||||
PrometheusMiddleware,
|
||||
MonitoringMiddleware,
|
||||
))
|
||||
```
|
||||
|
||||
这些方法利用了 `MiddlewareXFunc`(即返回 `HandlerFunc` 的工厂函数),确保中间件实例按需创建或高效复用。
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
# 快速开始
|
||||
|
||||
本指南将帮助您在几分钟内启动并运行一个 Touka 应用。
|
||||
|
||||
## 安装
|
||||
|
||||
确保您的环境中已经安装了 Go 1.25 或更高版本。
|
||||
|
||||
在您的项目目录中运行:
|
||||
|
||||
```bash
|
||||
go get github.com/infinite-iroha/touka
|
||||
```
|
||||
|
||||
## 基础示例
|
||||
|
||||
创建一个 `main.go` 文件,并粘贴以下代码:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
"log"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 创建默认引擎(包含 Recovery 中间件)
|
||||
r := touka.Default()
|
||||
|
||||
// 2. 注册一个简单的 GET 路由
|
||||
r.GET("/ping", func(c *touka.Context) {
|
||||
c.JSON(http.StatusOK, touka.H{
|
||||
"message": "pong",
|
||||
"time": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// 3. 注册带参数的路由
|
||||
r.GET("/hello/:name", func(c *touka.Context) {
|
||||
name := c.Param("name")
|
||||
c.String(http.StatusOK, "Hello, %s!", name)
|
||||
})
|
||||
|
||||
// 4. 启动服务器并监听 8080 端口
|
||||
log.Println("Touka server is running on :8080")
|
||||
if err := r.Run(":8080"); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 运行应用
|
||||
|
||||
执行以下命令启动服务器:
|
||||
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
现在,您可以访问:
|
||||
- `http://localhost:8080/ping`
|
||||
- `http://localhost:8080/hello/World`
|
||||
|
||||
## 优雅停机
|
||||
|
||||
在生产环境中,我们推荐使用 `RunShutdown` 方法来启动服务器,它会监听系统信号并在关闭前等待正在处理的请求完成。
|
||||
|
||||
```go
|
||||
// 等待 10 秒以处理剩余请求
|
||||
if err := r.RunShutdown(":8080", 10*time.Second); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
```
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
# 路由系统
|
||||
|
||||
Touka 拥有一个强大且灵活的路由系统,底层基于高性能的基数树(Radix Tree)实现。
|
||||
|
||||
## 基础路由
|
||||
|
||||
您可以为所有标准的 HTTP 方法注册处理器:
|
||||
|
||||
```go
|
||||
r.GET("/someGet", handle)
|
||||
r.POST("/somePost", handle)
|
||||
r.PUT("/somePut", handle)
|
||||
r.DELETE("/someDelete", handle)
|
||||
r.PATCH("/somePatch", handle)
|
||||
r.HEAD("/someHead", handle)
|
||||
r.OPTIONS("/someOptions", handle)
|
||||
|
||||
// 注册所有上述方法的路由
|
||||
r.ANY("/any", handle)
|
||||
```
|
||||
|
||||
## 路径参数 (Named Parameters)
|
||||
|
||||
使用冒号 `:` 定义路径参数。参数值可以通过 `c.Param(key)` 获取。
|
||||
|
||||
```go
|
||||
// 匹配 /user/john, 不匹配 /user/ 或 /user/john/send
|
||||
r.GET("/user/:name", func(c *touka.Context) {
|
||||
name := c.Param("name")
|
||||
c.String(http.StatusOK, "Hello %s", name)
|
||||
})
|
||||
|
||||
// 匹配 /user/john/send
|
||||
r.GET("/user/:name/:action", func(c *touka.Context) {
|
||||
name := c.Param("name")
|
||||
action := c.Param("action")
|
||||
c.String(http.StatusOK, "%s is doing %s", name, action)
|
||||
})
|
||||
```
|
||||
|
||||
## 通配符路由 (Catch-all Parameters)
|
||||
|
||||
使用星号 `*` 定义通配符路由,它会捕获路径中该位置之后的所有内容。
|
||||
|
||||
```go
|
||||
// 匹配 /src/main.go, /src/scripts/app.js 等
|
||||
r.GET("/src/*filepath", func(c *touka.Context) {
|
||||
path := c.Param("filepath")
|
||||
c.String(http.StatusOK, "Viewing file: %s", path)
|
||||
})
|
||||
```
|
||||
|
||||
## 路由组 (RouterGroup)
|
||||
|
||||
路由组允许您共享公共路径前缀或中间件,使代码结构更清晰。
|
||||
|
||||
```go
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/login", loginEndpoint)
|
||||
v1.GET("/submit", submitEndpoint)
|
||||
}
|
||||
|
||||
v2 := r.Group("/api/v2")
|
||||
v2.Use(AuthMiddleware()) // 仅应用于 v2 组
|
||||
{
|
||||
v2.POST("/data", dataEndpoint)
|
||||
}
|
||||
```
|
||||
|
||||
## 路由行为配置
|
||||
|
||||
Touka 允许您自定义路由匹配的行为:
|
||||
|
||||
- **RedirectTrailingSlash**: 如果启用(默认),请求 `/foo/` 会被重定向到 `/foo`(如果只有后者注册了),反之亦然。
|
||||
- **RedirectFixedPath**: 如果启用(默认),引擎会尝试修复路径大小写或移除多余的斜杠并重定向。
|
||||
- **HandleMethodNotAllowed**: 如果启用,当请求路径匹配但方法不匹配时,返回 405 而非 404。
|
||||
|
||||
```go
|
||||
r := touka.New()
|
||||
r.RedirectTrailingSlash = true
|
||||
r.HandleMethodNotAllowed = true
|
||||
```
|
||||
|
||||
## 获取已注册路由信息
|
||||
|
||||
您可以使用 `GetRouterInfo` 获取当前引擎中所有已注册路由的列表。
|
||||
|
||||
```go
|
||||
routes := r.GetRouterInfo()
|
||||
for _, route := range routes {
|
||||
fmt.Printf("Method: %s, Path: %s\n", route.Method, route.Path)
|
||||
}
|
||||
```
|
||||
131
docs/sse.md
131
docs/sse.md
|
|
@ -1,131 +0,0 @@
|
|||
# Server-Sent Events (SSE)
|
||||
|
||||
Server-Sent Events 允许服务器向客户端实时推送数据。Touka 对此提供了原生且易用的支持。
|
||||
|
||||
## 核心结构:`Event`
|
||||
|
||||
`Event` 结构体代表一个 SSE 消息:
|
||||
|
||||
```go
|
||||
type Event struct {
|
||||
Event string // 事件名称
|
||||
Data string // 数据内容 (支持多行)
|
||||
Id string // 事件 ID
|
||||
Retry string // 重连时间 (毫秒)
|
||||
}
|
||||
```
|
||||
|
||||
## 模式一:回调模式 (EventStream)
|
||||
|
||||
这是最推荐的使用方式,它更简单且能自动管理连接生命周期。
|
||||
|
||||
```go
|
||||
r.GET("/events", func(c *touka.Context) {
|
||||
c.EventStream(func(w io.Writer) bool {
|
||||
// 构建事件
|
||||
event := touka.Event{
|
||||
Data: "现在的时间是: " + time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// 渲染并写入
|
||||
if err := event.Render(w); err != nil {
|
||||
return false // 发生写入错误(如客户端断开),返回 false 停止流
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
return true // 返回 true 继续下一次循环
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 模式二:通道模式 (EventStreamChan)
|
||||
|
||||
如果您需要更高级的并发控制(例如从多个异步源接收数据),可以使用通道模式。
|
||||
|
||||
```go
|
||||
r.GET("/events-chan", func(c *touka.Context) {
|
||||
eventChan, errChan := c.EventStreamChan()
|
||||
|
||||
// 监听错误/断开连接
|
||||
go func() {
|
||||
if err := <-errChan; err != nil {
|
||||
log.Printf("SSE 错误: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 发送数据
|
||||
go func() {
|
||||
defer close(eventChan) // 务必在结束时关闭
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
default:
|
||||
eventChan <- touka.Event{
|
||||
Data: fmt.Sprintf("消息 #%d", i),
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **资源回收**: 确保在 `EventStreamChan` 模式下正确监听 `c.Request.Context().Done()` 以避免 Goroutine 泄漏。
|
||||
2. **数据格式**: SSE 协议要求数据为 UTF-8。Touka 的 `Render` 方法会自动处理多行数据并加上必要的 `data:` 前缀。
|
||||
3. **超时管理**: SSE 连接通常是长连接,请确保您的反向代理(如 Nginx)配置了足够大的写超时时间。
|
||||
|
||||
## 优雅关闭与资源清理
|
||||
|
||||
在长连接场景下,正确处理客户端断开或服务器关闭信号至关重要,以防止资源泄漏。
|
||||
|
||||
### 示例:监听 Context 取消信号
|
||||
|
||||
```go
|
||||
r.GET("/events-graceful", func(c *touka.Context) {
|
||||
// 设置响应头(如果手动处理,EventStream 会自动设置)
|
||||
|
||||
// 使用 Context 的 Done 通道来感知连接关闭
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 启动一个用于模拟数据生成的循环
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
c.EventStream(func(w io.Writer) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 收到优雅关闭信号(客户端离开或服务器正在关闭)
|
||||
fmt.Println("SSE 连接正在关闭,开始清理资源...")
|
||||
return false // 返回 false 告知框架停止流
|
||||
|
||||
case t := <-ticker.C:
|
||||
event := touka.Event{
|
||||
Data: "Tick at " + t.Format(time.RFC3339),
|
||||
}
|
||||
if err := event.Render(w); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
fmt.Println("SSE 连接已彻底释放")
|
||||
})
|
||||
```
|
||||
|
||||
在该示例中,我们显式地在回调函数中使用 `select` 监听 `ctx.Done()`。虽然 Touka 的 `EventStream` 内部也会检查此信号,但在回调内部自行处理可以执行更复杂的清理逻辑(如关闭数据库连接、停止特定的 Goroutine 等)。
|
||||
|
||||
### 为什么会出现 "context deadline exceeded"?
|
||||
|
||||
如果您在优雅停机时遇到 `context deadline exceeded` 错误,通常是因为 SSE 连接仍然活跃,而 `http.Server.Shutdown` 正在等待它们结束。
|
||||
|
||||
在 Touka 的新版本中,我们通过 `BaseContext` 将 `Engine` 的关闭信号注入到了每个请求的 `Context` 中。这意味着:
|
||||
1. 当服务器收到关闭信号时,`engine.shutdownCtx` 会被取消。
|
||||
2. 随后,所有活跃请求的 `c.Request.Context()` 也会收到取消信号。
|
||||
3. 您的 SSE 处理器中的 `case <-c.Request.Context().Done():` 会立即触发,从而优雅地结束连接。
|
||||
|
||||
**注意:** 请务必使用 `RunShutdown`、`RunTLS` 或 `RunTLSRedir` 来启动服务器,以便框架能自动管理这些信号。
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# 静态文件与资源
|
||||
|
||||
Touka 提供了多种方式来服务静态文件,这些方法都集成了 Touka 的统一错误处理机制。
|
||||
|
||||
## 服务本地目录
|
||||
|
||||
`StaticDir` 方法将 URL 路径映射到本地文件系统目录。
|
||||
|
||||
```go
|
||||
// 访问 /assets/js/main.js 将读取 ./static/js/main.js
|
||||
r.StaticDir("/assets", "./static")
|
||||
```
|
||||
|
||||
## 服务单个文件
|
||||
|
||||
`StaticFile` 用于将特定的 URL 映射到单个本地文件。
|
||||
|
||||
```go
|
||||
r.StaticFile("/favicon.ico", "./resources/favicon.ico")
|
||||
```
|
||||
|
||||
## 集成 Go 嵌入式资源 (embed.FS)
|
||||
|
||||
使用 Go 1.16+ 的 `embed` 特性,您可以将整个静态前端项目编译进二进制文件中。
|
||||
|
||||
```go
|
||||
//go:embed dist/*
|
||||
var content embed.FS
|
||||
|
||||
func main() {
|
||||
r := touka.Default()
|
||||
|
||||
// 剥离 "dist" 前缀并包装为 http.FS
|
||||
fsroot, _ := fs.Sub(content, "dist")
|
||||
|
||||
// 使用 StaticFS 提供服务
|
||||
r.StaticFS("/static", http.FS(fsroot))
|
||||
|
||||
// 您也可以使用 StaticFS 服务根路径
|
||||
// r.StaticFS("/", http.FS(fsroot))
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
## 未匹配路径作为文件服务 (UnMatchFS)
|
||||
|
||||
这是一个独特的功能:当没有任何 API 路由匹配时,尝试从指定的文件系统中查找并返回文件。这非常适合用于单页应用(SPA)的部署。
|
||||
|
||||
```go
|
||||
r := touka.New()
|
||||
r.SetUnMatchFS(http.Dir("./frontend/dist"), true)
|
||||
|
||||
// API 路由
|
||||
r.GET("/api/status", handleStatus)
|
||||
|
||||
// 如果请求 /index.html 且没有 /index.html 的路由,
|
||||
// 则会从 ./frontend/dist/index.html 读取。
|
||||
```
|
||||
|
||||
## 性能提示
|
||||
|
||||
对于高负载的静态资源分发,虽然 Touka 表现出色,但我们仍建议在生产环境中使用 Nginx 或 CDN 站在 Touka 前面来处理静态文件,让 Touka 专注于处理动态逻辑。
|
||||
10
engine.go
10
engine.go
|
|
@ -67,9 +67,6 @@ type Engine struct {
|
|||
Protocols ProtocolsConfig //协议版本配置
|
||||
useDefaultProtocols bool //是否使用默认协议
|
||||
|
||||
shutdownCtx context.Context
|
||||
shutdownCancel context.CancelFunc
|
||||
|
||||
// ServerConfigurator 允许在服务器启动前对其进行自定义配置
|
||||
// 例如,设置 ReadTimeout, WriteTimeout 等
|
||||
ServerConfigurator func(*http.Server)
|
||||
|
|
@ -210,7 +207,6 @@ func New() *Engine {
|
|||
TLSServerConfigurator: nil,
|
||||
GlobalMaxRequestBodySize: -1,
|
||||
}
|
||||
engine.shutdownCtx, engine.shutdownCancel = context.WithCancel(context.Background())
|
||||
//engine.SetProtocols(GetDefaultProtocolsConfig())
|
||||
engine.SetDefaultProtocols()
|
||||
engine.SetLoggerCfg(defaultLogRecoConfig)
|
||||
|
|
@ -770,9 +766,3 @@ func (engine *Engine) handleRequest(c *Context) {
|
|||
c.Next() // 执行处理函数链
|
||||
//c.Writer.Flush() // 确保所有缓冲的响应数据被发送
|
||||
}
|
||||
|
||||
// Context 返回 Engine 的根上下文, 该上下文在服务器优雅关闭时会被取消.
|
||||
// 它可以用于在长连接 (如 SSE) 中监听关闭信号.
|
||||
func (engine *Engine) Context() context.Context {
|
||||
return engine.shutdownCtx
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -1,11 +1,11 @@
|
|||
module github.com/infinite-iroha/touka
|
||||
|
||||
go 1.26
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.2
|
||||
github.com/WJQSERVER/wanf v0.0.6
|
||||
github.com/WJQSERVER/wanf v0.0.3
|
||||
github.com/fenthope/reco v0.0.4
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -2,8 +2,8 @@ github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbe
|
|||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok=
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk=
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y=
|
||||
github.com/WJQSERVER/wanf v0.0.6 h1:tB6Bsl7bg5uuJ4cn4l1Ctn9VvjNRE5/W0yAj3Z6367I=
|
||||
github.com/WJQSERVER/wanf v0.0.6/go.mod h1:zV0AQydpfiGsV2CcIy90SxSbiJgcvP3vinBJr0ZW3qs=
|
||||
github.com/WJQSERVER/wanf v0.0.3 h1:OqhG7ETiR5Knqr0lmbb+iUMw9O7re2vEogjVf06QevM=
|
||||
github.com/WJQSERVER/wanf v0.0.3/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE=
|
||||
github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM=
|
||||
github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
|
|
|
|||
16
serve.go
16
serve.go
|
|
@ -224,11 +224,7 @@ func (engine *Engine) RunShutdown(addr string, timeouts ...time.Duration) error
|
|||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: engine,
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return engine.shutdownCtx
|
||||
},
|
||||
}
|
||||
srv.RegisterOnShutdown(engine.shutdownCancel)
|
||||
|
||||
// 应用框架的默认配置和用户提供的自定义配置
|
||||
//engine.applyDefaultServerConfig(srv)
|
||||
|
|
@ -245,11 +241,7 @@ func (engine *Engine) RunShutdownWithContext(addr string, ctx context.Context, t
|
|||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: engine,
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return engine.shutdownCtx
|
||||
},
|
||||
}
|
||||
srv.RegisterOnShutdown(engine.shutdownCancel)
|
||||
|
||||
// 应用框架的默认配置和用户提供的自定义配置
|
||||
//engine.applyDefaultServerConfig(srv)
|
||||
|
|
@ -278,11 +270,7 @@ func (engine *Engine) RunTLS(addr string, tlsConfig *tls.Config, timeouts ...tim
|
|||
Addr: addr,
|
||||
Handler: engine,
|
||||
TLSConfig: tlsConfig,
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return engine.shutdownCtx
|
||||
},
|
||||
}
|
||||
srv.RegisterOnShutdown(engine.shutdownCancel)
|
||||
|
||||
// 应用框架的默认配置和用户提供的自定义配置
|
||||
// 优先使用 TLSServerConfigurator,如果未设置,则回退到通用的 ServerConfigurator
|
||||
|
|
@ -316,11 +304,7 @@ func (engine *Engine) RunTLSRedir(httpAddr, httpsAddr string, tlsConfig *tls.Con
|
|||
Addr: httpsAddr,
|
||||
Handler: engine,
|
||||
TLSConfig: tlsConfig,
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return engine.shutdownCtx
|
||||
},
|
||||
}
|
||||
httpsSrv.RegisterOnShutdown(engine.shutdownCancel)
|
||||
//engine.applyDefaultServerConfig(httpsSrv)
|
||||
if engine.TLSServerConfigurator != nil {
|
||||
engine.TLSServerConfigurator(httpsSrv)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue