Compare commits

...

19 Commits

Author SHA1 Message Date
maguodong 342d39a1a0 修改日志可以按照日期进行输出,并自动清理超过30天的旧日志文件 2026-03-28 17:52:52 +08:00
black1552 8add85cea7 refactor(utils): 替换日志库实现
- 导入自定义日志包 git.magicany.cc/black1552/gf-common/log
- 将 g.Log().Infof 替换为 log.Info 方法调用
- 将 g.Log().Errorf 替换为 log.Error 方法调用
- 统一所有 HTTP 请求方法的日志记录方式
- 移除上下文参数 ctx 在日志记录中的传递
2026-03-10 10:04:26 +08:00
black1552 95623f3802 refactor(gateway): 重构反向代理实现以支持 WebSocket 和 HTTP 请求
- 添加 gclient 和 gstr 依赖包用于 HTTP 客户端操作
- 实现新的 BuildRequest 函数,使用 gclient 处理普通 HTTP 请求
- 分离 WebSocket 请求处理到独立的 proxyWebSocket 函数
- 移除旧的 hasProtocol 函数和相关逻辑
- 添加完整的请求头和响应头复制机制
- 实现响应状态码和响应体的正确传递
- 简化 WebSocket 代理逻辑,使用标准反向代理处理
2026-03-06 17:00:37 +08:00
black1552 45262d8f88 fix(gateway): 修复代理响应头传递和JSON写入问题
- 添加 ModifyResponse 函数以正确传递原始响应头
- 实现响应头从代理服务器到客户端的完整传递
- 修复 WebSocket 升级处理中的响应写入逻辑
- 确保 JSON 数据通过 RawWriter 正确写入响应
2026-03-06 13:54:20 +08:00
black1552 f958579981 fix(gateway): 解决 WebSocket 升级和 HTTP 请求处理问题
- 使用 RawWriter() 获取原始的 http.ResponseWriter,避免 gf 框架的封装影响
- 确保代理能够正确处理 WebSocket 连接升级
- 修复了因框架封装导致的响应写入问题
2026-03-06 11:35:25 +08:00
black1552 737fd041a0 feat(gateway): 添加协议自动检测和补全功能
- 实现 hasProtocol 函数检查字符串是否包含协议前缀
- 支持 http、https、ws、wss 协议检测
- 自动为没有协议的主机地址添加相应协议前缀
- 根据请求类型判断协议:WebSocket 使用 ws,HTTP 使用 http
- 优化反向代理请求构建逻辑
2026-03-06 11:14:10 +08:00
black1552 969322a912 refactor(server): 重构网关请求代理实现
- 使用标准库 httputil.NewSingleHostReverseProxy 替代自定义 HTTP 客户端
- 移除 gclient、gcache 和相关依赖包导入
- 添加自定义 Director 函数处理目标 URL 设置
- 实现错误处理机制和 Bad Gateway 响应
- 简化 WebSocket 连接支持逻辑
- 优化请求头和响应头处理流程
2026-03-06 10:52:26 +08:00
black1552 f9fae10c76 feat(server): 添加网关代理功能
- 实现了 HTTP 请求转发功能,支持请求头和响应头的完整复制
- 添加了目标 URL 构建逻辑,处理路径和查询参数的正确拼接
- 集成了响应体读取和状态码设置功能
- 实现了 WebSocket 连接的反向代理处理
- 添加了缓存机制用于临时存储主机信息
- 集成了日志记录功能用于错误跟踪和调试
2026-03-06 10:31:06 +08:00
black1552 f130e18b8f chore(deps): 更新项目依赖包到最新版本
- 升级 github.com/gogf/gf 相关包从 v2.9.3 到 v2.10.0
- 升级 github.com/emirpasic/gods 从 v1.18.1 到 v2.0.0-alpha
- 升级 github.com/olekukonko/tablewriter 从 v1.0.9 到 v1.1.0
- 升级 go.opentelemetry.io/otel 相关包从 v1.37.0 到 v1.38.0
- 更新 github.com/stretchr/testify 从 v1.10.0 到 v1.11.1
- 调整 github.com/rogpeppe/go-internal 版本从 v1.14.1 到 v1.13.1
2026-03-06 10:24:02 +08:00
black1552 5a37aea56f feat(log): 实现日志系统彩色输出和文件分离功能
- 添加 ANSI 颜色常量定义支持彩色输出
- 实现 stripAnsiColors 函数去除 ANSI 颜色码
- 创建 logWriter 结构体实现控制台带颜色文件无颜色的输出
- 修改初始化逻辑使用自定义 writer 替代 MultiWriter
- 更新 Info、Error、Warn、Debug 函数添加时间戳和颜色前缀
- 将日志文件大小限制从 1MB 调整为 2MB
- 移除默认日志标志位,使用自定义格式化输出
2026-03-06 10:20:55 +08:00
black1552 6bfe26bbf7 feat(tcp): 实现消息帧格式处理,添加缓冲区存储未处理数据
添加消息帧格式处理逻辑,使用4字节长度前缀标识消息大小
在TcpConnection结构中添加buffer字段用于存储未处理的字节数据
修改sendMessage和receiveMessages方法以支持新的消息帧格式
2026-03-02 09:03:38 +08:00
black1552 c0629b4038 feat(database): 将数据库连接池从 NutsDB 迁移到 SQLite
- 修改 server/ws/example.go 中的数据库连接池引用,将 m.nutsPool 替换为 m.sqlitePool
- 更新 go.mod 文件中的 Go 版本从 1.24.3 升级到 1.25.0
- 移除 nutsdb 依赖项 github.com/nutsdb/nutsdb v1.1.0
- 移除其他间接依赖包括 github.com/antlabs/stl、github.com/antlabs/timer、
  github.com/bwmarrin/snowflake、github.com/cespare/xxhash/v2 等
- 添加 SQLite 相关依赖 github.com/glebarez/go-sqlite 和 github.com/glebarez/sqlite
- 更新 golang.org/x 相关包版本,包括 exp、sync、sys、text 等模块
- 在 go.sum 中同步更新所有依赖项的校验和变化
2026-02-27 16:35:21 +08:00
black1552 bb0f3de20a feat(pool): 添加NutsDB连接池实现
- 实现了基于NutsDB的连接池管理功能
- 定义了ConnectionInfo结构体用于存储连接信息
- 提供了连接的增删改查操作接口
- 实现了内存缓存机制提升并发性能
- 添加了按类型查询和统计连接数量的功能
- 实现了清理不活跃连接的定时任务功能
2026-02-27 10:46:24 +08:00
black1552 c50714e8a0 feat(pool): 移除BadgerDB连接池实现并集成NutsDB
- 删除pool/badger.go文件中的BadgerDB连接池相关代码
- 更新WebSocket示例移除badger目录参数依赖
- 更新TCP示例移除badger目录参数依赖
- 升级github.com/dgraph-io/badger/v4依赖从v4.2.0到v4.9.1
- 新增github.com/nutsdb/nutsdb依赖用于替代BadgerDB功能
- 添加WebSocket和TCP连接测试示例代码
- 更新多个间接依赖包版本包括ristretto、humanize、compress等
2026-02-27 10:46:04 +08:00
black1552 942eff81fb feat(pool): 添加获取所有在线连接ID的功能
- 实现了 GetAllConnIDs 方法来获取在线连接的ID列表
- 添加了内存缓存检查以提高性能
- 实现了从 BadgerDB 迭代获取所有连接记录的逻辑
- 添加了连接活跃状态过滤功能
- 实现了 JSON 反序列化连接信息的处理
- 添加了错误处理和日志记录机制
2026-02-27 10:15:02 +08:00
black1552 845afe6dac 去除未使用的导入 2026-02-27 10:08:59 +08:00
black1552 4c300374cb feat(pool): 修改NewBadgerPool函数接受数据库路径参数并移除GetAllConnIDs方法
- 修改NewBadgerPool函数签名以接受badgerDir字符串参数
- 将默认数据库路径配置改为通过参数传入
- 移除GetAllConnIDs方法及其相关实现代码
- 更新TCPServer和WebSocket Manager构造函数以传递数据库路径
- 修复TCP服务器和WebSocket示例中的初始化调用
2026-02-27 10:06:26 +08:00
black1552 5e22f2c2c0 feat(pool): 添加基于BadgerDB的连接池实现
- 新增BadgerPool结构体,支持WebSocket和TCP连接类型
- 实现连接的增删改查功能,包括内存缓存机制提升性能
- 添加按类型查询连接、统计连接数量等辅助方法
- 实现清理非活跃连接的功能,支持定期维护
- 更新示例代码以处理初始化错误并改进错误处理
- 添加BadgerDB依赖及其相关间接依赖包
2026-02-27 09:47:23 +08:00
black1552 22a6f33af2 feat(log): 添加日志系统功能
- 集成 lumberjack.v2 库实现日志轮转功能
- 创建 log 包并实现基础日志记录器
- 实现 Info、Error、Warn、Debug 四种日志级别方法
- 配置日志文件大小限制为 1MB 和保留策略
- 实现日志同时输出到文件和终端的功能
- 添加错误日志的堆栈跟踪信息记录
2026-02-25 14:22:56 +08:00
14 changed files with 1060 additions and 99 deletions

60
db/database.go Normal file
View File

@ -0,0 +1,60 @@
package db
import (
"database/sql"
"fmt"
"git.magicany.cc/black1552/gf-common/log"
"github.com/glebarez/sqlite"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
var (
Type gorm.Dialector
Db *gorm.DB
err error
sqlDb *sql.DB
dns = gfile.Join(gfile.Pwd(), "db", "database.db")
)
func init() {
if g.IsEmpty(dns) {
log.Error("gormDns未配置", "请检查配置文件")
return
}
sqliteInit()
Db, err = gorm.Open(Type, &gorm.Config{
SkipDefaultTransaction: true,
// 命名策略:保持与模型一致,避免字段/表名转换问题
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 表名禁用复数形式(例如 User 对应 user 表,而非 users
},
})
if err != nil {
log.Error("数据库连接失败: ", err)
return
}
sqlDb, err = Db.DB()
if err != nil {
log.Error("获取sqlDb失败", err)
return
}
if err = sqlDb.Ping(); err != nil {
log.Error("数据库未正常连接", err)
return
}
}
func sqliteInit() {
if !gfile.Exists(dns) {
_, err = gfile.Create(dns)
if err != nil {
log.Error("创建数据库文件失败: ", err)
return
}
}
Type = sqlite.Open(fmt.Sprintf("%s?cache=shared&mode=rwc&_busy_timeout=10000&_fk=1&_journal=WAL&_sync=FULL", dns))
}

1
db/index.go Normal file
View File

@ -0,0 +1 @@
package db

45
go.mod
View File

@ -1,18 +1,20 @@
module git.magicany.cc/black1552/gf-common
go 1.24.3
go 1.25.0
require (
github.com/duke-git/lancet/v2 v2.3.7
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3
github.com/gogf/gf/v2 v2.9.3
github.com/glebarez/sqlite v1.11.0
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
github.com/gogf/gf/contrib/registry/etcd/v2 v2.10.0
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.10.0
github.com/gogf/gf/v2 v2.10.0
github.com/gorilla/websocket v1.5.3
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/xuri/excelize/v2 v2.9.1
google.golang.org/grpc v1.76.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
)
@ -23,13 +25,15 @@ require (
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3 // indirect
github.com/gogf/gf/contrib/registry/file/v2 v2.10.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
@ -40,9 +44,12 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@ -53,21 +60,25 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect
go.etcd.io/etcd/client/v3 v3.5.17 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.68.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)

127
go.sum
View File

@ -15,14 +15,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HHHNs=
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -31,28 +37,32 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3 h1:P4jrnp+Vmh3kDeaH/kyHPI6rfoMmQD+sPJa716aMbS0=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3/go.mod h1:yEhfx78wgpxUJhH9C9bWJ7I3JLcVCzUg11A4ORYTKeg=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3 h1:4ztKAHfwtddPRwxlVRRfLdJMzp42Z+9K5tFHrPPZl1Q=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3/go.mod h1:ey99pcs/hSwShOLWjd4mUUmq4S9fLy7elf7SslUAs20=
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3 h1:7f+55KmwJzW0Kmam+N3VrLMlMkCTc5LTPDaZeElCLd0=
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3/go.mod h1:kOYsqBlbf6pElgfLyIEFN6FpXnSSdgkV4w+7+w/78do=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3 h1:HoUu3a7t0iLI0/GbVP46rPHbrfBlKa6rfHsnCvry4+4=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3/go.mod h1:F5v+4GumRPIFZsAiz6H5QNoOiPM7cluMCvGF0zbzsGE=
github.com/gogf/gf/v2 v2.9.3 h1:qjN4s55FfUzxZ1AE8vUHNDX3V0eIOUGXhF2DjRTVZQ4=
github.com/gogf/gf/v2 v2.9.3/go.mod h1:w6rcfD13SmO7FKI80k9LSLiSMGqpMYp50Nfkrrc2sEE=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0 h1:UvqxwinkelKxwdwnKUfdy51/ls4RL7MCeJqAZOVAy0I=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0/go.mod h1:6v7oGBF9wv59WERJIOJxXmLhkUcxwON3tPYW3AZ7wbY=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.10.0 h1:yOKc0YroBwCwyBHezJ1M6ISFfrLl7ZnRKsCS1dhF1DA=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.10.0/go.mod h1:ezkHf1r7YxkFYis7Y1807D1tr+3nXolfL8IsY816Cuw=
github.com/gogf/gf/contrib/registry/file/v2 v2.10.0 h1:yy93PGhz2CE3PpF2JSGa2MXJWd4JNHYC5l5NEFCFRV4=
github.com/gogf/gf/contrib/registry/file/v2 v2.10.0/go.mod h1:ZzGWiTbQ9nAmDynrmzaRJlqYHD/F3MeMwCvHrkv/LP4=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.10.0 h1:MWHgX/h/62+Oo1Jbeq8DrXbEWSOZxskpBBm3cFAV4/Q=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.10.0/go.mod h1:zGZdjS08IqiSGhKfy69/qgcYKUYUL/VsOx5fuT++TVE=
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -74,18 +84,23 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
@ -94,13 +109,13 @@ github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
@ -120,16 +135,16 @@ go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY
go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
@ -144,14 +159,16 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a h1:4iLhBPcpqFmylhnkbY3W0ONLUYYkDAW9xMFLfxgsvCw=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -164,8 +181,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -173,19 +190,21 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -198,12 +217,14 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -215,3 +236,31 @@ gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

200
log/log.go Normal file
View File

@ -0,0 +1,200 @@
package log
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
"time"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
logPath string
sysLog *log.Logger
filePath string
currentDate string // 当前日志文件对应的日期
fileLogger *lumberjack.Logger
)
const (
Reset = "\033[0m"
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Purple = "\033[35m"
Cyan = "\033[36m"
)
// 正则表达式匹配 ANSI 颜色码
var ansiColorRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// stripAnsiColors 去除字符串中的 ANSI 颜色码
func stripAnsiColors(s string) string {
return ansiColorRegex.ReplaceAllString(s, "")
}
// logWriter 自定义 writer用于分别处理控制台和文件输出
type logWriter struct {
console io.Writer
file io.Writer
}
func (w *logWriter) Write(p []byte) (n int, err error) {
// 控制台输出保留颜色
_, err = w.console.Write(p)
if err != nil {
return 0, err
}
// 文件输出去除颜色码
colorless := stripAnsiColors(string(p))
_, err = w.file.Write([]byte(colorless))
if err != nil {
return 0, err
}
return len(p), nil
}
// cleanOldLogs 删除指定天数之前的日志文件(包括主文件和备份文件)
func cleanOldLogs(days int) {
if !gfile.Exists(logPath) {
return
}
// 获取所有日志文件
files, err := gfile.DirNames(logPath)
if err != nil {
return
}
now := time.Now()
for _, file := range files {
path := filepath.Join(logPath, file)
if gfile.IsDir(path) {
continue
}
var dateStr string
var matched bool
// 匹配主日志文件格式log-YYYY-MM-DD.log
if strings.HasPrefix(file, "log-") && strings.HasSuffix(file, ".log") {
// 检查是否是主文件(没有备份时间戳)
// 主文件格式log-2026-04-25.log
// 备份文件格式log-2026-04-25-2026-04-25T10-30-45.123.log
parts := strings.Split(strings.TrimSuffix(file, ".log"), "-")
if len(parts) == 4 {
// 主文件log-YYYY-MM-DD
dateStr = parts[1] + "-" + parts[2] + "-" + parts[3]
matched = true
} else if len(parts) > 4 {
// 备份文件log-YYYY-MM-DD-YYYY-MM-DDTHH-MM-SS.mmm
// 提取主日期部分(第一个日期)
dateStr = parts[1] + "-" + parts[2] + "-" + parts[3]
matched = true
}
}
if !matched {
continue
}
// 解析日期
fileTime, err := time.Parse("2006-01-02", dateStr)
if err != nil {
continue // 日期格式不正确,跳过
}
// 计算文件年龄
tage := now.Sub(fileTime)
if tage.Hours() > float64(days*24) {
// 超过指定天数,删除文件
err = os.Remove(path)
if err == nil {
Info(fmt.Sprintf("已删除过期日志文件:%s", file))
}
}
}
}
// checkAndRotateLogFile 检查是否需要切换日志文件(跨天时)
func checkAndRotateLogFile() {
date := gtime.Date()
if currentDate != date {
// 日期变化,需要重新初始化
currentDate = date
filePath = gfile.Join(logPath, fmt.Sprintf("log-%s.log", currentDate))
fileLogger = &lumberjack.Logger{
Filename: filePath,
MaxSize: 2, // 单个文件最大 10MB
MaxBackups: 5, // 最多保留 5 个备份
MaxAge: 30, // 保留 30 天
Compress: false, // 启用压缩
}
// 创建新的 writer
multiWriter := &logWriter{
console: os.Stdout,
file: fileLogger,
}
sysLog = log.New(multiWriter, "", 0)
// 清理 30 天前的旧日志
cleanOldLogs(30)
}
}
func Init() {
if sysLog != nil {
checkAndRotateLogFile() // 检查是否需要切换文件
return
}
logPath = gfile.Join(gfile.Pwd(), "logs")
currentDate = gtime.Date()
filePath = gfile.Join(logPath, fmt.Sprintf("log-%s.log", currentDate))
fileLogger = &lumberjack.Logger{
Filename: filePath,
MaxSize: 2, // 单个文件最大 10MB
MaxBackups: 5, // 最多保留 5 个备份
MaxAge: 30, // 保留 30 天
Compress: false, // 启用压缩
}
// 使用自定义 writer 实现控制台带颜色、文件无颜色的输出
multiWriter := &logWriter{
console: os.Stdout,
file: fileLogger,
}
sysLog = log.New(multiWriter, "", 0)
// 启动时清理 30 天前的旧日志
cleanOldLogs(30)
}
func Info(v ...any) {
Init()
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[INFO]%s ", time.Now().Format("2006-01-02 15:04:05"), Green, Reset))
sysLog.Println(fmt.Sprint(v...))
}
func Error(v ...any) {
Init()
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[ERROR]%s ", time.Now().Format("2006-01-02 15:04:05"), Red, Reset))
msg := fmt.Sprint(v...)
sysLog.Println(msg, strings.TrimSpace(string(debug.Stack())))
}
func Warn(v ...any) {
Init()
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[WARN]%s ", time.Now().Format("2006-01-02 15:04:05"), Yellow, Reset))
sysLog.Println(fmt.Sprint(v...))
}
func Debug(v ...any) {
Init()
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[DEBUG]%s ", time.Now().Format("2006-01-02 15:04:05"), Blue, Reset))
sysLog.Println(fmt.Sprint(v...))
}

25
pool/common.go Normal file
View File

@ -0,0 +1,25 @@
package pool
import (
"time"
)
// ConnType 连接类型
type ConnType string
const (
ConnTypeWebSocket ConnType = "websocket"
ConnTypeTCP ConnType = "tcp"
)
// ConnectionInfo 连接信息
type ConnectionInfo struct {
ID string `json:"id" gorm:"primaryKey"`
Type ConnType `json:"type" gorm:"index"`
Address string `json:"address"`
IsActive bool `json:"isActive" gorm:"index"`
LastUsed time.Time `json:"lastUsed"`
CreatedAt time.Time `json:"createdAt"`
// 额外的连接数据,根据不同类型存储不同的信息
Data map[string]interface{} `json:"data" gorm:"-"`
}

247
pool/sqlite.go Normal file
View File

@ -0,0 +1,247 @@
package pool
import (
"context"
"fmt"
"sync"
"time"
"git.magicany.cc/black1552/gf-common/db"
"gorm.io/gorm"
)
// SQLitePool SQLite连接池
type SQLitePool struct {
db *gorm.DB
mutex sync.RWMutex
ctx context.Context
cancel context.CancelFunc
// 内存缓存,提高并发性能
cache map[string]*ConnectionInfo
}
// NewSQLitePool 创建SQLite连接池
func NewSQLitePool() (*SQLitePool, error) {
ctx, cancel := context.WithCancel(context.Background())
// 检查数据库连接是否正常
if db.Db == nil {
return nil, fmt.Errorf("database connection is not initialized")
}
// 自动迁移ConnectionInfo模型
err := db.Db.AutoMigrate(&ConnectionInfo{})
if err != nil {
cancel()
return nil, fmt.Errorf("failed to migrate connection info model: %w", err)
}
return &SQLitePool{
db: db.Db,
ctx: ctx,
cancel: cancel,
cache: make(map[string]*ConnectionInfo),
}, nil
}
// Close 关闭连接池
func (p *SQLitePool) Close() error {
p.cancel()
// SQLite连接由db包管理不需要在这里关闭
return nil
}
// Add 添加连接
func (p *SQLitePool) Add(conn *ConnectionInfo) error {
p.mutex.Lock()
defer p.mutex.Unlock()
// 存储到SQLite
result := p.db.Create(conn)
if result.Error != nil {
return fmt.Errorf("failed to store connection: %w", result.Error)
}
// 更新内存缓存
p.cache[conn.ID] = conn
return nil
}
// Get 获取连接
func (p *SQLitePool) Get(connID string) (*ConnectionInfo, error) {
p.mutex.RLock()
// 先从内存缓存获取
if conn, ok := p.cache[connID]; ok {
p.mutex.RUnlock()
return conn, nil
}
p.mutex.RUnlock()
// 从SQLite获取
var connInfo ConnectionInfo
result := p.db.First(&connInfo, "id = ?", connID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, fmt.Errorf("failed to get connection: %w", result.Error)
}
// 更新内存缓存
p.mutex.Lock()
p.cache[connID] = &connInfo
p.mutex.Unlock()
return &connInfo, nil
}
// Remove 移除连接
func (p *SQLitePool) Remove(connID string) error {
p.mutex.Lock()
defer p.mutex.Unlock()
// 从SQLite删除
result := p.db.Delete(&ConnectionInfo{}, "id = ?", connID)
if result.Error != nil {
return fmt.Errorf("failed to remove connection: %w", result.Error)
}
// 从内存缓存删除
delete(p.cache, connID)
return nil
}
// Update 更新连接信息
func (p *SQLitePool) Update(conn *ConnectionInfo) error {
p.mutex.Lock()
defer p.mutex.Unlock()
// 存储到SQLite
result := p.db.Save(conn)
if result.Error != nil {
return fmt.Errorf("failed to update connection: %w", result.Error)
}
// 更新内存缓存
p.cache[conn.ID] = conn
return nil
}
// GetAll 获取所有连接
func (p *SQLitePool) GetAll() ([]*ConnectionInfo, error) {
p.mutex.RLock()
// 如果内存缓存不为空,直接返回缓存
if len(p.cache) > 0 {
conns := make([]*ConnectionInfo, 0, len(p.cache))
for _, conn := range p.cache {
conns = append(conns, conn)
}
p.mutex.RUnlock()
return conns, nil
}
p.mutex.RUnlock()
// 从SQLite获取所有连接
var conns []*ConnectionInfo
result := p.db.Find(&conns)
if result.Error != nil {
return nil, fmt.Errorf("failed to get all connections: %w", result.Error)
}
// 更新内存缓存
p.mutex.Lock()
for _, conn := range conns {
p.cache[conn.ID] = conn
}
p.mutex.Unlock()
return conns, nil
}
// GetByType 根据类型获取连接
func (p *SQLitePool) GetByType(connType ConnType) ([]*ConnectionInfo, error) {
allConns, err := p.GetAll()
if err != nil {
return nil, err
}
var filtered []*ConnectionInfo
for _, conn := range allConns {
if conn.Type == connType {
filtered = append(filtered, conn)
}
}
return filtered, nil
}
// Count 获取连接数量
func (p *SQLitePool) Count() (int, error) {
p.mutex.RLock()
// 如果内存缓存不为空,直接返回缓存大小
if len(p.cache) > 0 {
count := len(p.cache)
p.mutex.RUnlock()
return count, nil
}
p.mutex.RUnlock()
// 从SQLite统计数量
var count int64
result := p.db.Model(&ConnectionInfo{}).Count(&count)
if result.Error != nil {
return 0, fmt.Errorf("failed to count connections: %w", result.Error)
}
return int(count), nil
}
// GetAllConnIDs 获取所有在线连接的ID列表
func (p *SQLitePool) GetAllConnIDs() ([]string, error) {
p.mutex.RLock()
// 如果内存缓存不为空从缓存中提取在线连接的ID
if len(p.cache) > 0 {
ids := make([]string, 0, len(p.cache))
for id, conn := range p.cache {
if conn.IsActive {
ids = append(ids, id)
}
}
p.mutex.RUnlock()
return ids, nil
}
p.mutex.RUnlock()
// 从SQLite获取所有在线连接的ID
var conns []*ConnectionInfo
result := p.db.Where("is_active = ?", true).Find(&conns)
if result.Error != nil {
return nil, fmt.Errorf("failed to get all connection IDs: %w", result.Error)
}
ids := make([]string, 0, len(conns))
for _, conn := range conns {
ids = append(ids, conn.ID)
}
return ids, nil
}
// CleanupInactive 清理不活跃的连接
func (p *SQLitePool) CleanupInactive(duration time.Duration) error {
allConns, err := p.GetAll()
if err != nil {
return err
}
now := time.Now()
for _, conn := range allConns {
if !conn.IsActive || now.Sub(conn.LastUsed) > duration {
if err := p.Remove(conn.ID); err != nil {
return err
}
}
}
return nil
}

89
server/gateway.go Normal file
View File

@ -0,0 +1,89 @@
package server
import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"git.magicany.cc/black1552/gf-common/log"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/text/gstr"
)
// hasProtocol 检查字符串是否包含协议前缀
func hasProtocol(s string) bool {
return strings.HasPrefix(s, "http://") ||
strings.HasPrefix(s, "https://") ||
strings.HasPrefix(s, "ws://") ||
strings.HasPrefix(s, "wss://")
}
// BuildRequest 反向代理请求到指定主机
// 自动支持所有 HTTP 方法及 WebSocket 连接
func BuildRequest(r *ghttp.Request, host string) {
if gstr.Contains(r.RequestURI, "/ws") {
proxyWebSocket(r, host)
return
}
client := gclient.New()
// 构建目标URL而不是直接复制RequestURI
targetURL := host + r.URL.Path
if r.URL.RawQuery != "" {
targetURL += "?" + r.URL.RawQuery
}
// 复制请求头
for key, values := range r.Header {
for _, value := range values {
client.SetHeader(key, value)
}
}
response, err := client.DoRequest(gctx.New(), r.Method, targetURL, r.GetBody())
if err != nil {
log.Error(gctx.New(), "request error:", err)
panic(fmt.Sprintf("request error: %v", err))
}
defer response.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(response.Body)
if err != nil {
log.Error(gctx.New(), "read response body error:", err)
panic(fmt.Sprintf("read response body error: %v", err))
}
// 复制响应头
for key, values := range response.Header {
for _, value := range values {
r.Response.Header().Add(key, value)
}
}
// 设置响应状态码并写入响应体
r.Response.Status = response.StatusCode
r.Response.Write(respBody)
}
// proxyWebSocket 处理 WebSocket 连接的代理
func proxyWebSocket(r *ghttp.Request, targetHost string) {
// 解析目标主机 URL
targetURL, err := url.Parse(targetHost)
if err != nil {
log.Error(gctx.New(), "parse target host error:", err)
r.Response.WriteStatus(http.StatusInternalServerError)
r.Response.Write([]byte("Invalid target host"))
return
}
// 创建反向代理
proxy := httputil.NewSingleHostReverseProxy(targetURL)
// 修改请求 URL保留原始路径和查询参数
r.URL.Scheme = targetURL.Scheme
r.URL.Host = targetURL.Host
log.Info(gctx.New(), r.GetBodyString())
// 处理 WebSocket 连接
proxy.ServeHTTP(r.Response.Writer, r.Request)
}

View File

@ -19,7 +19,10 @@ func NewWs() *Manager {
}
// 2. 创建管理器
m := NewManager(customConfig)
m, err := NewManager(customConfig)
if err != nil {
log.Fatalf("Failed to create manager: %v", err)
}
// 3. 覆盖业务回调(核心:自定义消息处理逻辑)
// 连接建立回调
@ -71,3 +74,35 @@ func main() {
log.Println("WebSocket服务启动http://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// TestWebSocket 测试WebSocket连接
func TestWebSocket() {
log.Println("=== 测试WebSocket连接 ===")
log.Println("1. 创建WebSocket管理器")
m, err := NewManager(DefaultConfig())
if err != nil {
log.Fatalf("创建管理器失败:%v", err)
}
log.Println("2. 管理器创建成功")
log.Println("3. 获取在线连接数")
count, err := m.sqlitePool.Count()
if err != nil {
log.Printf("获取在线连接数失败:%v", err)
} else {
log.Printf("当前在线连接数:%d", count)
}
log.Println("4. 获取所有在线连接ID")
connIDs, err := m.GetAllConnIDs()
if err != nil {
log.Printf("获取在线连接ID失败%v", err)
} else {
log.Printf("在线连接ID%v", connIDs)
}
log.Println("5. 关闭管理器")
if err := m.Close(); err != nil {
log.Printf("关闭管理器失败:%v", err)
} else {
log.Println("管理器关闭成功")
}
log.Println("=== WebSocket测试完成 ===")
}

View File

@ -9,6 +9,7 @@ import (
"sync"
"time"
"git.magicany.cc/black1552/gf-common/pool"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
@ -92,7 +93,8 @@ type Connection struct {
type Manager struct {
config *Config // 配置
upgrader *websocket.Upgrader // HTTP升级器
connections map[string]*Connection // 所有在线连接connID -> Connection
connections map[string]*Connection // 内存中的连接connID -> Connection
sqlitePool *pool.SQLitePool // SQLite连接池
mutex sync.RWMutex // 读写锁保护connections
// 业务回调:收到消息时触发(用户自定义处理逻辑)
OnMessage func(connID string, msgType int, data any)
@ -148,16 +150,16 @@ func (c *Config) Merge(other *Config) *Config {
}
// NewManager 创建连接管理器
func NewManager(config *Config) *Manager {
func NewManager(config *Config) (*Manager, error) {
defaultConfig := DefaultConfig()
finalConfig := defaultConfig.Merge(config)
// 初始化升级器
upgrader := &websocket.Upgrader{
ReadBufferSize: config.ReadBufferSize,
WriteBufferSize: config.WriteBufferSize,
ReadBufferSize: finalConfig.ReadBufferSize,
WriteBufferSize: finalConfig.WriteBufferSize,
CheckOrigin: func(r *http.Request) bool {
// 跨域检查
if config.AllowAllOrigins {
if finalConfig.AllowAllOrigins {
return true
}
origin := r.Header.Get("Origin")
@ -170,10 +172,17 @@ func NewManager(config *Config) *Manager {
},
}
// 初始化SQLite连接池
sqlitePool, err := pool.NewSQLitePool()
if err != nil {
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
}
return &Manager{
config: finalConfig,
upgrader: upgrader,
connections: make(map[string]*Connection),
sqlitePool: sqlitePool,
mutex: sync.RWMutex{},
// 默认回调(用户可覆盖)
OnMessage: func(connID string, msgType int, data any) {
@ -185,7 +194,7 @@ func NewManager(config *Config) *Manager {
OnDisconnect: func(connID string, err error) {
log.Printf("[默认回调] 连接[%s]已关闭:%v", connID, err)
},
}
}, nil
}
// Upgrade HTTP升级为WebSocket连接
@ -237,6 +246,24 @@ func (m *Manager) Upgrade(w http.ResponseWriter, r *http.Request, connID string)
m.connections[connID] = wsConn
m.mutex.Unlock()
// 存储到SQLite
connInfo := &pool.ConnectionInfo{
ID: connID,
Type: pool.ConnTypeWebSocket,
Address: r.RemoteAddr,
IsActive: true,
LastUsed: time.Now(),
CreatedAt: time.Now(),
Data: map[string]interface{}{
"origin": r.Header.Get("Origin"),
"userAgent": r.Header.Get("User-Agent"),
},
}
if err := m.sqlitePool.Add(connInfo); err != nil {
log.Printf("[错误] 存储连接到SQLite失败%v", err)
// 不影响连接建立,仅记录错误
}
// 触发连接建立回调
m.OnConnect(connID)
@ -282,6 +309,18 @@ func (c *Connection) ReadPump() {
return
}
// 更新最后使用时间
now := time.Now()
// 从SQLite获取连接信息并更新
connInfo, err := c.manager.sqlitePool.Get(c.connID)
if err == nil && connInfo != nil {
connInfo.LastUsed = now
if err := c.manager.sqlitePool.Update(connInfo); err != nil {
log.Printf("[错误] 更新SQLite连接信息失败%v", err)
// 不影响消息处理,仅记录错误
}
}
// 尝试解析JSON格式的心跳消息精准判断替代包含判断
isHeartbeat := false
// 先尝试解析为JSON对象
@ -369,6 +408,19 @@ func (c *Connection) Send(data []byte) error {
if err != nil {
return fmt.Errorf("发送消息失败:%w", err)
}
// 更新最后使用时间
now := time.Now()
// 从SQLite获取连接信息并更新
connInfo, err := c.manager.sqlitePool.Get(c.connID)
if err == nil && connInfo != nil {
connInfo.LastUsed = now
if err := c.manager.sqlitePool.Update(connInfo); err != nil {
log.Printf("[错误] 更新SQLite连接信息失败%v", err)
// 不影响消息发送,仅记录错误
}
}
return nil
}
}
@ -394,6 +446,12 @@ func (c *Connection) Close(err error) {
delete(c.manager.connections, c.connID)
c.manager.mutex.Unlock()
// 从SQLite移除
if err := c.manager.sqlitePool.Remove(c.connID); err != nil {
log.Printf("[错误] 从SQLite移除连接失败%v", err)
// 不影响连接关闭,仅记录错误
}
// 触发断开回调
c.manager.OnDisconnect(c.connID, err)
@ -462,12 +520,18 @@ func (m *Manager) GetAllConn() map[string]*Connection {
return connCopy
}
// GetConn 获取指定连接
func (m *Manager) GetConn(connID string) *Connection {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.connections[connID]
}
// GetAllConnIDs 获取所有在线连接的ID列表
func (m *Manager) GetAllConnIDs() ([]string, error) {
return m.sqlitePool.GetAllConnIDs()
}
// CloseAll 关闭所有连接
func (m *Manager) CloseAll() {
m.mutex.RLock()
@ -486,3 +550,14 @@ func (m *Manager) CloseAll() {
}
}
}
// Close 关闭管理器,清理资源
func (m *Manager) Close() error {
// 关闭所有连接
m.CloseAll()
// 关闭SQLite连接池
if m.sqlitePool != nil {
return m.sqlitePool.Close()
}
return nil
}

View File

@ -18,7 +18,11 @@ func Example() {
}
// 创建TCP服务器
server := NewTCPServer("0.0.0.0:8888", config)
server, err := NewTCPServer("0.0.0.0:8888", config)
if err != nil {
fmt.Printf("Failed to create server: %v\n", err)
return
}
// 设置消息处理函数
server.SetMessageHandler(func(conn *TcpConnection, msg *TcpMessage) error {
@ -45,3 +49,48 @@ func Example() {
fmt.Println("TCP server stopped.")
}
// TestTCP 测试TCP连接
func TestTCP() {
fmt.Println("=== 测试TCP连接 ===")
fmt.Println("1. 创建TCP服务器配置")
config := &TcpPoolConfig{
BufferSize: 2048,
MaxConnections: 100000,
ConnectTimeout: time.Second * 5,
ReadTimeout: time.Second * 30,
WriteTimeout: time.Second * 10,
MaxIdleTime: time.Minute * 5,
}
fmt.Println("2. 创建TCP服务器")
server, err := NewTCPServer("0.0.0.0:8888", config)
if err != nil {
fmt.Printf("创建服务器失败:%v\n", err)
return
}
fmt.Println("3. 服务器创建成功")
fmt.Println("4. 获取在线连接数")
count := server.Connection.Count()
fmt.Printf("当前在线连接数:%d\n", count)
fmt.Println("5. 获取所有在线连接ID")
connIDs, err := server.GetAllConnIDs()
if err != nil {
fmt.Printf("获取在线连接ID失败%v\n", err)
} else {
fmt.Printf("在线连接ID%v\n", connIDs)
}
fmt.Println("6. 启动服务器")
if err := server.Start(); err != nil {
fmt.Printf("启动服务器失败:%v\n", err)
return
}
fmt.Println("7. 服务器启动成功运行2秒后停止")
time.Sleep(time.Second * 2)
fmt.Println("8. 停止服务器")
if err := server.Stop(); err != nil {
fmt.Printf("停止服务器失败:%v\n", err)
} else {
fmt.Println("服务器停止成功")
}
fmt.Println("=== TCP测试完成 ===")
}

View File

@ -2,10 +2,12 @@ package tcp
import (
"context"
"encoding/binary"
"fmt"
"sync"
"time"
"git.magicany.cc/black1552/gf-common/pool"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/glog"
@ -32,18 +34,26 @@ type TCPServer struct {
// ConnectionPool 连接池结构
type ConnectionPool struct {
connections map[string]*TcpConnection
sqlitePool *pool.SQLitePool
mutex sync.RWMutex
config *TcpPoolConfig
logger *glog.Logger
}
// NewTCPServer 创建一个新的TCP服务器
func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
func NewTCPServer(address string, config *TcpPoolConfig) (*TCPServer, error) {
logger := g.Log(address)
ctx, cancel := context.WithCancel(context.Background())
// 初始化SQLite连接池
sqlitePool, err := pool.NewSQLitePool()
if err != nil {
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
}
pool := &ConnectionPool{
connections: make(map[string]*TcpConnection),
sqlitePool: sqlitePool,
config: config,
logger: logger,
}
@ -58,7 +68,7 @@ func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
}
server.Listener = gtcp.NewServer(address, server.handleConnection)
return server
return server, nil
}
// SetMessageHandler 设置消息处理函数
@ -86,6 +96,11 @@ func (s *TCPServer) Stop() error {
s.Listener.Close()
s.wg.Wait()
s.Connection.Clear()
// 关闭SQLite连接池
if err := s.Connection.sqlitePool.Close(); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to close SQLite pool: %v", err))
// 不影响服务器停止,仅记录错误
}
s.Logger.Info(s.ctx, "TCP server stopped")
return nil
}
@ -109,6 +124,23 @@ func (s *TCPServer) handleConnection(conn *gtcp.Conn) {
s.Connection.Add(tcpConn)
s.Logger.Info(s.ctx, fmt.Sprintf("New connection established: %s", connID))
// 存储到SQLite
connInfo := &pool.ConnectionInfo{
ID: connID,
Type: pool.ConnTypeTCP,
Address: conn.RemoteAddr().String(),
IsActive: true,
LastUsed: time.Now(),
CreatedAt: time.Now(),
Data: map[string]interface{}{
"localAddress": conn.LocalAddr().String(),
},
}
if err := s.Connection.sqlitePool.Add(connInfo); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to store connection to SQLite: %v", err))
// 不影响连接建立,仅记录错误
}
// 启动消息接收协程
go s.receiveMessages(tcpConn)
}
@ -121,6 +153,11 @@ func (s *TCPServer) receiveMessages(conn *TcpConnection) {
}
s.Connection.Remove(conn.Id)
conn.Server.Close()
// 从SQLite移除
if err := s.Connection.sqlitePool.Remove(conn.Id); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to remove connection from SQLite: %v", err))
// 不影响连接关闭,仅记录错误
}
s.Logger.Info(s.ctx, fmt.Sprintf("Connection closed: %s", conn.Id))
}()
@ -142,37 +179,79 @@ func (s *TCPServer) receiveMessages(conn *TcpConnection) {
if n > 0 {
// 更新最后使用时间
now := time.Now()
conn.Mutex.Lock()
conn.LastUsed = time.Now()
conn.LastUsed = now
// 将读取的数据添加到连接的缓冲区
conn.buffer = append(conn.buffer, buffer[:n]...)
conn.Mutex.Unlock()
// 处理消息
data := make([]byte, n)
copy(data, buffer[:n])
msg := &TcpMessage{
Id: fmt.Sprintf("msg_%d", gtime.TimestampNano()),
ConnId: conn.Id,
Data: data,
Timestamp: time.Now(),
IsSend: false,
// 更新SQLite中的连接信息
connInfo, err := s.Connection.sqlitePool.Get(conn.Id)
if err == nil && connInfo != nil {
connInfo.LastUsed = now
if err := s.Connection.sqlitePool.Update(connInfo); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to update connection in SQLite: %v", err))
// 不影响消息处理,仅记录错误
}
}
// 使用协程池处理消息,避免阻塞
grpool.AddWithRecover(s.ctx, func(ctx context.Context) {
if s.MessageHandler != nil {
if err := s.MessageHandler(conn, msg); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Message handling error: %v", err))
}
}
}, func(ctx context.Context, err error) {
s.Logger.Error(ctx, fmt.Sprintf("Message handling error: %v", err))
})
// 解析消息帧
s.parseMessageFrames(conn)
}
}
}
}
// parseMessageFrames 解析消息帧
func (s *TCPServer) parseMessageFrames(conn *TcpConnection) {
conn.Mutex.Lock()
defer conn.Mutex.Unlock()
for {
// 检查缓冲区是否有足够的数据来读取长度前缀
if len(conn.buffer) < messageLengthPrefixSize {
// 数据不足,等待下一次读取
return
}
// 读取长度前缀
length := binary.BigEndian.Uint32(conn.buffer[:messageLengthPrefixSize])
// 检查缓冲区是否有足够的数据来读取完整的消息
if len(conn.buffer) < messageLengthPrefixSize+int(length) {
// 数据不足,等待下一次读取
return
}
// 提取消息数据
data := conn.buffer[messageLengthPrefixSize : messageLengthPrefixSize+int(length)]
// 移除已处理的消息数据
conn.buffer = conn.buffer[messageLengthPrefixSize+int(length):]
// 创建消息对象
msg := &TcpMessage{
Id: fmt.Sprintf("msg_%d", gtime.TimestampNano()),
ConnId: conn.Id,
Data: data,
Timestamp: time.Now(),
IsSend: false,
}
// 使用协程池处理消息,避免阻塞
grpool.AddWithRecover(s.ctx, func(ctx context.Context) {
if s.MessageHandler != nil {
if err := s.MessageHandler(conn, msg); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Message handling error: %v", err))
}
}
}, func(ctx context.Context, err error) {
s.Logger.Error(ctx, fmt.Sprintf("Message handling error: %v", err))
})
}
}
// SendTo 发送消息到指定连接
func (s *TCPServer) SendTo(connID string, data []byte) error {
conn := s.Connection.Get(connID)
@ -202,14 +281,33 @@ func (s *TCPServer) sendMessage(conn *TcpConnection, data []byte) error {
// 设置写入超时
conn.Server.SetWriteDeadline(time.Now().Add(s.Config.WriteTimeout))
// 创建消息帧4字节长度前缀 + 消息数据
frame := make([]byte, messageLengthPrefixSize+len(data))
// 写入长度前缀(大端序)
binary.BigEndian.PutUint32(frame[:messageLengthPrefixSize], uint32(len(data)))
// 写入消息数据
copy(frame[messageLengthPrefixSize:], data)
// 发送数据
_, err := conn.Server.Write(data)
_, err := conn.Server.Write(frame)
if err != nil {
return err
}
// 更新最后使用时间
conn.LastUsed = time.Now()
now := time.Now()
conn.LastUsed = now
// 更新SQLite中的连接信息
connInfo, err := s.Connection.sqlitePool.Get(conn.Id)
if err == nil && connInfo != nil {
connInfo.LastUsed = now
if err := s.Connection.sqlitePool.Update(connInfo); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to update connection in SQLite: %v", err))
// 不影响消息发送,仅记录错误
}
}
return nil
}
@ -224,11 +322,21 @@ func (s *TCPServer) Kick(connID string) error {
conn.Server.Close()
// 从连接池移除
s.Connection.Remove(connID)
// 从SQLite移除
if err := s.Connection.sqlitePool.Remove(connID); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to remove connection from SQLite: %v", err))
// 不影响连接关闭,仅记录错误
}
s.Logger.Info(s.ctx, fmt.Sprintf("Kicked connection: %s", connID))
return nil
}
// GetAllConnIDs 获取所有在线连接的ID列表
func (s *TCPServer) GetAllConnIDs() ([]string, error) {
return s.Connection.GetAllConnIDs()
}
// Add 添加连接到连接池
func (p *ConnectionPool) Add(conn *TcpConnection) {
p.mutex.Lock()
@ -278,3 +386,8 @@ func (p *ConnectionPool) Count() int {
defer p.mutex.RUnlock()
return len(p.connections)
}
// GetAllConnIDs 获取所有在线连接的ID列表
func (p *ConnectionPool) GetAllConnIDs() ([]string, error) {
return p.sqlitePool.GetAllConnIDs()
}

View File

@ -7,6 +7,11 @@ import (
"github.com/gogf/gf/v2/net/gtcp"
)
// 消息帧格式4字节长度前缀 + 消息数据
const (
messageLengthPrefixSize = 4 // 消息长度前缀大小4字节
)
// TcpPoolConfig TCP连接池配置
type TcpPoolConfig struct {
BufferSize int `json:"bufferSize"` // 缓冲区大小
@ -26,6 +31,7 @@ type TcpConnection struct {
LastUsed time.Time `json:"lastUsed"` // 最后使用时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
Mutex sync.RWMutex `json:"-"` // 读写锁
buffer []byte `json:"-"` // 用于存储未处理的字节数据
}
// TcpMessage TCP消息结构

View File

@ -3,6 +3,7 @@ package utils
import (
"context"
"git.magicany.cc/black1552/gf-common/log"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/database/gdb"
@ -51,51 +52,51 @@ func NewClient[R any](request any, url string, header map[string]string) *SClien
return s
}
func (w *SClient[R]) Post(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "post", w.request)
log.Info("请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "post", w.request)
resp := w.client.PostVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
log.Error("请求异常:", err)
return nil, err
}
return
}
func (w *SClient[R]) Get(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "get", w.request)
log.Info("请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "get", w.request)
resp := w.client.GetVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
log.Error("解析响应体异常:", err)
return nil, err
}
return
}
func (w *SClient[R]) Put(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "put", w.request)
log.Info("请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "put", w.request)
resp := w.client.PutVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
log.Error("解析响应体异常:", err)
return nil, err
}
return
}
func (w *SClient[R]) Delete(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "delete", w.request)
log.Info("请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "delete", w.request)
resp := w.client.DeleteVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
log.Error("解析响应体异常:", err)
return nil, err
}
return
}
func (w *SClient[R]) Patch(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "patch", w.request)
log.Info("请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "patch", w.request)
resp := w.client.PatchVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
log.Error("解析响应体异常:", err)
return nil, err
}
return