Compare commits

..

No commits in common. "f1ac0dd6ff0046b4ca12babe024a7da9d63ffb19" and "016df0efe40dd6b99317fc45587a09164e3cfb13" have entirely different histories.

5 changed files with 26 additions and 248 deletions

View file

@ -23,7 +23,6 @@ import (
"sync"
"time"
"github.com/WJQSERVER/wanf"
"github.com/fenthope/reco"
"github.com/go-json-experiment/json"
@ -43,7 +42,7 @@ type Context struct {
index int8 // 当前执行到处理链的哪个位置
mu sync.RWMutex
Keys map[string]any // 用于在中间件之间传递数据
Keys map[string]interface{} // 用于在中间件之间传递数据
Errors []error // 用于收集处理过程中的错误
@ -78,18 +77,20 @@ func (c *Context) reset(w http.ResponseWriter, req *http.Request) {
} else {
c.Writer = newResponseWriter(w)
}
//c.Writer = newResponseWriter(w)
c.Request = req
c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组
c.handlers = nil
c.index = -1 // 初始为 -1`Next()` 将其设置为 0
c.Keys = make(map[string]any) // 每次请求重新创建 map避免数据污染
c.Keys = make(map[string]interface{}) // 每次请求重新创建 map避免数据污染
c.Errors = c.Errors[:0] // 清空 Errors 切片
c.queryCache = nil // 清空查询参数缓存
c.formCache = nil // 清空表单数据缓存
c.ctx = req.Context() // 使用请求的上下文,继承其取消信号和值
c.sameSite = http.SameSiteDefaultMode // 默认 SameSite 模式
c.MaxRequestBodySize = c.engine.GlobalMaxRequestBodySize
// c.HTTPClient 和 c.engine 保持不变,它们引用 Engine 实例的成员
}
// Next 在处理链中执行下一个处理函数
@ -121,10 +122,10 @@ func (c *Context) AbortWithStatus(code int) {
// Set 将一个键值对存储到 Context 中
// 这是一个线程安全的操作,用于在中间件之间传递数据
func (c *Context) Set(key string, value any) {
func (c *Context) Set(key string, value interface{}) {
c.mu.Lock() // 加写锁
if c.Keys == nil {
c.Keys = make(map[string]any)
c.Keys = make(map[string]interface{})
}
c.Keys[key] = value
c.mu.Unlock() // 解写锁
@ -132,7 +133,7 @@ func (c *Context) Set(key string, value any) {
// Get 从 Context 中获取一个值
// 这是一个线程安全的操作
func (c *Context) Get(key string) (value any, exists bool) {
func (c *Context) Get(key string) (value interface{}, exists bool) {
c.mu.RLock() // 加读锁
value, exists = c.Keys[key]
c.mu.RUnlock() // 解读锁
@ -207,7 +208,7 @@ func (c *Context) GetDuration(key string) (value time.Duration, exists bool) {
// MustGet 从 Context 中获取一个值,如果不存在则 panic
// 适用于确定值一定存在的场景
func (c *Context) MustGet(key string) any {
func (c *Context) MustGet(key string) interface{} {
if value, exists := c.Get(key); exists {
return value
}
@ -268,7 +269,7 @@ func (c *Context) Raw(code int, contentType string, data []byte) {
}
// String 向响应写入格式化的字符串
func (c *Context) String(code int, format string, values ...any) {
func (c *Context) String(code int, format string, values ...interface{}) {
c.Writer.WriteHeader(code)
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
@ -282,7 +283,7 @@ func (c *Context) Text(code int, text string) {
// JSON 向响应写入 JSON 数据
// 设置 Content-Type 为 application/json
func (c *Context) JSON(code int, obj any) {
func (c *Context) JSON(code int, obj interface{}) {
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.Writer.WriteHeader(code)
if err := json.MarshalWrite(c.Writer, obj); err != nil {
@ -294,7 +295,7 @@ func (c *Context) JSON(code int, obj any) {
// GOB 向响应写入GOB数据
// 设置 Content-Type 为 application/octet-stream
func (c *Context) GOB(code int, obj any) {
func (c *Context) GOB(code int, obj interface{}) {
c.Writer.Header().Set("Content-Type", "application/octet-stream") // 设置合适的 Content-Type
c.Writer.WriteHeader(code)
// GOB 编码
@ -306,25 +307,11 @@ func (c *Context) GOB(code int, obj any) {
}
}
// WANF向响应写入WANF数据
// 设置 application/vnd.wjqserver.wanf; charset=utf-8
func (c *Context) WANF(code int, obj any) {
c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8")
c.Writer.WriteHeader(code)
// WANF 编码
encoder := wanf.NewStreamEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
c.AddError(fmt.Errorf("failed to encode WANF: %w", err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err))
return
}
}
// HTML 渲染 HTML 模板
// 如果 Engine 配置了 HTMLRender则使用它进行渲染
// 否则,会进行简单的字符串输出
// 预留接口,可以扩展为支持多种模板引擎
func (c *Context) HTML(code int, name string, obj any) {
func (c *Context) HTML(code int, name string, obj interface{}) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(code)
@ -355,7 +342,7 @@ func (c *Context) Redirect(code int, location string) {
}
// ShouldBindJSON 尝试将请求体绑定到 JSON 对象
func (c *Context) ShouldBindJSON(obj any) error {
func (c *Context) ShouldBindJSON(obj interface{}) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
@ -366,28 +353,10 @@ func (c *Context) ShouldBindJSON(obj any) error {
return nil
}
// ShouldBindWANF
func (c *Context) ShouldBindWANF(obj any) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
decoder, err := wanf.NewStreamDecoder(c.Request.Body)
if err != nil {
return fmt.Errorf("failed to create WANF decoder: %w", err)
}
if err := decoder.Decode(obj); err != nil {
return fmt.Errorf("WANF binding error: %w", err)
}
return nil
}
// Deprecated: This function is a reserved placeholder for future API extensions
// and is not yet implemented. It will either be properly defined or removed in v2.0.0. Do not use.
// ShouldBind 尝试将请求体绑定到各种类型JSON, Form, XML 等)
// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
// 预留接口,可根据项目需求进行扩展
func (c *Context) ShouldBind(obj any) error {
func (c *Context) ShouldBind(obj interface{}) error {
// TODO: 完整的通用绑定逻辑
// 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等
// 例如:
@ -440,7 +409,7 @@ func (c *Context) Err() error {
// Value returns the value associated with this context for key, or nil if no
// value is associated with key.
// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys
func (c *Context) Value(key any) any {
func (c *Context) Value(key interface{}) interface{} {
if keyAsString, ok := key.(string); ok {
if val, exists := c.Get(keyAsString); exists {
return val

View file

@ -6,6 +6,7 @@ package touka
import (
"errors"
"fmt"
"net/http"
"path"
"strings"
@ -18,19 +19,13 @@ var allowedFileServerMethods = map[string]struct{}{
http.MethodHead: {},
}
var (
ErrInputFSisNil = errors.New("input FS is nil")
ErrMethodNotAllowed = errors.New("method not allowed")
)
// FileServer方式, 返回一个HandleFunc, 统一化处理
func FileServer(fs http.FileSystem) HandlerFunc {
if fs == nil {
return func(c *Context) {
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
c.ErrorUseHandle(500, errors.New("Input FileSystem is nil"))
}
}
fileServerInstance := http.FileServer(fs)
return func(c *Context) {
FileServerHandleServe(c, fileServerInstance)
@ -42,6 +37,7 @@ func FileServer(fs http.FileSystem) HandlerFunc {
func FileServerHandleServe(c *Context, fsHandle http.Handler) {
if fsHandle == nil {
ErrInputFSisNil := errors.New("Input FileSystem Handle is nil")
c.AddError(ErrInputFSisNil)
// 500
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
@ -63,7 +59,7 @@ func FileServerHandleServe(c *Context, fsHandle http.Handler) {
return
} else {
// 否则,返回 405 Method Not Allowed
c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, ErrMethodNotAllowed)
c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, fmt.Errorf("Method %s is Not Allowed on FileServer", c.Request.Method))
}
} else {
c.Next()

7
go.mod
View file

@ -1,16 +1,15 @@
module github.com/infinite-iroha/touka
go 1.25.1
go 1.24.5
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.0-20250810023226-e51d9d0737ee
github.com/fenthope/reco v0.0.4
github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2
)
require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/net v0.42.0 // indirect
)

10
go.sum
View file

@ -2,13 +2,11 @@ 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.0-20250810023226-e51d9d0737ee h1:tJ31DNBn6UhWkk8fiikAQWqULODM+yBcGAEar1tzdZc=
github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee/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-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU=
github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=

184
sse.go
View file

@ -1,184 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2025 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"bytes"
"io"
"net/http"
"strings"
)
// Event 代表一个服务器发送事件(SSE).
type Event struct {
// Event 是事件的名称.
Event string
// Data 是事件的内容, 可以是多行文本.
Data string
// Id 是事件的唯一标识符.
Id string
// Retry 是指定客户端在连接丢失后应等待多少毫秒后尝试重新连接.
Retry string
}
// Render 将事件格式化并写入给定的 writer.
// 通过逐行处理数据, 此方法可防止因数据中包含换行符而导致的CRLF注入问题.
// 为了性能, 它使用 bytes.Buffer 并通过 WriteTo 直接写入, 以避免不必要的内存分配.
func (e *Event) Render(w io.Writer) error {
var buf bytes.Buffer
if len(e.Id) > 0 {
buf.WriteString("id: ")
buf.WriteString(e.Id)
buf.WriteString("\n")
}
if len(e.Event) > 0 {
buf.WriteString("event: ")
buf.WriteString(e.Event)
buf.WriteString("\n")
}
if len(e.Data) > 0 {
lines := strings.Split(e.Data, "\n")
for _, line := range lines {
buf.WriteString("data: ")
buf.WriteString(line)
buf.WriteString("\n")
}
}
if len(e.Retry) > 0 {
buf.WriteString("retry: ")
buf.WriteString(e.Retry)
buf.WriteString("\n")
}
// 每个事件都以一个额外的换行符结尾.
buf.WriteString("\n")
// 直接将 buffer 的内容写入 writer, 避免生成中间字符串.
_, err := buf.WriteTo(w)
return err
}
// EventStream 启动一个 SSE 事件流.
// 这是推荐的、更简单安全的方式, 采用阻塞和回调的设计, 框架负责管理连接生命周期.
//
// 详细用法:
//
// r.GET("/sse/callback", func(c *touka.Context) {
// // streamer 回调函数会在一个循环中被调用.
// c.EventStream(func(w io.Writer) bool {
// event := touka.Event{
// Event: "time-tick",
// Data: time.Now().Format(time.RFC1123),
// }
//
// if err := event.Render(w); err != nil {
// // 发生写入错误, 停止发送.
// return false // 返回 false 结束事件流.
// }
//
// time.Sleep(2 * time.Second)
// return true // 返回 true 继续事件流.
// })
// // 当事件流结束后(例如客户端关闭页面), 这行代码会被执行.
// fmt.Println("Client disconnected from /sse/callback")
// })
func (c *Context) EventStream(streamer func(w io.Writer) bool) {
// 为现代网络协议优化头部.
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush() // 直接调用, ResponseWriter 接口保证了 Flush 方法的存在.
for {
select {
case <-c.Request.Context().Done():
return
default:
if !streamer(c.Writer) {
return
}
c.Writer.Flush()
}
}
}
// EventStreamChan 返回用于 SSE 事件流的 channel.
// 这是为高级并发场景设计的、更灵活的API.
//
// 重要:
// - 调用者必须 close(eventChan) 来结束事件流.
// - 调用者必须在独立的 goroutine 中消费 errChan 来处理错误和连接断开.
// - 为防止 goroutine 泄漏, 建议发送方在 select 中同时监听 c.Request.Context().Done().
//
// 详细用法:
//
// r.GET("/sse/channel", func(c *touka.Context) {
// eventChan, errChan := c.EventStreamChan()
//
// // 必须在独立的goroutine中处理错误和连接断开.
// go func() {
// if err := <-errChan; err != nil {
// c.Errorf("SSE channel error: %v", err)
// }
// }()
//
// // 在另一个goroutine中异步发送事件.
// go func() {
// // 重要: 必须在逻辑结束时关闭channel, 以通知框架.
// defer close(eventChan)
//
// for i := 1; i <= 5; i++ {
// select {
// case <-c.Request.Context().Done():
// return // 客户端已断开, 退出 goroutine.
// default:
// eventChan <- touka.Event{
// Id: fmt.Sprintf("%d", i),
// Data: "hello from channel",
// }
// time.Sleep(2 * time.Second)
// }
// }
// }()
// })
func (c *Context) EventStreamChan() (chan<- Event, <-chan error) {
eventChan := make(chan Event)
errChan := make(chan error, 1)
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
go func() {
defer close(errChan)
for {
select {
case event, ok := <-eventChan:
if !ok {
return
}
if err := event.Render(c.Writer); err != nil {
errChan <- err
return
}
c.Writer.Flush()
case <-c.Request.Context().Done():
errChan <- c.Request.Context().Err()
return
}
}
}()
return eventChan, errChan
}