diff --git a/README.md b/README.md index a449962..3ab971f 100644 --- a/README.md +++ b/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 参考。 +* **贡献:** 我们欢迎任何形式的贡献,无论是错误报告、功能建议还是代码提交。请遵循项目的贡献指南。 ## 相关项目 diff --git a/docs/advanced.md b/docs/advanced.md deleted file mode 100644 index 613b180..0000000 --- a/docs/advanced.md +++ /dev/null @@ -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) -``` diff --git a/docs/context.md b/docs/context.md deleted file mode 100644 index b67bec7..0000000 --- a/docs/context.md +++ /dev/null @@ -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 前提取所需的值。** diff --git a/docs/error-handling.md b/docs/error-handling.md deleted file mode 100644 index 5b53579..0000000 --- a/docs/error-handling.md +++ /dev/null @@ -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) -}) -``` diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index d1aec3e..0000000 --- a/docs/introduction.md +++ /dev/null @@ -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 应用的坚实基础。 diff --git a/docs/middleware.md b/docs/middleware.md deleted file mode 100644 index f13de8e..0000000 --- a/docs/middleware.md +++ /dev/null @@ -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` 的工厂函数),确保中间件实例按需创建或高效复用。 diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index 6006e54..0000000 --- a/docs/quickstart.md +++ /dev/null @@ -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) -} -``` diff --git a/docs/routing.md b/docs/routing.md deleted file mode 100644 index 17897d8..0000000 --- a/docs/routing.md +++ /dev/null @@ -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) -} -``` diff --git a/docs/sse.md b/docs/sse.md deleted file mode 100644 index bafc553..0000000 --- a/docs/sse.md +++ /dev/null @@ -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` 来启动服务器,以便框架能自动管理这些信号。 diff --git a/docs/static-files.md b/docs/static-files.md deleted file mode 100644 index 46954c8..0000000 --- a/docs/static-files.md +++ /dev/null @@ -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 专注于处理动态逻辑。 diff --git a/engine.go b/engine.go index 1e7bb18..0a95765 100644 --- a/engine.go +++ b/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 -} diff --git a/go.mod b/go.mod index b48afdc..f9d10a9 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 0f09a8c..b75fec4 100644 --- a/go.sum +++ b/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= diff --git a/serve.go b/serve.go index 6a4cf2a..7e05b8c 100644 --- a/serve.go +++ b/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)