游戏库
This commit is contained in:
parent
dfe83bc0ec
commit
44c8f13453
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
66
db/clickhouse.go
Normal file
66
db/clickhouse.go
Normal file
@ -0,0 +1,66 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ClickHouseRepo struct {
|
||||
host string
|
||||
port string
|
||||
user string
|
||||
password string
|
||||
database string
|
||||
connection *sql.DB
|
||||
}
|
||||
|
||||
func NewClickHouseRepo(host, port, user, password, database string) *ClickHouseRepo {
|
||||
return &ClickHouseRepo{
|
||||
host: host,
|
||||
port: port,
|
||||
user: user,
|
||||
password: password,
|
||||
database: database,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClickHouseRepo) Open() error {
|
||||
c.connection = clickhouse.OpenDB(&clickhouse.Options{
|
||||
Addr: []string{fmt.Sprintf("%v:%v", c.host, c.port)},
|
||||
Auth: clickhouse.Auth{
|
||||
Database: c.database,
|
||||
Username: c.user,
|
||||
Password: c.password,
|
||||
},
|
||||
Settings: clickhouse.Settings{
|
||||
"max_execution_time": 60,
|
||||
},
|
||||
DialTimeout: 5 * time.Second,
|
||||
Compression: &clickhouse.Compression{
|
||||
Method: clickhouse.CompressionLZ4,
|
||||
Level: 5,
|
||||
},
|
||||
Protocol: clickhouse.Native,
|
||||
// Debug: true,
|
||||
})
|
||||
if c.connection == nil {
|
||||
return fmt.Errorf("connect clickhouse fail")
|
||||
}
|
||||
c.connection.SetMaxIdleConns(5)
|
||||
c.connection.SetMaxOpenConns(10)
|
||||
c.connection.SetConnMaxLifetime(0)
|
||||
return c.connection.Ping()
|
||||
}
|
||||
|
||||
func (c *ClickHouseRepo) Close() {
|
||||
_ = c.connection.Close()
|
||||
}
|
||||
|
||||
func (c *ClickHouseRepo) Select(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return c.connection.Query(query, args...)
|
||||
}
|
||||
func (c *ClickHouseRepo) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
return c.connection.Exec(query, args...)
|
||||
}
|
97
db/clickhouse_test.go
Normal file
97
db/clickhouse_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testCreateClickHouse(_ *testing.T) *ClickHouseRepo {
|
||||
host := "192.168.2.224"
|
||||
port := "9000"
|
||||
// port = 8123
|
||||
username := "default"
|
||||
password := "123456"
|
||||
database := "samba"
|
||||
return NewClickHouseRepo(host, port, username, password, database)
|
||||
}
|
||||
|
||||
func testSelect(t *testing.T) {
|
||||
ch := testCreateClickHouse(t)
|
||||
err := ch.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ch.Close()
|
||||
rows, err := ch.Select("select user_id, coins from coins_flow limit 1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Println(strings.Join(cols, ","))
|
||||
// 一行数据,使用any,避开数据类型问题
|
||||
var vRows = make([]any, len(cols))
|
||||
// 存实际的值,byte数组,长度以列的数量为准
|
||||
var values = make([][]byte, len(cols))
|
||||
for i := 0; i < len(cols); i++ {
|
||||
vRows[i] = &values[i]
|
||||
}
|
||||
for rows.Next() {
|
||||
err = rows.Scan(vRows...)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
break
|
||||
}
|
||||
var vString []string
|
||||
for _, v := range values {
|
||||
vString = append(vString, string(v))
|
||||
}
|
||||
fmt.Println(vString, ",")
|
||||
}
|
||||
}
|
||||
|
||||
func testInsert(t *testing.T) {
|
||||
ch := testCreateClickHouse(t)
|
||||
err := ch.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ch.Close()
|
||||
_, err = ch.Exec("INSERT INTO coins_flow(user_id, coins) VALUES (11,11)", 11, 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// last, err := result.LastInsertId()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(last)
|
||||
}
|
||||
|
||||
func testDelete(t *testing.T) {
|
||||
ch := testCreateClickHouse(t)
|
||||
err := ch.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ch.Close()
|
||||
_, err = ch.Exec("DELETE FROM coins_flow WHERE user_id = 11 AND coins = 11")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// last, err := result.LastInsertId()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(last)
|
||||
}
|
||||
|
||||
func TestClickHouse(t *testing.T) {
|
||||
testSelect(t)
|
||||
// testInsert(t)
|
||||
testDelete(t)
|
||||
}
|
36
db/db.go
Normal file
36
db/db.go
Normal file
@ -0,0 +1,36 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func InitMysql(username, password, address, port, dbName string) (*gorm.DB, error) {
|
||||
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", username, password, address, port, dbName)
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: &dbLogger{}})
|
||||
return db, err
|
||||
}
|
||||
|
||||
func InitRedis(password, address, port string, dbName int) (*redis.Client, error) {
|
||||
rdb := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("%v:%v", address, port), Password: password, DB: dbName})
|
||||
if rdb == nil {
|
||||
return nil, fmt.Errorf("init redis fail")
|
||||
}
|
||||
return rdb, nil
|
||||
}
|
||||
|
||||
func InitClickHouse(host, port, user, password, database string) (*ClickHouseRepo, error) {
|
||||
ch := NewClickHouseRepo(host, port, user, password, database)
|
||||
return ch, ch.Open()
|
||||
}
|
||||
|
||||
/*
|
||||
sudo docker run -d \
|
||||
--name my-redis \
|
||||
-p 6379:6379 \
|
||||
-e REDIS_PASSWORD=fox379@@zyxi \
|
||||
redis:latest \
|
||||
--requirepass fox379@@zyxi
|
||||
*/
|
46
db/dblog.go
Normal file
46
db/dblog.go
Normal file
@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"gorm.io/gorm/logger"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dbLogger struct{}
|
||||
|
||||
func (l *dbLogger) LogMode(_ logger.LogLevel) logger.Interface {
|
||||
// 可以在这里根据 logLevel 做出不同的处理
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *dbLogger) Info(_ context.Context, msg string, args ...interface{}) {
|
||||
log.InfoF(msg, args...)
|
||||
}
|
||||
|
||||
func (l *dbLogger) Warn(_ context.Context, msg string, args ...interface{}) {
|
||||
log.WarnF(msg, args...)
|
||||
}
|
||||
|
||||
func (l *dbLogger) Error(_ context.Context, msg string, args ...interface{}) {
|
||||
s := fmt.Sprintf(msg, args...)
|
||||
if !strings.Contains(s, "record not found") {
|
||||
// 只有当错误不是“record not found”时才记录
|
||||
log.ErrorF(msg, args...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (l *dbLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
if err != nil {
|
||||
// SQL 执行错误
|
||||
l.Error(ctx, "Error occurred while executing SQL: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
sql, rows := fc()
|
||||
elapsed := time.Since(begin)
|
||||
log.DebugF("SQL: %s, RowsAffected: %d, Elapsed: %s\n", sql, rows, elapsed)
|
||||
}
|
65
etcd/etcd.go
Normal file
65
etcd/etcd.go
Normal file
@ -0,0 +1,65 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type resultT[T any] struct {
|
||||
Value T
|
||||
Err error
|
||||
}
|
||||
|
||||
type Registry[T INode] struct {
|
||||
*etcdRegistryImpl
|
||||
nodes sync.Map
|
||||
}
|
||||
|
||||
func NewRegistry[T INode](endpoints []string, rootKey, username, password string) (*Registry[T], error) {
|
||||
var err error
|
||||
e := &Registry[T]{}
|
||||
e.etcdRegistryImpl, err = newServiceRegistryImpl(endpoints, rootKey, username, password, e.saveNode)
|
||||
return e, err
|
||||
}
|
||||
|
||||
func (e *Registry[T]) Register(node INode) error {
|
||||
bs, err := json.Marshal(node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.etcdRegistryImpl.Register(node.EtcdKey(), string(bs))
|
||||
}
|
||||
|
||||
// 获取当前服务
|
||||
func (sr *Registry[T]) saveNode(jsonBytes []byte) {
|
||||
var tmp = resultT[T]{Err: nil}
|
||||
if err := json.Unmarshal(jsonBytes, &tmp.Value); err != nil {
|
||||
log.ErrorF(err.Error())
|
||||
}
|
||||
sr.nodes.Store(tmp.Value.MapKey(), tmp.Value)
|
||||
}
|
||||
|
||||
// 获取当前根节点下所有节点信息
|
||||
func (sr *Registry[T]) GetNodes() []T {
|
||||
var nodes []T
|
||||
sr.nodes.Range(func(k, v interface{}) bool {
|
||||
nodes = append(nodes, v.(T))
|
||||
return true
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
// 获取当前根节点下所有节点信息
|
||||
func (sr *Registry[T]) FindNode(key string) (T, error) {
|
||||
var tmp = resultT[T]{Err: nil}
|
||||
v, ok := sr.nodes.Load(key)
|
||||
if !ok {
|
||||
return tmp.Value, fmt.Errorf("%v not exist", key)
|
||||
}
|
||||
if tmp.Value, ok = v.(T); ok {
|
||||
return tmp.Value, nil
|
||||
}
|
||||
return tmp.Value, fmt.Errorf("%v 类型转换失败", key)
|
||||
}
|
167
etcd/etcdImpl.go
Normal file
167
etcd/etcdImpl.go
Normal file
@ -0,0 +1,167 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fox/fox/ksync"
|
||||
"github.com/fox/fox/log"
|
||||
"time"
|
||||
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDialTimeout = 3 * time.Second // 默认拨号超时时间
|
||||
DefaultLeaseTTL = int64(10) // 默认租约TTL为60秒
|
||||
DefaultKeepAliveInterval = 5 * time.Second // 默认续租间隔为30秒
|
||||
KeepAliveFailCount = 100
|
||||
)
|
||||
|
||||
type etcdRegistryImpl struct {
|
||||
cli *clientv3.Client
|
||||
leaseID clientv3.LeaseID
|
||||
nodeKey string
|
||||
cancelFunc context.CancelFunc
|
||||
rootKey string
|
||||
saveNodeFunc func(jsonBytes []byte)
|
||||
}
|
||||
|
||||
// 创建服务注册中心
|
||||
func newServiceRegistryImpl(endpoints []string, rootKey, username, password string, saveNode func([]byte)) (*etcdRegistryImpl, error) {
|
||||
cli, err := clientv3.New(clientv3.Config{
|
||||
Endpoints: endpoints,
|
||||
DialTimeout: DefaultDialTimeout,
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &etcdRegistryImpl{
|
||||
cli: cli,
|
||||
rootKey: rootKey,
|
||||
saveNodeFunc: saveNode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 注册服务 RegisterService
|
||||
func (sr *etcdRegistryImpl) Register(key, value string) error {
|
||||
// 生成唯一服务key
|
||||
// /services/serviceType/serviceName
|
||||
sr.nodeKey = key
|
||||
log.DebugF("register %s to etcd", key)
|
||||
|
||||
// 创建租约
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sr.cancelFunc = cancel
|
||||
|
||||
// 申请租约
|
||||
resp, err := sr.cli.Grant(ctx, DefaultLeaseTTL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sr.leaseID = resp.ID
|
||||
|
||||
// 序列化服务信息
|
||||
// data, err := json.Marshal(node)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// 写入ETCD
|
||||
_, err = sr.cli.Put(ctx, key, value, clientv3.WithLease(sr.leaseID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 启动自动续租
|
||||
go sr.keepAlive(ctx)
|
||||
|
||||
if err = sr.discoverServices(); err == nil {
|
||||
// ss := sr.GetService()
|
||||
// for _, s := range ss {
|
||||
// log.Debug(fmt.Sprintf("Discovered services: %+v\n", s))
|
||||
// }
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 自动续租逻辑
|
||||
func (sr *etcdRegistryImpl) keepAlive(ctx context.Context) {
|
||||
retryCount := 0
|
||||
ticker := time.NewTicker(DefaultKeepAliveInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err := sr.cli.KeepAliveOnce(ctx, sr.leaseID)
|
||||
if err != nil {
|
||||
if retryCount > KeepAliveFailCount {
|
||||
log.DebugF("KeepAlive failed after %d retries: %v", KeepAliveFailCount, err)
|
||||
sr.UnregisterService()
|
||||
return
|
||||
}
|
||||
retryCount++
|
||||
// log.DebugF("KeepAlive error (retry %d/%d): %v", retryCount, KeepAliveFailCount, err)
|
||||
} else {
|
||||
retryCount = 0
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 反注册服务
|
||||
func (sr *etcdRegistryImpl) UnregisterService() {
|
||||
if sr.cancelFunc != nil {
|
||||
sr.cancelFunc()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := sr.cli.Delete(ctx, sr.nodeKey); err != nil {
|
||||
log.ErrorF("unregister:%v failed:%v from etcd", sr.nodeKey, err)
|
||||
} else {
|
||||
log.DebugF("unregister:%v from etcd", sr.nodeKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 服务发现
|
||||
func (sr *etcdRegistryImpl) discoverServices() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
srv := fmt.Sprintf("/%s/", sr.rootKey)
|
||||
resp, err := sr.cli.Get(ctx, srv, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// log.Debug(fmt.Sprintf("discoverServices srv:%s", srv))
|
||||
|
||||
for _, kv := range resp.Kvs {
|
||||
sr.saveNodeFunc(kv.Value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 监控服务变化
|
||||
func (sr *etcdRegistryImpl) WatchServices() {
|
||||
watchKey := fmt.Sprintf("/%s/", sr.rootKey)
|
||||
ksync.GoSafe(func() {
|
||||
rch := sr.cli.Watch(context.Background(), watchKey, clientv3.WithPrefix())
|
||||
for resp := range rch {
|
||||
for range resp.Events {
|
||||
// 当有变化时获取最新服务列表
|
||||
if err := sr.discoverServices(); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
}
|
91
etcd/etcd_test.go
Normal file
91
etcd/etcd_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package etcd
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
etcdAddress1 = "192.168.232.128:2379"
|
||||
etcdAddress2 = "114.132.124.145:2379"
|
||||
)
|
||||
|
||||
/*
|
||||
sudo docker run -d \
|
||||
--name my-etcd \
|
||||
-p 2379:2379 \
|
||||
-p 2380:2380 \
|
||||
quay.io/coreos/etcd:v3.6.0 \
|
||||
etcd \
|
||||
--name etcd-single \
|
||||
--data-dir /etcd-data \
|
||||
--initial-advertise-peer-urls http://0.0.0.0:2380 \
|
||||
--listen-peer-urls http://0.0.0.0:2380 \
|
||||
--advertise-client-urls http://0.0.0.0:2379 \
|
||||
--listen-client-urls http://0.0.0.0:2379 \
|
||||
--initial-cluster etcd-single=http://0.0.0.0:2380
|
||||
|
||||
sudo docker run -d --name etcdkeeper -p 8080:8080 evildecay/etcdkeeper
|
||||
*/
|
||||
func TestService(t *testing.T) {
|
||||
_ = etcdAddress1
|
||||
_ = etcdAddress2
|
||||
log.Open("test.log", log.DebugL)
|
||||
// 创建注册中心
|
||||
registry, err := NewRegistry[ServiceNode]([]string{etcdAddress2}, rootKeyServices, "", "")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// 注册示例服务
|
||||
service := &ServiceNode{
|
||||
Name: "instance-1",
|
||||
Type: "user-service",
|
||||
Address: "localhost",
|
||||
Port: 8080,
|
||||
}
|
||||
|
||||
if err := registry.Register(service); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// 监控服务变化
|
||||
registry.WatchServices()
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
registry.UnregisterService()
|
||||
}
|
||||
|
||||
func TestTopicRegistry(t *testing.T) {
|
||||
_ = etcdAddress1
|
||||
_ = etcdAddress2
|
||||
log.Open("test.log", log.DebugL)
|
||||
// 创建注册中心
|
||||
registry, err := NewRegistry[TopicNode]([]string{etcdAddress2}, rootKeyTopic, "", "")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// 注册示例服务
|
||||
node := &TopicNode{
|
||||
Name: "instance-1",
|
||||
Creator: "instance-1",
|
||||
}
|
||||
|
||||
if err := registry.Register(node); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// 监控服务变化
|
||||
registry.WatchServices()
|
||||
for _, n := range registry.GetNodes() {
|
||||
log.DebugF("发现topic:%v, 创建者:%v", n.Name, n.Creator)
|
||||
if v, err := registry.FindNode(n.MapKey()); err == nil {
|
||||
log.DebugF("topic:%v exist", v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
registry.UnregisterService()
|
||||
}
|
53
etcd/inode.go
Normal file
53
etcd/inode.go
Normal file
@ -0,0 +1,53 @@
|
||||
package etcd
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
rootKeyServices = "services"
|
||||
rootKeyTopic = "topic"
|
||||
)
|
||||
|
||||
type INode interface {
|
||||
// 注册到etcd的key
|
||||
EtcdKey() string
|
||||
EtcdRootKey() string
|
||||
MapKey() string
|
||||
}
|
||||
|
||||
type ServiceNode struct {
|
||||
Name string `json:"name"` // 服务名 多个同类服务依赖name区分:1,2,3等等
|
||||
Type string `json:"type"` // 服务类型:lobby, game, gate等等
|
||||
Address string `json:"address"` // 地址
|
||||
Port int `json:"port"` // 端口
|
||||
Version string `json:"version"` // 版本号
|
||||
ServiceType ServiceType `json:"service_type"` // 服务类型
|
||||
}
|
||||
|
||||
func (s ServiceNode) EtcdKey() string {
|
||||
return fmt.Sprintf("/%s/%s/%s", rootKeyServices, s.Type, s.Name)
|
||||
}
|
||||
|
||||
func (s ServiceNode) EtcdRootKey() string {
|
||||
return rootKeyServices
|
||||
}
|
||||
|
||||
func (s ServiceNode) MapKey() string {
|
||||
return fmt.Sprintf("%s-%s", s.Type, s.Name)
|
||||
}
|
||||
|
||||
type TopicNode struct {
|
||||
Name string `json:"name"` // topic名
|
||||
Creator string `json:"creator"` // topic创建者
|
||||
}
|
||||
|
||||
func (s TopicNode) EtcdKey() string {
|
||||
return fmt.Sprintf("/%s/%s/%s", rootKeyTopic, s.Creator, s.Name)
|
||||
}
|
||||
|
||||
func (s TopicNode) EtcdRootKey() string {
|
||||
return rootKeyTopic
|
||||
}
|
||||
|
||||
func (s TopicNode) MapKey() string {
|
||||
return fmt.Sprintf("%s", s.Name)
|
||||
}
|
19
etcd/serviceType.go
Normal file
19
etcd/serviceType.go
Normal file
@ -0,0 +1,19 @@
|
||||
package etcd
|
||||
|
||||
type ServiceType int
|
||||
|
||||
const (
|
||||
Unique ServiceType = 1 // 唯一
|
||||
Multiple ServiceType = 2 // 多个
|
||||
)
|
||||
|
||||
func (s ServiceType) String() string {
|
||||
switch s {
|
||||
case Unique:
|
||||
return "unique"
|
||||
case Multiple:
|
||||
return "multiple"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
67
go.mod
Normal file
67
go.mod
Normal file
@ -0,0 +1,67 @@
|
||||
module github.com/fox/fox
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.7
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.34.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-module/carbon/v2 v2.6.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/nats-io/nats.go v1.42.0
|
||||
github.com/nsqio/go-nsq v1.1.0
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wanghuiyt/ding v0.0.2
|
||||
go.etcd.io/etcd/client/v3 v3.5.19
|
||||
go.uber.org/zap v1.27.0
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.26.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.65.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dromara/carbon/v2 v2.6.5 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.19 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.19 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
198
go.sum
Normal file
198
go.sum
Normal file
@ -0,0 +1,198 @@
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU=
|
||||
github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dromara/carbon/v2 v2.6.5 h1:OC1k8zGBpSnRoPjezlWeajx+3nCMq7xhZqAS4WWrKmE=
|
||||
github.com/dromara/carbon/v2 v2.6.5/go.mod h1:7GXqCUplwN1s1b4whGk2zX4+g4CMCoDIZzmjlyt0vLY=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-module/carbon/v2 v2.6.5 h1:C3YpydJZmo77AyMsgQ1QpVDQeLJ0itOa5sQB4Y/jk4I=
|
||||
github.com/golang-module/carbon/v2 v2.6.5/go.mod h1:JvSYEoe3+OcMnSQyRvUNQFpg0T/4xCq7moHu1S8ESXQ=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/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/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=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||
github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
|
||||
github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
|
||||
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
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/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/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/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/wanghuiyt/ding v0.0.2 h1:6ZISlgCSy6MVeaFR8kAdniALMRqd56GyO9LlmYdTw/s=
|
||||
github.com/wanghuiyt/ding v0.0.2/go.mod h1:T1vPz74YMmGCBVKZzVsen/YAYRZ2bvBYXldUyD7Y4vc=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/etcd/api/v3 v3.5.19 h1:w3L6sQZGsWPuBxRQ4m6pPP3bVUtV8rjW033EGwlr0jw=
|
||||
go.etcd.io/etcd/api/v3 v3.5.19/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.19 h1:9VsyGhg0WQGjDWWlDI4VuaS9PZJGNbPkaHEIuLwtixk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.19/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0=
|
||||
go.etcd.io/etcd/client/v3 v3.5.19 h1:+4byIz6ti3QC28W0zB0cEZWwhpVHXdrKovyycJh1KNo=
|
||||
go.etcd.io/etcd/client/v3 v3.5.19/go.mod h1:FNzyinmMIl0oVsty1zA3hFeUrxXI/JpEnz4sG+POzjU=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
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/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=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/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=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/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=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
|
||||
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
76
ksync/ksync.go
Normal file
76
ksync/ksync.go
Normal file
@ -0,0 +1,76 @@
|
||||
package ksync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/wanghuiyt/ding"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 辅助函数列表
|
||||
var (
|
||||
DingAccessToken = "" // 钉钉token
|
||||
DingSecret = "" // 钉钉加签
|
||||
lastAttributeCode = "" // 最近一条error信息
|
||||
)
|
||||
|
||||
func getAttributeCode(stackErr string) string {
|
||||
lines := strings.Split(stackErr, "\n")
|
||||
// 检查是否有足够的行数,并提取第9行(索引为8,因为索引从0开始)
|
||||
if len(lines) >= 9 {
|
||||
goIndex := strings.LastIndex(lines[8], ".go")
|
||||
if goIndex != -1 {
|
||||
filteredStr := lines[8][:goIndex]
|
||||
filteredStr = strings.TrimLeft(filteredStr, "\t ")
|
||||
return filteredStr
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 发送消息给钉钉
|
||||
func sendMessage(msg interface{}, stackErr string, dingToken, dingSecret string) {
|
||||
if dingToken != "" {
|
||||
d := ding.Webhook{
|
||||
AccessToken: dingToken, // 上面获取的 access_token
|
||||
Secret: dingSecret, // 上面获取的加签的值
|
||||
}
|
||||
_ = d.SendMessageText(fmt.Sprintf("Recover panic:%v\n%v", msg, stackErr), "13145922265", "17353003985")
|
||||
}
|
||||
}
|
||||
|
||||
func Recover(recoverFunc func()) {
|
||||
if msg := recover(); msg != nil {
|
||||
stackErr := string(debug.Stack())
|
||||
attributeCode := getAttributeCode(stackErr)
|
||||
if lastAttributeCode != attributeCode {
|
||||
lastAttributeCode = attributeCode
|
||||
log.ErrorF("Recover panic:%v", msg)
|
||||
log.Error(stackErr)
|
||||
sendMessage(msg, stackErr, DingAccessToken, DingSecret)
|
||||
}
|
||||
if recoverFunc != nil {
|
||||
recoverFunc()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// RunSafe runs the given fn, recovers if fn panics.
|
||||
func RunSafe(fn func(), recoverFunc func()) {
|
||||
defer Recover(recoverFunc)
|
||||
fn()
|
||||
}
|
||||
|
||||
// GoSafe runs the given fn using another goroutine, recovers if fn panics.
|
||||
func GoSafe(fn func(), recoverFunc func()) {
|
||||
go RunSafe(fn, recoverFunc)
|
||||
}
|
||||
|
||||
// WrapSafe return a RunSafe wrap func
|
||||
func WrapSafe(fn func(), recoverFunc func()) func() {
|
||||
return func() {
|
||||
RunSafe(fn, recoverFunc)
|
||||
}
|
||||
}
|
34
ksync/ksync_test.go
Normal file
34
ksync/ksync_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
package ksync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/wanghuiyt/ding"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEtcd(t *testing.T) {
|
||||
log.Open("test.log", log.DebugL)
|
||||
WrapSafe(func() { fmt.Println("hello world") }, nil)
|
||||
}
|
||||
|
||||
func TestSendDingTalkMessage(t *testing.T) {
|
||||
d := ding.Webhook{
|
||||
AccessToken: "9802639f5dea7dd4aaff98a8f264b98c224f90ceeed3c26b438534e3a79222b1", // 上面获取的 access_token
|
||||
Secret: "SECffd7d2afe9d0590fd04e36a0efa44d73553fb8f702eb1690e579996aec6c1386", // 上面获取的加签的值
|
||||
}
|
||||
_ = d.SendMessageText("这是普通的群消息", "13145922265", "17353003985")
|
||||
}
|
||||
|
||||
func TestRecover(t *testing.T) {
|
||||
log.Open("test.log", log.DebugL)
|
||||
cb := func() {
|
||||
i := new(int)
|
||||
*i = 1
|
||||
i = nil
|
||||
*i = 2
|
||||
}
|
||||
GoSafe(cb, cb)
|
||||
time.Sleep(time.Minute)
|
||||
}
|
156
log/log.go
Normal file
156
log/log.go
Normal file
@ -0,0 +1,156 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/natefinch/lumberjack"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *zap.Logger
|
||||
file *os.File
|
||||
)
|
||||
|
||||
const (
|
||||
DebugL = zapcore.DebugLevel
|
||||
InfoL = zapcore.InfoLevel
|
||||
WarnL = zapcore.WarnLevel
|
||||
ErrorL = zapcore.ErrorLevel
|
||||
)
|
||||
|
||||
func Open(filepath string, level zapcore.Level) {
|
||||
if level < DebugL || level > ErrorL {
|
||||
level = DebugL
|
||||
}
|
||||
// 自定义时间编码器,不显示时区
|
||||
customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006-01-02 15:04:05.000")) // 使用不含时区的格式
|
||||
}
|
||||
// 配置lumberjack
|
||||
file := &lumberjack.Logger{
|
||||
Filename: filepath, // 日志文件的位置
|
||||
MaxSize: 100, // 每个日志文件保存的最大尺寸 单位:MB
|
||||
MaxBackups: 3, // 日志文件最多保存多少个备份
|
||||
MaxAge: 28, // 文件最多保存多少天
|
||||
Compress: true, // 是否压缩/归档旧文件
|
||||
}
|
||||
|
||||
// var err error
|
||||
// // 打开日志文件
|
||||
// file, err = os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
// if err != nil {
|
||||
// zap.L().Fatal("无法打开日志文件", zap.Error(err))
|
||||
// }
|
||||
|
||||
// 设置编码器配置
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: customTimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
// 创建控制台输出
|
||||
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
consoleWriter := zapcore.AddSync(os.Stdout)
|
||||
consoleCore := zapcore.NewCore(consoleEncoder, consoleWriter, zap.NewAtomicLevelAt(zap.DebugLevel))
|
||||
|
||||
// 创建文件输出
|
||||
fileEncoder := zapcore.NewJSONEncoder(encoderConfig)
|
||||
fileWriter := zapcore.AddSync(file)
|
||||
fileCore := zapcore.NewCore(fileEncoder, fileWriter, zap.NewAtomicLevelAt(level))
|
||||
|
||||
// 将两个 Core 组合成一个 MultiWriteSyncer
|
||||
core := zapcore.NewTee(consoleCore, fileCore)
|
||||
|
||||
// 创建logger对象
|
||||
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||
|
||||
// Info = logger.Info
|
||||
// Debug = logger.Debug
|
||||
// Warn = logger.Warn
|
||||
// Error = logger.Error
|
||||
// Fatal = logger.Fatal
|
||||
}
|
||||
|
||||
func Close() {
|
||||
if logger != nil {
|
||||
_ = logger.Sync()
|
||||
logger = nil
|
||||
}
|
||||
if file != nil {
|
||||
_ = file.Close()
|
||||
file = nil
|
||||
}
|
||||
}
|
||||
|
||||
func StackTrace() string {
|
||||
var pcs [32]uintptr
|
||||
n := runtime.Callers(3, pcs[:]) // 跳过前3个栈帧,即printStackTrace, caller, 和runtime.Callers本身
|
||||
frames := runtime.CallersFrames(pcs[:n])
|
||||
s := ""
|
||||
for {
|
||||
frame, more := frames.Next()
|
||||
s += fmt.Sprintf("%+v\n", frame)
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func InfoF(format string, args ...any) {
|
||||
logger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Info(msg string, fields ...zap.Field) {
|
||||
logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
func Debug(msg string, fields ...zap.Field) {
|
||||
logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
func DebugF(format string, args ...any) {
|
||||
logger.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Warn(msg string, fields ...zap.Field) {
|
||||
logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
func WarnF(format string, args ...any) {
|
||||
logger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Error(msg string, fields ...zap.Field) {
|
||||
logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
func ErrorF(format string, args ...any) {
|
||||
logger.Error(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Fatal(msg string, fields ...zap.Field) {
|
||||
logger.Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
func FatalF(format string, args ...any) {
|
||||
logger.Fatal(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// 提供logger,方便其它库打印正确的日志调用点
|
||||
func GetLogger() *zap.Logger {
|
||||
return logger
|
||||
}
|
44
log/log_test.go
Normal file
44
log/log_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testLogger(t *testing.T, level zapcore.Level) {
|
||||
_ = t
|
||||
Open("test.log", level)
|
||||
defer Close()
|
||||
|
||||
Debug("debug. ", zap.String("name", "liu"))
|
||||
Info("info. ", zap.Error(fmt.Errorf("this is an error")))
|
||||
Warn("warn.", zap.Int64("int64", 111))
|
||||
Error("error.", zap.Bool("bool", true))
|
||||
Fatal("fatal. ", zap.String("name", "liu"))
|
||||
DebugF("debugF: %.2f", 1.23)
|
||||
InfoF("infoF: %.2f", 1.23)
|
||||
WarnF("warnF: %.2f", 1.23)
|
||||
ErrorF("errorF: %.2f", 1.23)
|
||||
FatalF("fatalF: %.2f", 1.23)
|
||||
}
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
testLogger(t, DebugL)
|
||||
testLogger(t, InfoL)
|
||||
testLogger(t, WarnL)
|
||||
testLogger(t, ErrorL)
|
||||
}
|
||||
|
||||
func testStackTrace(t *testing.T, level zapcore.Level) {
|
||||
_ = t
|
||||
Open("test.log", level)
|
||||
defer Close()
|
||||
stack := StackTrace()
|
||||
Debug(stack)
|
||||
}
|
||||
|
||||
func TestStackTrace(t *testing.T) {
|
||||
testStackTrace(t, DebugL)
|
||||
}
|
147
nat/nats.go
Normal file
147
nat/nats.go
Normal file
@ -0,0 +1,147 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/fox/fox/safeChan"
|
||||
"github.com/nats-io/nats.go"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RpcHandler func(msg []byte) ([]byte, error)
|
||||
|
||||
type Nats struct {
|
||||
address []string
|
||||
nc *nats.Conn
|
||||
sub []*nats.Subscription
|
||||
name string
|
||||
mt sync.Mutex
|
||||
}
|
||||
|
||||
// []string{
|
||||
// "nats://server1:4222",
|
||||
// "nats://server2:4222",
|
||||
// "nats://server3:4222",
|
||||
// }
|
||||
func NewNats(name string, address ...string) *Nats {
|
||||
n := &Nats{
|
||||
address: address,
|
||||
nc: nil,
|
||||
sub: nil,
|
||||
name: name,
|
||||
mt: sync.Mutex{},
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *Nats) Connect() error {
|
||||
opts := nats.GetDefaultOptions()
|
||||
opts.Servers = n.address
|
||||
opts.AllowReconnect = true
|
||||
opts.MaxReconnect = 10
|
||||
opts.ReconnectWait = 5 * time.Second
|
||||
opts.Name = n.name
|
||||
|
||||
opts.ClosedCB = func(conn *nats.Conn) {
|
||||
_ = conn
|
||||
log.Info("nats 连接已关闭")
|
||||
}
|
||||
opts.DisconnectedErrCB = func(conn *nats.Conn, err error) {
|
||||
if err != nil {
|
||||
log.ErrorF("nats 连接断开, err:%v", err)
|
||||
}
|
||||
}
|
||||
nc, err := opts.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.nc = nc
|
||||
log.InfoF("连接nats成功,当前服务器:%v", nc.ConnectedUrl())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nats) Close() {
|
||||
if n.nc != nil {
|
||||
n.nc.Close()
|
||||
}
|
||||
n.clearAllSub()
|
||||
}
|
||||
|
||||
func (n *Nats) SubscribeRpc(topic string, rpcHandler RpcHandler) error {
|
||||
if rpcHandler == nil {
|
||||
return fmt.Errorf("rpc handler is nil")
|
||||
}
|
||||
rspErrF := func(m *nats.Msg) {
|
||||
if err := m.Respond([]byte("error")); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) {
|
||||
rsp, err := rpcHandler(m.Data)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
rspErrF(m)
|
||||
return
|
||||
}
|
||||
if err = m.Respond(rsp); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.addSub(sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nats) Subscribe(topic string, msgChan *safeChan.ByteChan) error {
|
||||
sub, err := n.nc.Subscribe(topic, func(m *nats.Msg) {
|
||||
_ = msgChan.Write(m.Data)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.addSub(sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nats) Publish(topic string, msg []byte) error {
|
||||
return n.nc.Publish(topic, msg)
|
||||
}
|
||||
|
||||
func (n *Nats) Rpc(topic string, msg []byte) ([]byte, error) {
|
||||
rsp, err := n.nc.Request(topic, msg, 30*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rsp.Data, nil
|
||||
}
|
||||
|
||||
// 队列订阅,队列中只会有一个消费者消费该消息
|
||||
func (n *Nats) QueueSubscribe(topic string, queue string, msgChan *safeChan.ByteChan) error {
|
||||
sub, err := n.nc.QueueSubscribe(topic, queue, func(m *nats.Msg) {
|
||||
_ = msgChan.Write(m.Data)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.addSub(sub)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nats) addSub(sub *nats.Subscription) {
|
||||
n.mt.Lock()
|
||||
defer n.mt.Unlock()
|
||||
n.sub = append(n.sub, sub)
|
||||
}
|
||||
|
||||
func (n *Nats) clearAllSub() {
|
||||
n.mt.Lock()
|
||||
defer n.mt.Unlock()
|
||||
for _, sub := range n.sub {
|
||||
_ = sub.Unsubscribe()
|
||||
}
|
||||
n.sub = n.sub[:0]
|
||||
}
|
117
nat/nats_test.go
Normal file
117
nat/nats_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/ksync"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/fox/fox/safeChan"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
nats1 = "nats://192.168.232.128:4222"
|
||||
nats2 = "nats://114.132.124.145:4222"
|
||||
)
|
||||
|
||||
func initLog() {
|
||||
_ = nats1
|
||||
_ = nats2
|
||||
log.Open("test.log", log.DebugL)
|
||||
}
|
||||
|
||||
/*
|
||||
docker pull nats:latest
|
||||
docker run -d --name my-nats -p 4222:4222 -p 8222:8222 nats
|
||||
*/
|
||||
func TestNats(t *testing.T) {
|
||||
initLog()
|
||||
n := NewNats("test", nats2)
|
||||
if err := n.Connect(); err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
|
||||
topic := "test.topic"
|
||||
for i := 0; i < 2; i++ {
|
||||
msgChan := safeChan.NewSafeChan[[]byte](128)
|
||||
if err := n.Subscribe(topic, msgChan); err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
ksync.GoSafe(func() {
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-msgChan.Reader():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
t.Log("consumer:", i, string(msg))
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
}
|
||||
count := 0
|
||||
for {
|
||||
count++
|
||||
_ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count)))
|
||||
if count > 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
n.Close()
|
||||
}
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
initLog()
|
||||
n := NewNats("test", "nats://192.168.232.128:4222")
|
||||
if err := n.Connect(); err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
|
||||
topic := "test.group"
|
||||
queue := "test.queue"
|
||||
|
||||
count2 := int32(0)
|
||||
for i := 0; i < 3; i++ {
|
||||
msgChan := safeChan.NewSafeChan[[]byte](128)
|
||||
if err := n.QueueSubscribe(topic, queue, msgChan); err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
ksync.GoSafe(func() {
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-msgChan.Reader():
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = atomic.AddInt32(&count2, 1)
|
||||
_ = msg
|
||||
// t.Log("consumer:", i, string(msg))
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
}
|
||||
|
||||
count := int32(0)
|
||||
for {
|
||||
count++
|
||||
_ = n.Publish(topic, []byte(fmt.Sprintf("hello nats:%v", count)))
|
||||
if count > 900000 {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
c := atomic.LoadInt32(&count2)
|
||||
if c == count {
|
||||
t.Log("count==count2==", c)
|
||||
} else {
|
||||
t.Log("count:", count, " count2:", count2)
|
||||
}
|
||||
|
||||
n.Close()
|
||||
}
|
128
nsq/consumer.go
Normal file
128
nsq/consumer.go
Normal file
@ -0,0 +1,128 @@
|
||||
package nsq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/nsqio/go-nsq"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
chanLen = 20 // 消息队列容量
|
||||
)
|
||||
|
||||
type chanState int
|
||||
|
||||
const (
|
||||
idle chanState = 0
|
||||
busy chanState = 1
|
||||
)
|
||||
|
||||
func (ch chanState) String() string {
|
||||
switch ch {
|
||||
case idle:
|
||||
return "idle"
|
||||
case busy:
|
||||
return "busy"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Consumer struct {
|
||||
name string
|
||||
addr string
|
||||
topic string
|
||||
channel string
|
||||
msgChan chan *nsq.Message
|
||||
consumer *nsq.Consumer
|
||||
state chanState
|
||||
}
|
||||
|
||||
func newConsumer(nsqAddr, topic, channel string) (*Consumer, error) {
|
||||
var err error
|
||||
config := nsq.NewConfig()
|
||||
// 心跳设置
|
||||
config.HeartbeatInterval = 30 * time.Second
|
||||
// 关键配置项
|
||||
// config.DialTimeout = 10 * time.Second // TCP 连接超时(默认系统级)
|
||||
// config.ReadTimeout = 60 * time.Second // 读取数据超时
|
||||
// config.WriteTimeout = 60 * time.Second // 写入数据超时
|
||||
//
|
||||
// // 如果是通过 nsqlookupd 发现服务,调整 HTTP 客户端超时
|
||||
// config.LookupdPollInterval = 5 * time.Second // 轮询间隔
|
||||
// config.LookupdPollTimeout = 10 * time.Second // 单个 HTTP 请求超时
|
||||
|
||||
c := &Consumer{}
|
||||
c.addr = nsqAddr
|
||||
c.state = idle
|
||||
c.topic = topic
|
||||
c.channel = channel
|
||||
c.name = fmt.Sprintf("%v-%v", topic, channel)
|
||||
c.msgChan = make(chan *nsq.Message, chanLen)
|
||||
c.consumer, err = nsq.NewConsumer(topic, channel, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.consumer.AddHandler(c)
|
||||
|
||||
c.consumer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// 创建消费者,直连NSQd发现服务
|
||||
func NewConsumerByNsqD(nsqDAddr, topic, channel string) (*Consumer, error) {
|
||||
c, err := newConsumer(nsqDAddr, topic, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 连接方式选择:直连NSQd
|
||||
if err = c.consumer.ConnectToNSQD(nsqDAddr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.DebugF("consumer %v-%v 注册成功", topic, channel)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// 创建消费者,连接NSQLookupD发现服务
|
||||
func NewConsumer(lookupAddr, topic, channel string) (*Consumer, error) {
|
||||
c, err := newConsumer(lookupAddr, topic, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 连接NSQLookupD发现服务
|
||||
err = c.consumer.ConnectToNSQLookupd(c.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.DebugF("consumer %v-%v 注册成功", topic, channel)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (c *Consumer) HandleMessage(msg *nsq.Message) error {
|
||||
num := len(c.msgChan)
|
||||
if num > chanLen/2 && c.state == idle {
|
||||
c.state = busy
|
||||
log.WarnF("%v-%v 通道已从闲时转为繁忙状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan))
|
||||
} else if c.state == busy && num < chanLen/4 {
|
||||
c.state = idle
|
||||
log.WarnF("%v-%v 通道已从繁忙转为闲时状态。当前积压消息数:%v", c.topic, c.channel, len(c.msgChan))
|
||||
}
|
||||
c.msgChan <- msg
|
||||
msg.Finish()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Consumer) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c *Consumer) Read() <-chan *nsq.Message {
|
||||
return c.msgChan
|
||||
}
|
||||
|
||||
func (c *Consumer) Close() {
|
||||
if c.consumer != nil {
|
||||
c.consumer.Stop()
|
||||
}
|
||||
}
|
93
nsq/nsq_test.go
Normal file
93
nsq/nsq_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
package nsq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/nsqio/go-nsq"
|
||||
"go.uber.org/zap"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
testTopic = "test_topic"
|
||||
testChannel = "test_channel"
|
||||
testChannelD = "test_channelD"
|
||||
testAddress = "192.168.232.128:4150"
|
||||
testLookupAddress = "192.168.232.128:4161"
|
||||
)
|
||||
|
||||
func initLog() {
|
||||
log.Open("test.log", log.DebugL)
|
||||
}
|
||||
|
||||
func toString(id nsq.MessageID) string {
|
||||
sid := ""
|
||||
for _, b := range id {
|
||||
sid += string(b)
|
||||
}
|
||||
return sid
|
||||
}
|
||||
|
||||
func testProducer() *Producer {
|
||||
// 初始化一个生产者
|
||||
producer, err := NewProducer(testAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
for n := 0; n <= 20; n++ {
|
||||
err = producer.Publish(testTopic, []byte(fmt.Sprintf("hello nsq %d", n)))
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
// time.Sleep(1 * time.Second)
|
||||
}
|
||||
return producer
|
||||
}
|
||||
|
||||
func testConsumers() {
|
||||
consumer, err := NewConsumerByNsqD(testAddress, testTopic, testChannel)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
defer consumer.Close()
|
||||
time.Sleep(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case msg := <-consumer.Read():
|
||||
log.Debug(consumer.Name(), zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testConsumerByLookupD() {
|
||||
consumer, err := NewConsumer(testLookupAddress, testTopic, testChannelD)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
defer consumer.Close()
|
||||
time.Sleep(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case msg := <-consumer.Read():
|
||||
log.Debug(consumer.name, zap.String("id", toString(msg.ID)), zap.String("body", string(msg.Body)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNsq(t *testing.T) {
|
||||
initLog()
|
||||
// go testConsumers()
|
||||
// go testConsumerByLookupD()
|
||||
producer := testProducer()
|
||||
if producer != nil {
|
||||
defer producer.Close()
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
log.Debug("shutdown")
|
||||
}
|
31
nsq/nsqlog.go
Normal file
31
nsq/nsqlog.go
Normal file
@ -0,0 +1,31 @@
|
||||
package nsq
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/nsqio/go-nsq"
|
||||
)
|
||||
|
||||
type nsqLogger struct {
|
||||
level nsq.LogLevel
|
||||
}
|
||||
|
||||
func newNSQLogger(level nsq.LogLevel) *nsqLogger {
|
||||
return &nsqLogger{level}
|
||||
}
|
||||
|
||||
func (l *nsqLogger) Output(callDepth int, s string) error {
|
||||
_ = callDepth
|
||||
switch l.level {
|
||||
case nsq.LogLevelDebug:
|
||||
log.GetLogger().Debug(s)
|
||||
case nsq.LogLevelInfo:
|
||||
log.GetLogger().Error(s)
|
||||
case nsq.LogLevelWarning:
|
||||
log.GetLogger().Error(s)
|
||||
case nsq.LogLevelError:
|
||||
log.GetLogger().Error(s)
|
||||
default:
|
||||
log.GetLogger().Error(s)
|
||||
}
|
||||
return nil
|
||||
}
|
76
nsq/producer.go
Normal file
76
nsq/producer.go
Normal file
@ -0,0 +1,76 @@
|
||||
package nsq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/nsqio/go-nsq"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Producer struct {
|
||||
nsqAddr []string
|
||||
config *nsq.Config
|
||||
producer *nsq.Producer
|
||||
size int
|
||||
pos int
|
||||
}
|
||||
|
||||
func initProducer(p *Producer, nsqAddr string) error {
|
||||
var err error
|
||||
p.producer, err = nsq.NewProducer(nsqAddr, p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.producer.SetLogger(newNSQLogger(nsq.LogLevelError), nsq.LogLevelError)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建建生产者 nsqAddr:127.0.0.1:4150, 127.0.0.2:4150
|
||||
func NewProducer(nsqAddr ...string) (*Producer, error) {
|
||||
var err error
|
||||
p := new(Producer)
|
||||
p.config = nsq.NewConfig()
|
||||
// 心跳设置
|
||||
p.config.HeartbeatInterval = 30 * time.Second
|
||||
|
||||
p.nsqAddr = nsqAddr
|
||||
p.size = len(nsqAddr)
|
||||
p.pos = 0
|
||||
if len(nsqAddr) == 0 {
|
||||
return nil, errors.New("nsqAddr is empty")
|
||||
}
|
||||
|
||||
err = initProducer(p, p.nsqAddr[0])
|
||||
return p, err
|
||||
}
|
||||
|
||||
// 向主题发布消息
|
||||
func (p *Producer) Publish(topic string, data []byte) error {
|
||||
var err error
|
||||
for {
|
||||
if p.producer != nil {
|
||||
err = p.producer.Publish(topic, data)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
// 切换ip重发
|
||||
p.producer.Stop()
|
||||
}
|
||||
|
||||
p.pos = p.pos + 1
|
||||
if p.pos < p.size {
|
||||
err = initProducer(p, p.nsqAddr[p.pos])
|
||||
} else {
|
||||
p.pos = 0
|
||||
return fmt.Errorf("连接nsq:%v 失败", p.nsqAddr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Producer) Close() {
|
||||
if p.producer != nil {
|
||||
p.producer.Stop()
|
||||
p.producer = nil
|
||||
}
|
||||
}
|
111
rmq/rmq.go
Normal file
111
rmq/rmq.go
Normal file
@ -0,0 +1,111 @@
|
||||
package rmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rabbitmq/amqp091-go"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ExchangeDirect = "direct"
|
||||
// ExchangeFanout = "fanout"
|
||||
ExchangeTopic = "topic"
|
||||
)
|
||||
|
||||
type Rmq struct {
|
||||
conn *amqp091.Connection
|
||||
ch *amqp091.Channel
|
||||
consumerTag string
|
||||
}
|
||||
|
||||
// url:amqp://guest:guest@localhost:5672/
|
||||
func NewRmq(url string) (*Rmq, error) {
|
||||
rmq := &Rmq{consumerTag: uuid.NewString()}
|
||||
|
||||
retries := 0
|
||||
retryDelay := 1 * time.Second
|
||||
|
||||
var err error
|
||||
for {
|
||||
rmq.conn, err = amqp091.DialConfig(url, amqp091.Config{Heartbeat: 10 * time.Second})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
if retryDelay < 30*time.Second {
|
||||
retryDelay *= 2
|
||||
}
|
||||
log.ErrorF("amqp connection failed after %v reconnect.err:%v url:%v", retryDelay, err, url)
|
||||
}
|
||||
|
||||
rmq.ch, err = rmq.conn.Channel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rmq, nil
|
||||
}
|
||||
|
||||
func (r *Rmq) Close() {
|
||||
if r.conn != nil {
|
||||
_ = r.conn.Close()
|
||||
}
|
||||
if r.ch != nil {
|
||||
_ = r.ch.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Rmq) ExchangeDeclare(exchangeName, typ string) error {
|
||||
return r.ch.ExchangeDeclare(exchangeName, typ, true, false, false, false, nil)
|
||||
}
|
||||
|
||||
func (r *Rmq) QueueDeclare(queueName string) error {
|
||||
_, err := r.ch.QueueDeclare(queueName, false, false, false, false, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Rmq) QueueDelete(queueName string) error {
|
||||
_, err := r.ch.QueueDelete(queueName, false, false, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Rmq) QueueBind(queueName, routerKey, exchangeName string) error {
|
||||
return r.ch.QueueBind(queueName, routerKey, exchangeName, false, nil)
|
||||
}
|
||||
|
||||
func (r *Rmq) QueueUnbind(queueName, routerKey, exchangeName string) error {
|
||||
return r.ch.QueueUnbind(queueName, routerKey, exchangeName, nil)
|
||||
}
|
||||
|
||||
// 发布消息到交换机,带有指定的路由键
|
||||
func (r *Rmq) Publish(exchangeName, routerKey string, msg []byte) error {
|
||||
return r.ch.Publish(exchangeName, routerKey, true, false, amqp091.Publishing{
|
||||
ContentType: "text/plain",
|
||||
Body: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// 发布消息到交换机,带有指定的路由键
|
||||
func (r *Rmq) PublishRpc(d *amqp091.Delivery, msg []byte) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return r.ch.PublishWithContext(ctx, "", d.ReplyTo, false, false, amqp091.Publishing{
|
||||
ContentType: "text/plain",
|
||||
CorrelationId: d.CorrelationId,
|
||||
Body: msg,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Rmq) PublishRaw(exchangeName, routerKey string, data amqp091.Publishing) error {
|
||||
return r.ch.Publish(exchangeName, routerKey, true, false, data)
|
||||
}
|
||||
|
||||
func (r *Rmq) Consume(queueName string) (<-chan amqp091.Delivery, error) {
|
||||
return r.ch.Consume(queueName, r.consumerTag, true, false, false, false, nil)
|
||||
}
|
||||
|
||||
func (r *Rmq) ConsumeDelete() error {
|
||||
return r.ch.Cancel(r.consumerTag, true)
|
||||
}
|
53
rmq/rmq_test.go
Normal file
53
rmq/rmq_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package rmq
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const url = "amqp://samba:samba@testbuild.shoa.com:5672/vh_samba"
|
||||
const exchangeName = "test_e"
|
||||
const queueName = "test_q"
|
||||
|
||||
func testCreateMq(t *testing.T) *Rmq {
|
||||
mq, err := NewRmq(url)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = mq.ExchangeDeclare(exchangeName, ExchangeDirect)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = mq.QueueDeclare(queueName)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return mq
|
||||
}
|
||||
|
||||
func testPublish(t *testing.T, mq *Rmq) {
|
||||
err := mq.Publish(exchangeName, queueName, []byte("hello world"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func testConsume(t *testing.T, mq *Rmq) {
|
||||
msgs, err := mq.Consume(queueName)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for msg := range msgs {
|
||||
t.Log(string(msg.Body))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestRabbitmq(t *testing.T) {
|
||||
mq := testCreateMq(t)
|
||||
defer mq.Close()
|
||||
go testConsume(t, mq)
|
||||
testPublish(t, mq)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
54
safeChan/safeChan.go
Normal file
54
safeChan/safeChan.go
Normal file
@ -0,0 +1,54 @@
|
||||
package safeChan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ByteChan = SafeChan[[]byte]
|
||||
|
||||
type SafeChan[T any] struct {
|
||||
ch chan T
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewSafeChan[T any](size int) *SafeChan[T] {
|
||||
ch := &SafeChan[T]{}
|
||||
ch.ctx, ch.cancel = context.WithCancel(context.Background())
|
||||
if size < 1 {
|
||||
ch.ch = make(chan T)
|
||||
} else {
|
||||
ch.ch = make(chan T, size)
|
||||
}
|
||||
// ch.ch = make(chan T, size)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *SafeChan[T]) Close() {
|
||||
s.once.Do(func() {
|
||||
s.cancel()
|
||||
close(s.ch)
|
||||
})
|
||||
}
|
||||
|
||||
// 管道中剩余数量
|
||||
func (s *SafeChan[T]) Size() int {
|
||||
return len(s.ch)
|
||||
}
|
||||
|
||||
func (s *SafeChan[T]) Reader() <-chan T {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *SafeChan[T]) Write(d T) error {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return fmt.Errorf("chan was closed")
|
||||
default:
|
||||
s.ch <- d
|
||||
return nil
|
||||
}
|
||||
}
|
43
safeChan/safeChan_test.go
Normal file
43
safeChan/safeChan_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package safeChan
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSafeChan(t *testing.T) {
|
||||
ch := NewSafeChan[string](12)
|
||||
|
||||
// go func() {
|
||||
_ = ch.Write("hello")
|
||||
t.Log("write hello. 剩余数量:", ch.Size())
|
||||
// }()
|
||||
ch.Close()
|
||||
if err := ch.Write("zzz"); err != nil {
|
||||
t.Log("write zzz err.", err)
|
||||
}
|
||||
|
||||
breakNum := 0
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-ch.ctx.Done():
|
||||
t.Log("done")
|
||||
breakNum++
|
||||
case v, ok := <-ch.Reader():
|
||||
if ok {
|
||||
t.Log("read", v, " 剩余数量:", ch.Size())
|
||||
} else {
|
||||
t.Log("break")
|
||||
breakNum++
|
||||
}
|
||||
|
||||
default:
|
||||
t.Log("panic")
|
||||
breakNum++
|
||||
}
|
||||
if breakNum > 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
182
service/baseService.go
Normal file
182
service/baseService.go
Normal file
@ -0,0 +1,182 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/fox/fox/ksync"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/fox/fox/safeChan"
|
||||
"github.com/fox/fox/timer"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseService struct {
|
||||
*timer.Timer
|
||||
type_ string
|
||||
name string
|
||||
|
||||
onFunc IOnFunc
|
||||
sender ISender
|
||||
|
||||
msg *safeChan.SafeChan[[]byte]
|
||||
job *safeChan.SafeChan[func()]
|
||||
stop context.Context
|
||||
stopFunc context.CancelFunc
|
||||
waitStop context.Context
|
||||
waitStopFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewBaseService(type_, name string, onFunc IOnFunc, sender ISender) *BaseService {
|
||||
s := &BaseService{
|
||||
type_: type_,
|
||||
name: name,
|
||||
Timer: timer.NewTimer(),
|
||||
onFunc: onFunc,
|
||||
sender: sender,
|
||||
|
||||
msg: safeChan.NewSafeChan[[]byte](128),
|
||||
job: safeChan.NewSafeChan[func()](128),
|
||||
}
|
||||
s.stop, s.stopFunc = context.WithCancel(context.Background())
|
||||
s.waitStop, s.waitStopFunc = context.WithCancel(context.Background())
|
||||
|
||||
s.Run()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *BaseService) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *BaseService) Type() string {
|
||||
return s.type_
|
||||
}
|
||||
|
||||
func (s *BaseService) Write(msg []byte) error {
|
||||
return s.msg.Write(msg)
|
||||
}
|
||||
|
||||
func (s *BaseService) RunOnce(cb func()) {
|
||||
select {
|
||||
case <-s.stop.Done():
|
||||
log.Error(s.Log("want stop, can not call RunOnce function"))
|
||||
return
|
||||
default:
|
||||
_ = s.job.Write(cb)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseService) RunWait(cb func() (retValue any)) (retValue any, err error) {
|
||||
select {
|
||||
case <-s.stop.Done():
|
||||
err = fmt.Errorf(s.Log("want stop, can not call RunOnce function"))
|
||||
log.Error(err.Error())
|
||||
return nil, err
|
||||
default:
|
||||
wait := make(chan any, 2)
|
||||
err = s.job.Write(func() {
|
||||
retValue = cb()
|
||||
wait <- retValue
|
||||
})
|
||||
if err == nil {
|
||||
select {
|
||||
case retValue = <-wait:
|
||||
return retValue, nil
|
||||
case <-time.After(time.Second * time.Duration(30)):
|
||||
return nil, fmt.Errorf("timeout fail")
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseService) Send(topic string, msg []byte) error {
|
||||
if s.sender != nil {
|
||||
return s.sender.Send(topic, msg)
|
||||
}
|
||||
return s.Err("send is nil")
|
||||
}
|
||||
func (s *BaseService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
|
||||
if s.sender != nil {
|
||||
return s.sender.Call(topic, timeout, msg)
|
||||
}
|
||||
return nil, s.Err("call is nil")
|
||||
}
|
||||
|
||||
func (s *BaseService) WaitStop() {
|
||||
select {
|
||||
case <-s.waitStop.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseService) NotifyStop() {
|
||||
s.stopFunc()
|
||||
// log.Debug(fmt.Sprintf("notify %v service stop", s.name))
|
||||
}
|
||||
|
||||
func (s *BaseService) allChanEmpty() bool {
|
||||
if s.job.Size() == 0 && s.msg.Size() == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *BaseService) canStop() bool {
|
||||
select {
|
||||
case <-s.stop.Done():
|
||||
if s.allChanEmpty() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseService) run() {
|
||||
for {
|
||||
if s.canStop() {
|
||||
if s.onFunc != nil {
|
||||
s.onFunc.OnStop()
|
||||
s.waitStopFunc()
|
||||
}
|
||||
break
|
||||
}
|
||||
select {
|
||||
case msg, ok := <-s.msg.Reader():
|
||||
if ok && s.onFunc != nil {
|
||||
_ = s.onFunc.OnMessage(msg)
|
||||
}
|
||||
case cb, ok := <-s.job.Reader():
|
||||
if ok && cb != nil {
|
||||
cb()
|
||||
}
|
||||
case t, ok := <-s.Timer.Reader():
|
||||
if ok && t != nil && t.Func != nil {
|
||||
t.Func()
|
||||
}
|
||||
case _ = <-s.stop.Done():
|
||||
if s.onFunc != nil {
|
||||
s.msg.Close()
|
||||
s.job.Close()
|
||||
s.Timer.CancelAllTimer()
|
||||
s.Timer.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseService) Run() {
|
||||
ksync.GoSafe(s.run, s.Run)
|
||||
}
|
||||
|
||||
func (s *BaseService) Log(format string, a ...any) string {
|
||||
head := fmt.Sprintf("service:%v-%v ", s.type_, s.name)
|
||||
return head + fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
func (s *BaseService) Err(format string, a ...any) error {
|
||||
head := fmt.Sprintf("service:%v-%v ", s.type_, s.name)
|
||||
return fmt.Errorf(head + fmt.Sprintf(format, a...))
|
||||
}
|
35
service/iservice.go
Normal file
35
service/iservice.go
Normal file
@ -0,0 +1,35 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type IService interface {
|
||||
Name() string
|
||||
Type() string
|
||||
RunOnce(cb func())
|
||||
RunWait(cb func() (retValue any)) (retValue any, err error)
|
||||
|
||||
NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32
|
||||
CancelTimer(timerId uint32)
|
||||
CancelAllTimer()
|
||||
|
||||
// 向服务内部消息管道写入消息
|
||||
Write(msg []byte) error
|
||||
Send(topic string, msg []byte) error
|
||||
Call(topic string, timeout time.Duration, msg []byte) ([]byte, error)
|
||||
|
||||
WaitStop()
|
||||
NotifyStop()
|
||||
}
|
||||
|
||||
type ISender interface {
|
||||
Send(topic string, msg []byte) error
|
||||
Call(topic string, timeout time.Duration, msg []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type IOnFunc interface {
|
||||
OnStop()
|
||||
OnInit()
|
||||
OnMessage(msg []byte) error
|
||||
}
|
48
service/natsService.go
Normal file
48
service/natsService.go
Normal file
@ -0,0 +1,48 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/nat"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NatsService struct {
|
||||
*BaseService
|
||||
nats *nat.Nats
|
||||
}
|
||||
|
||||
func NewNatsService(type_, name string, onFunc IOnFunc, natsAddress ...string) (*NatsService, error) {
|
||||
s := new(NatsService)
|
||||
s.BaseService = NewBaseService(type_, name, onFunc, s)
|
||||
s.nats = nat.NewNats(fmt.Sprintf("%v-%v", type_, name), natsAddress...)
|
||||
if err := s.nats.Connect(); err != nil {
|
||||
// log.Error(err.Error())
|
||||
s.BaseService.NotifyStop()
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (n *NatsService) Subscribe(topic string) error {
|
||||
return n.nats.Subscribe(topic, n.msg)
|
||||
}
|
||||
|
||||
// 队列订阅,队列中只会有一个消费者消费该消息
|
||||
func (n *NatsService) QueueSubscribe(topic string, queue string) error {
|
||||
return n.nats.QueueSubscribe(topic, queue, n.msg)
|
||||
}
|
||||
|
||||
func (s *NatsService) OnStop() {
|
||||
s.nats.Close()
|
||||
}
|
||||
|
||||
func (s *NatsService) Send(topic string, msg []byte) error {
|
||||
return s.nats.Publish(topic, msg)
|
||||
}
|
||||
|
||||
func (s *NatsService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
|
||||
_ = topic
|
||||
_ = timeout
|
||||
_ = msg
|
||||
return nil, nil
|
||||
}
|
97
service/natsService_test.go
Normal file
97
service/natsService_test.go
Normal file
@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/etcd"
|
||||
"github.com/fox/fox/log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
GameSrv = "game"
|
||||
NatsAddress = "nats://192.168.232.128:4222"
|
||||
EtcdAddress = "192.168.232.128:2379"
|
||||
EtcdAddress2 = "114.132.124.145:2379"
|
||||
NatsAddress2 = "nats://114.132.124.145:4222"
|
||||
)
|
||||
|
||||
func newGameService() *gameService {
|
||||
_ = NatsAddress2
|
||||
_ = NatsAddress
|
||||
_ = EtcdAddress2
|
||||
_ = EtcdAddress
|
||||
|
||||
var err error
|
||||
s := new(gameService)
|
||||
if s.NatsService, err = NewNatsService(GameSrv, "1", s, NatsAddress2); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return nil
|
||||
}
|
||||
if s.etcdService, err = etcd.NewRegistry[etcd.ServiceNode]([]string{EtcdAddress2}, etcd.ServiceNode{}.EtcdRootKey(), "", ""); err != nil {
|
||||
log.Error(err.Error())
|
||||
s.NatsService.OnStop()
|
||||
return nil
|
||||
}
|
||||
endpoint := &etcd.ServiceNode{
|
||||
Name: s.Name(),
|
||||
Type: s.Type(),
|
||||
Address: "",
|
||||
Port: 0,
|
||||
Version: "",
|
||||
ServiceType: etcd.Unique,
|
||||
}
|
||||
if err = s.etcdService.Register(endpoint); err != nil {
|
||||
log.Error(err.Error())
|
||||
s.NatsService.OnStop()
|
||||
return nil
|
||||
}
|
||||
s.OnInit()
|
||||
return s
|
||||
}
|
||||
|
||||
type gameService struct {
|
||||
*NatsService
|
||||
etcdService *etcd.Registry[etcd.ServiceNode]
|
||||
etcdTopic *etcd.Registry[etcd.TopicNode]
|
||||
srvTopic string
|
||||
}
|
||||
|
||||
func (s *gameService) OnInit() {
|
||||
s.etcdService.WatchServices()
|
||||
s.etcdTopic.WatchServices()
|
||||
if err := s.NatsService.Subscribe(Topic(s)); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
if err := s.NatsService.QueueSubscribe(GroupTopic(s), GroupQueue(s)); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
log.Debug("onInit")
|
||||
}
|
||||
|
||||
func (s *gameService) OnStop() {
|
||||
s.etcdService.UnregisterService()
|
||||
s.etcdService.UnregisterService()
|
||||
s.NatsService.OnStop()
|
||||
log.Debug("OnStop")
|
||||
}
|
||||
|
||||
func (s *gameService) OnMessage(msg []byte) error {
|
||||
log.Debug(s.Log("on message:%v", string(msg)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGameService(t *testing.T) {
|
||||
log.Open("test.log", log.DebugL)
|
||||
s := newGameService()
|
||||
msg := "hello world"
|
||||
if err := s.Send(Topic(s), []byte(msg)); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
for _, srv := range s.etcdService.GetNodes() {
|
||||
log.Debug(s.Log("发现有服务:%v", srv))
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
s.NotifyStop()
|
||||
s.WaitStop()
|
||||
log.Debug("exit")
|
||||
}
|
61
service/service_test.go
Normal file
61
service/service_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
TestSrv = "test"
|
||||
)
|
||||
|
||||
type EchoService struct {
|
||||
*BaseService
|
||||
}
|
||||
|
||||
func (s *EchoService) OnInit() {
|
||||
log.Debug("onInit")
|
||||
}
|
||||
|
||||
func (s *EchoService) OnStop() {
|
||||
log.Debug("OnStop")
|
||||
}
|
||||
|
||||
func (s *EchoService) Send(topic string, msg []byte) error {
|
||||
log.Debug(s.Log("send %v to topic:%v", string(msg), topic))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EchoService) Call(topic string, timeout time.Duration, msg []byte) ([]byte, error) {
|
||||
_ = timeout
|
||||
_ = msg
|
||||
log.Debug(s.Log("call topic:%v", topic))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *EchoService) OnMessage(msg []byte) error {
|
||||
log.Debug(s.Log("on message:%v", string(msg)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTestService() *EchoService {
|
||||
s := new(EchoService)
|
||||
s.BaseService = NewBaseService(TestSrv, "1", s, s)
|
||||
s.OnInit()
|
||||
return s
|
||||
}
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
log.Open("test.log", log.DebugL)
|
||||
s := NewTestService()
|
||||
msg := "hello world"
|
||||
_ = s.Write([]byte(msg))
|
||||
s.RunOnce(func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
})
|
||||
|
||||
s.NotifyStop()
|
||||
s.WaitStop()
|
||||
log.Debug("exit")
|
||||
}
|
18
service/topic.go
Normal file
18
service/topic.go
Normal file
@ -0,0 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 每个服务都有自己的服务topic
|
||||
func Topic(s IService) string {
|
||||
return fmt.Sprintf("%v-%v.topic", s.Type(), s.Name())
|
||||
}
|
||||
|
||||
func GroupTopic(s IService) string {
|
||||
return s.Type() + ".topic"
|
||||
}
|
||||
|
||||
func GroupQueue(s IService) string {
|
||||
return s.Type() + ".group"
|
||||
}
|
108
timer/timer.go
Normal file
108
timer/timer.go
Normal file
@ -0,0 +1,108 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/fox/fox/safeChan"
|
||||
cmap "github.com/orcaman/concurrent-map/v2"
|
||||
"math"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type timerData struct {
|
||||
timer *time.Timer
|
||||
id uint32
|
||||
Func func()
|
||||
desc string
|
||||
needLog bool
|
||||
}
|
||||
|
||||
type Timer struct {
|
||||
chTimer *safeChan.SafeChan[*timerData]
|
||||
timers cmap.ConcurrentMap[uint32, *timerData]
|
||||
no uint32
|
||||
stop int32
|
||||
}
|
||||
|
||||
func NewTimer() *Timer {
|
||||
rt := &Timer{}
|
||||
rt.chTimer = safeChan.NewSafeChan[*timerData](16)
|
||||
rt.timers = cmap.NewWithCustomShardingFunction[uint32, *timerData](func(key uint32) uint32 { return key })
|
||||
rt.no = math.MaxUint32 - 2
|
||||
rt.stop = 0
|
||||
return rt
|
||||
}
|
||||
|
||||
func (rt *Timer) NewTimer(duration time.Duration, cb func(), needLog bool, desc ...string) uint32 {
|
||||
tData := &timerData{
|
||||
timer: nil,
|
||||
id: atomic.AddUint32(&rt.no, 1),
|
||||
Func: cb,
|
||||
needLog: needLog,
|
||||
}
|
||||
if len(desc) > 0 {
|
||||
tData.desc = desc[0]
|
||||
}
|
||||
t := time.AfterFunc(duration, func() {
|
||||
_ = rt.chTimer.Write(tData)
|
||||
rt.timers.Remove(tData.id)
|
||||
if needLog {
|
||||
if tData.desc != "" {
|
||||
log.DebugF("移除定时器:%v NewTimer desc:%v time:%v", tData.id, tData.desc, duration)
|
||||
} else {
|
||||
log.DebugF("移除定时器:%v NewTimer", tData.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
tData.timer = t
|
||||
rt.timers.Set(tData.id, tData)
|
||||
if needLog {
|
||||
if tData.desc != "" {
|
||||
log.DebugF("设置定时器:%v NewTimer desc:%v", tData.id, tData.desc)
|
||||
} else {
|
||||
log.DebugF("设置定时器:%v NewTimer", tData.id)
|
||||
}
|
||||
}
|
||||
|
||||
return tData.id
|
||||
}
|
||||
|
||||
func (rt *Timer) CancelTimer(timerId uint32) {
|
||||
if t, _ := rt.timers.Get(timerId); t != nil {
|
||||
if t.timer != nil {
|
||||
_ = t.timer.Stop()
|
||||
}
|
||||
rt.timers.Remove(timerId)
|
||||
if t.needLog {
|
||||
if t.desc != "" {
|
||||
log.DebugF("移除定时器:%v CancelTimer desc:%v", t.id, t.desc)
|
||||
} else {
|
||||
log.DebugF("移除定时器:%v CancelTimer", t.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *Timer) CancelAllTimer() {
|
||||
rt.timers.IterCb(func(_ uint32, t *timerData) {
|
||||
if t.timer != nil {
|
||||
t.timer.Stop()
|
||||
if t.needLog {
|
||||
if t.desc != "" {
|
||||
log.DebugF("移除定时器:%v CancelAllTimer desc:%v", t.id, t.desc)
|
||||
} else {
|
||||
log.DebugF("移除定时器:%v CancelAllTimer", t.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
rt.timers.Clear()
|
||||
}
|
||||
|
||||
func (rt *Timer) Reader() <-chan *timerData {
|
||||
return rt.chTimer.Reader()
|
||||
}
|
||||
|
||||
func (rt *Timer) Close() {
|
||||
rt.chTimer.Close()
|
||||
}
|
51
timer/timer_test.go
Normal file
51
timer/timer_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package timer
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/ksync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func runTimerEvent(tm *Timer) {
|
||||
ksync.GoSafe(func() {
|
||||
for {
|
||||
select {
|
||||
case t, ok := <-tm.chTimer.Reader():
|
||||
if ok && t != nil && t.Func != nil {
|
||||
t.Func()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestTimer(t *testing.T) {
|
||||
timer := NewTimer()
|
||||
runTimerEvent(timer)
|
||||
id := timer.NewTimer(1*time.Second, func() {
|
||||
t.Log("this is timer1")
|
||||
}, false)
|
||||
t.Log("new timer:", id)
|
||||
|
||||
id = timer.NewTimer(2*time.Second, func() {
|
||||
t.Log("this is timer2")
|
||||
}, false)
|
||||
t.Log("new timer:", id)
|
||||
|
||||
id = timer.NewTimer(3*time.Second, func() {
|
||||
t.Log("this is timer3")
|
||||
}, false)
|
||||
t.Log("new timer:", id)
|
||||
|
||||
id = timer.NewTimer(1*time.Second, func() {
|
||||
t.Log("this is timer4")
|
||||
}, false)
|
||||
t.Log("new timer:", id)
|
||||
|
||||
timer.CancelTimer(id)
|
||||
t.Log("cancel timer:", id)
|
||||
|
||||
// fmt.Println("rrr id:", id)
|
||||
time.Sleep(5 * time.Second)
|
||||
t.Log("rest timer:", timer.timers.Count(), " count")
|
||||
}
|
10
ws/iconn.go
Normal file
10
ws/iconn.go
Normal file
@ -0,0 +1,10 @@
|
||||
package ws
|
||||
|
||||
type IConn interface {
|
||||
Close()
|
||||
SendMsg(data []byte) error
|
||||
Name() string
|
||||
Id() uint32
|
||||
UserId() int64
|
||||
Log(format string, v ...interface{}) string
|
||||
}
|
41
ws/userMgr.go
Normal file
41
ws/userMgr.go
Normal file
@ -0,0 +1,41 @@
|
||||
package ws
|
||||
|
||||
import cmap "github.com/orcaman/concurrent-map/v2"
|
||||
|
||||
var userMgr = newUserManager()
|
||||
|
||||
type userManager struct {
|
||||
users cmap.ConcurrentMap[int64, uint32]
|
||||
}
|
||||
|
||||
func newUserManager() *userManager {
|
||||
return &userManager{
|
||||
users: cmap.NewWithCustomShardingFunction[int64, uint32](func(key int64) uint32 {
|
||||
return uint32(key)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *userManager) Add(connId uint32, userId int64) bool {
|
||||
if userId < 1 {
|
||||
return false
|
||||
}
|
||||
if conn, ok := wsMgr.Get(connId); ok {
|
||||
conn.setUserId(userId)
|
||||
m.users.Set(userId, connId)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *userManager) GetConnId(userId int64) uint32 {
|
||||
connId, _ := m.users.Get(userId)
|
||||
return connId
|
||||
}
|
||||
|
||||
func (m *userManager) Remove(userId int64) {
|
||||
if userId < 1 {
|
||||
return
|
||||
}
|
||||
m.users.Remove(userId)
|
||||
}
|
125
ws/wsClient.go
Normal file
125
ws/wsClient.go
Normal file
@ -0,0 +1,125 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/fox/fox/ksync"
|
||||
"github.com/fox/fox/log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
sendChan chan *wsMessage
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewClient(url string) (*Client, error) {
|
||||
dialer := websocket.DefaultDialer
|
||||
dialer.HandshakeTimeout = 30 * time.Second
|
||||
conn, _, err := dialer.Dial(url, http.Header{"User-Agent": {"MyClient/1.0"}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
sendChan: make(chan *wsMessage, 100),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Start() {
|
||||
c.wg.Add(3)
|
||||
ksync.GoSafe(c.readLoop, nil)
|
||||
ksync.GoSafe(c.writeLoop, nil)
|
||||
ksync.GoSafe(c.heartbeatLoop, nil)
|
||||
}
|
||||
|
||||
func (c *Client) readLoop() {
|
||||
defer c.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
messageType, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
// log.Error(fmt.Sprintf("读取错误:%v", err))
|
||||
c.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
switch messageType {
|
||||
case websocket.PingMessage:
|
||||
c.sendChan <- &wsMessage{messageType: websocket.PongMessage, data: []byte("pong")}
|
||||
case websocket.PongMessage:
|
||||
|
||||
case websocket.TextMessage, websocket.BinaryMessage:
|
||||
log.DebugF("收到消息,类型:%v 内容:%v", messageType, string(message))
|
||||
case websocket.CloseMessage:
|
||||
log.Debug("收到关闭帧")
|
||||
c.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SendMsg(data []byte) {
|
||||
c.sendChan <- &wsMessage{messageType: websocket.BinaryMessage, data: data}
|
||||
}
|
||||
|
||||
func (c *Client) writeLoop() {
|
||||
defer c.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case msg := <-c.sendChan:
|
||||
switch msg.messageType {
|
||||
case websocket.PingMessage:
|
||||
_ = c.conn.WriteMessage(websocket.PingMessage, []byte("ping"))
|
||||
case websocket.PongMessage:
|
||||
_ = c.conn.WriteMessage(websocket.PongMessage, []byte("pong"))
|
||||
default:
|
||||
_ = c.conn.WriteMessage(msg.messageType, msg.data)
|
||||
}
|
||||
case <-c.ctx.Done():
|
||||
// 发送关闭帧
|
||||
_ = c.conn.WriteControl(
|
||||
websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
|
||||
time.Now().Add(10*time.Second),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) heartbeatLoop() {
|
||||
defer c.wg.Done()
|
||||
ticker := time.NewTicker(25 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.sendChan <- &wsMessage{messageType: websocket.PingMessage, data: []byte("ping")}
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Stop() {
|
||||
c.cancel()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
}
|
194
ws/wsConn.go
Normal file
194
ws/wsConn.go
Normal file
@ -0,0 +1,194 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/gorilla/websocket"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 连接id
|
||||
var (
|
||||
wsMsgType = websocket.BinaryMessage
|
||||
nextConnId uint32
|
||||
)
|
||||
|
||||
// 客户端读写消息
|
||||
type wsMessage struct {
|
||||
messageType int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// 客户端连接
|
||||
type wsConnect struct {
|
||||
wsConn *websocket.Conn // 底层websocket
|
||||
inChan chan *wsMessage // 读队列
|
||||
outChan chan *wsMessage // 写队列
|
||||
mutex sync.Mutex // 避免重复关闭管道,加锁处理
|
||||
isClosed bool
|
||||
closeCh chan struct{} // 关闭通知
|
||||
id uint32
|
||||
userId int64
|
||||
onDisconnect func(IConn)
|
||||
}
|
||||
|
||||
func newWsConnect(wsConn *websocket.Conn, onDisconnect func(IConn)) *wsConnect {
|
||||
return &wsConnect{
|
||||
wsConn: wsConn,
|
||||
inChan: make(chan *wsMessage, 1000),
|
||||
outChan: make(chan *wsMessage, 1000),
|
||||
closeCh: make(chan struct{}),
|
||||
isClosed: false,
|
||||
id: nextConnId,
|
||||
userId: 0,
|
||||
onDisconnect: onDisconnect,
|
||||
}
|
||||
}
|
||||
|
||||
// 从读队列读取消息
|
||||
func (c *wsConnect) readFromChan() (*wsMessage, error) {
|
||||
select {
|
||||
case msg := <-c.inChan:
|
||||
return msg, nil
|
||||
case <-c.closeCh:
|
||||
return nil, fmt.Errorf("连接已关闭")
|
||||
}
|
||||
}
|
||||
|
||||
// 把消息放进写队列
|
||||
func (c *wsConnect) sendMsg(msgType int, data []byte) error {
|
||||
select {
|
||||
case c.outChan <- &wsMessage{messageType: msgType, data: data}:
|
||||
case <-c.closeCh:
|
||||
return fmt.Errorf("连接已关闭")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 把消息放进写队列
|
||||
func (c *wsConnect) SendMsg(data []byte) error {
|
||||
return c.sendMsg(wsMsgType, data)
|
||||
}
|
||||
|
||||
// 关闭链接
|
||||
func (c *wsConnect) Close() {
|
||||
log.Debug(c.Log("关闭链接"))
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if c.isClosed == false {
|
||||
c.isClosed = true
|
||||
wsMgr.Remove(c)
|
||||
close(c.closeCh)
|
||||
if c.onDisconnect != nil {
|
||||
c.onDisconnect(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 循环从websocket中读取消息放入到读队列中
|
||||
func (c *wsConnect) readWsLoop() {
|
||||
c.wsConn.SetReadLimit(maxMessageSize)
|
||||
_ = c.wsConn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
for {
|
||||
// 读一个message
|
||||
msgType, data, err := c.wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) {
|
||||
log.Error(c.Log("消息读取出现错误:%v", err))
|
||||
}
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
switch msgType {
|
||||
case websocket.PingMessage:
|
||||
_ = c.sendMsg(websocket.PongMessage, []byte("pong"))
|
||||
case websocket.PongMessage:
|
||||
// _ = c.sendMsg(websocket.PingMessage, []byte("ping"))
|
||||
case websocket.CloseMessage:
|
||||
code := websocket.CloseNormalClosure
|
||||
reason := ""
|
||||
if len(data) >= 2 {
|
||||
code = int(binary.BigEndian.Uint16(data))
|
||||
reason = string(data[2:])
|
||||
}
|
||||
log.Debug(c.Log("关闭原因码:%d 描述:%s", code, reason))
|
||||
// 发送响应关闭帧(必须回传相同状态码)
|
||||
rspMsg := websocket.FormatCloseMessage(code, reason)
|
||||
_ = c.wsConn.WriteControl(websocket.CloseMessage, rspMsg, time.Now().Add(5*time.Second))
|
||||
c.Close()
|
||||
default:
|
||||
if msgType != wsMsgType {
|
||||
continue
|
||||
}
|
||||
msg := &wsMessage{messageType: msgType, data: data}
|
||||
select {
|
||||
case c.inChan <- msg:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wsConnect) writeWsLoop() {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
// 取一个消息发送给客户端
|
||||
case msg := <-c.outChan:
|
||||
if err := c.wsConn.WriteMessage(msg.messageType, msg.data); err != nil {
|
||||
log.Error(c.Log("发送消息错误:%v", err))
|
||||
// 关闭连接
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
case <-c.closeCh:
|
||||
// 收到关闭通知
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = c.wsConn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := c.wsConn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wsConnect) handle(process func(IConn, []byte)) {
|
||||
for {
|
||||
msg, err := c.readFromChan()
|
||||
if err != nil {
|
||||
// log.Error(c.Log("获取消息错误:%v", err))
|
||||
break
|
||||
}
|
||||
// Log.Debug(c.Log("接收消息:%v", msg.data))
|
||||
process(c, msg.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户id
|
||||
func (c *wsConnect) setUserId(uid int64) {
|
||||
c.userId = uid
|
||||
}
|
||||
|
||||
// 获取连接id
|
||||
func (c *wsConnect) Id() uint32 {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// 获取用户id
|
||||
func (c *wsConnect) UserId() int64 {
|
||||
return c.userId
|
||||
}
|
||||
|
||||
func (c *wsConnect) Name() string {
|
||||
return fmt.Sprintf("用户:%v, 地址:%v", c.userId, c.wsConn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (c *wsConnect) Log(format string, v ...interface{}) string {
|
||||
s := fmt.Sprintf("连接:%v, id:%v ", c.wsConn.RemoteAddr().String(), c.id)
|
||||
return s + fmt.Sprintf(format, v...)
|
||||
}
|
45
ws/wsMgr.go
Normal file
45
ws/wsMgr.go
Normal file
@ -0,0 +1,45 @@
|
||||
package ws
|
||||
|
||||
import cmap "github.com/orcaman/concurrent-map/v2"
|
||||
|
||||
var wsMgr = newManager()
|
||||
|
||||
type wsManager struct {
|
||||
wsConnAll cmap.ConcurrentMap[uint32, *wsConnect]
|
||||
}
|
||||
|
||||
func newManager() *wsManager {
|
||||
return &wsManager{
|
||||
wsConnAll: cmap.NewWithCustomShardingFunction[uint32, *wsConnect](func(key uint32) uint32 {
|
||||
return key
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *wsManager) Add(conn *wsConnect) {
|
||||
m.wsConnAll.Set(conn.id, conn)
|
||||
}
|
||||
|
||||
func (m *wsManager) SetUserId(connId uint32, userId int64) {
|
||||
userMgr.Add(connId, userId)
|
||||
}
|
||||
|
||||
func (m *wsManager) Remove(conn *wsConnect) {
|
||||
if conn.UserId() > 0 {
|
||||
userMgr.Remove(conn.UserId())
|
||||
}
|
||||
m.wsConnAll.Remove(conn.id)
|
||||
}
|
||||
|
||||
func (m *wsManager) Get(connId uint32) (*wsConnect, bool) {
|
||||
return m.wsConnAll.Get(connId)
|
||||
}
|
||||
|
||||
func (m *wsManager) FindByUserId(userId int64) (*wsConnect, bool) {
|
||||
connId := userMgr.GetConnId(userId)
|
||||
return m.wsConnAll.Get(connId)
|
||||
}
|
||||
|
||||
func (m *wsManager) Count() int {
|
||||
return m.wsConnAll.Count()
|
||||
}
|
81
ws/wsServer.go
Normal file
81
ws/wsServer.go
Normal file
@ -0,0 +1,81 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/fox/fox/ksync"
|
||||
"github.com/fox/fox/log"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// 允许等待的写入时间
|
||||
writeWait = 10 * time.Second
|
||||
// pong间隔时间
|
||||
pongWait = 60 * time.Second
|
||||
// ping间隔时间
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
// 最大消息长度
|
||||
maxMessageSize = 512
|
||||
)
|
||||
|
||||
var upGrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 允许所有跨域请求(生产环境应限制)
|
||||
// 跨域限制
|
||||
// allowedOrigins := map[string]bool{
|
||||
// "https://yourdomain.com": true,
|
||||
// "https://api.yourdomain.com": true,
|
||||
// }
|
||||
// return allowedOrigins[r.Header.Get("Origin")]
|
||||
},
|
||||
HandshakeTimeout: 10 * time.Second, // 握手超时
|
||||
ReadBufferSize: 4096, // 读缓冲区
|
||||
WriteBufferSize: 4096, // 写缓冲区
|
||||
// MaxMessageSize: 1024 * 1024 * 2, // 最大消息2MB
|
||||
}
|
||||
|
||||
type WsServer struct {
|
||||
addr string // 0.0.0.0:8888
|
||||
onMessage func(IConn, []byte)
|
||||
onDisconnect func(IConn)
|
||||
}
|
||||
|
||||
func NewWsServer(addr string, onMessage func(IConn, []byte), onDisconnect func(IConn)) *WsServer {
|
||||
return &WsServer{addr: addr, onMessage: onMessage, onDisconnect: onDisconnect}
|
||||
}
|
||||
|
||||
func (s *WsServer) wsHandle(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upGrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.ErrorF("升级到WebSocket失败:%v", err)
|
||||
return
|
||||
}
|
||||
// defer func() { _ = conn.Close() }()
|
||||
nextConnId++
|
||||
wsConn := newWsConnect(conn, s.onDisconnect)
|
||||
wsMgr.Add(wsConn)
|
||||
log.DebugF("当前在线人数:%v", wsMgr.Count())
|
||||
ksync.GoSafe(func() { wsConn.handle(s.onMessage) }, nil)
|
||||
ksync.GoSafe(wsConn.readWsLoop, nil)
|
||||
ksync.GoSafe(wsConn.writeWsLoop, nil)
|
||||
}
|
||||
|
||||
func (s *WsServer) Run() {
|
||||
http.HandleFunc("/", s.wsHandle)
|
||||
log.DebugF("websocket server listening on :%v", s.addr)
|
||||
ksync.GoSafe(func() {
|
||||
err := http.ListenAndServe(s.addr, nil)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (s *WsServer) SetUserId(connId uint32, userId int64) {
|
||||
wsMgr.SetUserId(connId, userId)
|
||||
}
|
||||
|
||||
func (s *WsServer) FindConnByUserId(userId int64) (IConn, bool) {
|
||||
return wsMgr.FindByUserId(userId)
|
||||
}
|
66
ws/ws_test.go
Normal file
66
ws/ws_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fox/fox/log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
serverAddr = ":8080"
|
||||
addr = "localhost:8080"
|
||||
)
|
||||
|
||||
func initLog() {
|
||||
log.Open("test.Log", log.DebugL)
|
||||
}
|
||||
|
||||
func wsServer() {
|
||||
s := NewWsServer(serverAddr, func(conn IConn, data []byte) {
|
||||
log.DebugF("服务端收到消息:%v", string(data))
|
||||
_ = conn.SendMsg(data)
|
||||
}, func(conn IConn) {
|
||||
log.Debug(conn.Log("退出"))
|
||||
})
|
||||
s.Run()
|
||||
}
|
||||
|
||||
func wsClient() {
|
||||
client, err := NewClient(fmt.Sprintf("ws://%v", addr))
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
defer client.Stop()
|
||||
|
||||
count := 1
|
||||
client.Start()
|
||||
msg := fmt.Sprintf("hell world %v", count)
|
||||
// Log.Debug(fmt.Sprintf("%v", []byte(msg)))
|
||||
client.SendMsg([]byte(msg))
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
count++
|
||||
client.SendMsg([]byte(fmt.Sprintf("hell world %v", count)))
|
||||
if count > 30 {
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
initLog()
|
||||
wsServer()
|
||||
wsClient()
|
||||
|
||||
// time.Sleep(60 * time.Second)
|
||||
log.Debug("shutdown")
|
||||
}
|
67
xrand/rand.go
Normal file
67
xrand/rand.go
Normal file
@ -0,0 +1,67 @@
|
||||
package xrand
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
var Int = rand.Int
|
||||
|
||||
func Intn(n int) int {
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
||||
func Int31() int32 {
|
||||
return rand.Int31()
|
||||
}
|
||||
|
||||
func Int31n(n int32) int32 {
|
||||
return rand.Int31n(n)
|
||||
}
|
||||
|
||||
func Int63() int64 {
|
||||
return rand.Int63()
|
||||
}
|
||||
|
||||
func Int63n(n int64) int64 {
|
||||
return rand.Int63n(n)
|
||||
}
|
||||
|
||||
func Uint32() uint32 {
|
||||
return rand.Uint32()
|
||||
}
|
||||
|
||||
func Uint64() uint64 {
|
||||
return rand.Uint64()
|
||||
}
|
||||
|
||||
func Float32() float32 {
|
||||
return rand.Float32()
|
||||
}
|
||||
|
||||
func Float64() float64 {
|
||||
return rand.Float64()
|
||||
}
|
||||
|
||||
func Perm(n int) []int {
|
||||
return rand.Perm(n)
|
||||
}
|
||||
|
||||
func Read(p []byte) (n int, err error) {
|
||||
return rand.Read(p)
|
||||
}
|
||||
|
||||
func Shuffle[T any](slice []T) {
|
||||
rand.Shuffle(len(slice), func(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
})
|
||||
}
|
||||
|
||||
// RandomInt64 随机生成一个[min, max]之间的整数
|
||||
func RandomInt64(min, max int64) int64 {
|
||||
return Int63n(max-min) + min
|
||||
}
|
9
xrand/rand_test.go
Normal file
9
xrand/rand_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package xrand
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
a := []int{1, 2, 3}
|
||||
Shuffle(a)
|
||||
t.Log(a)
|
||||
}
|
42
xrand/weight.go
Normal file
42
xrand/weight.go
Normal file
@ -0,0 +1,42 @@
|
||||
package xrand
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrWeightRandomBadParam = errors.New("WeightRandom: bad param")
|
||||
|
||||
type randomItem[T any] struct {
|
||||
Weight uint
|
||||
Data T
|
||||
}
|
||||
|
||||
// WeightRandom 加权随机
|
||||
func WeightRandom[W int | uint | int32 | uint32 | int64 | uint64, T any](m map[W]T) (result T, err error) {
|
||||
sum := uint(0)
|
||||
var items []randomItem[T]
|
||||
for weight, data := range m {
|
||||
sum += uint(weight)
|
||||
items = append(items, randomItem[T]{
|
||||
Weight: uint(weight),
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
if len(items) == 0 || sum == 0 {
|
||||
err = ErrWeightRandomBadParam
|
||||
return
|
||||
}
|
||||
if len(items) == 1 {
|
||||
return items[0].Data, nil
|
||||
}
|
||||
|
||||
r := Intn(int(sum))
|
||||
for _, item := range items {
|
||||
r -= int(item.Weight)
|
||||
if r < 0 {
|
||||
return item.Data, nil
|
||||
}
|
||||
}
|
||||
|
||||
err = ErrWeightRandomBadParam
|
||||
return
|
||||
}
|
25
xrand/weight_test.go
Normal file
25
xrand/weight_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package xrand
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWeightRandom(t *testing.T) {
|
||||
stat := make(map[any]int)
|
||||
for i := 1; i <= 10000; i++ {
|
||||
items := map[uint]string{
|
||||
5: "a",
|
||||
15: "b",
|
||||
30: "c",
|
||||
50: "d",
|
||||
}
|
||||
item, err := WeightRandom(items)
|
||||
assert.Nil(t, err)
|
||||
if _, ok := stat[item]; !ok {
|
||||
stat[item] = 0
|
||||
}
|
||||
stat[item]++
|
||||
}
|
||||
t.Log(stat)
|
||||
}
|
42
xtime/constant.go
Normal file
42
xtime/constant.go
Normal file
@ -0,0 +1,42 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimezoneOffset = int
|
||||
|
||||
// 系统时区偏移量,也就是默认时区偏移量
|
||||
var sysTimezoneOffset = SaoPauloTimezoneOffset
|
||||
|
||||
var nowOffset int
|
||||
|
||||
const (
|
||||
// SaoPauloTimezoneOffset 南美/圣保罗 西三区时间,UTC-3
|
||||
SaoPauloTimezoneOffset TimezoneOffset = -3 * 3600
|
||||
// ShangHaiTimezoneOffset 亚洲/上海 东八区时间,UTC+3
|
||||
ShangHaiTimezoneOffset TimezoneOffset = 8 * 3600
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// 每周起始日
|
||||
WeekStartsAt = carbon.Monday
|
||||
|
||||
// 时间间隔
|
||||
NoDuration = time.Duration(0)
|
||||
Day = time.Hour * 24
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
)
|
||||
|
||||
// SetSysTimezoneOffset 设置系统时区偏移量,影响所有默认行为
|
||||
func SetSysTimezoneOffset(offset int) {
|
||||
sysTimezoneOffset = offset
|
||||
}
|
||||
|
||||
// SetNowTimeOffset 设置当前时间偏移,影响Now的结果
|
||||
func SetNowTimeOffset(offset int) {
|
||||
nowOffset = offset
|
||||
}
|
83
xtime/duration.go
Normal file
83
xtime/duration.go
Normal file
@ -0,0 +1,83 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 刷新点:时间边界
|
||||
|
||||
func GetDuration(t time.Time) (d time.Duration) {
|
||||
return t.Sub(time.Now())
|
||||
}
|
||||
|
||||
// 现在到明日刷新点的剩余时间间隔
|
||||
func GetDurationToNextHour() (d time.Duration) {
|
||||
return GetDurationToNextNHour(1)
|
||||
}
|
||||
|
||||
// 现在到第N天刷新点的剩余时间间隔 (n为1是明天)
|
||||
func GetDurationToNextNHour(n int) (d time.Duration) {
|
||||
now := Now().StdTime()
|
||||
next := Now().AddHours(n).StartOfHour().StdTime()
|
||||
return next.Sub(now)
|
||||
}
|
||||
|
||||
// 现在到明日刷新点的剩余时间间隔
|
||||
func GetDurationToTomorrow() (d time.Duration) {
|
||||
return GetDurationToNextNDay(1)
|
||||
}
|
||||
|
||||
// 现在到第N天刷新点的剩余时间间隔 (n为1是明天)
|
||||
func GetDurationToNextNDay(n int) (d time.Duration) {
|
||||
now := Now().StdTime()
|
||||
next := Now().AddDays(n).StartOfDay().StdTime()
|
||||
return next.Sub(now)
|
||||
}
|
||||
|
||||
// 现在到下周刷新点的剩余时间间隔
|
||||
func GetDurationToNextWeek() (expiration time.Duration) {
|
||||
return GetDurationToNextNWeek(1)
|
||||
}
|
||||
|
||||
// 现在到下N周刷新点的剩余时间间隔 (n为1是下周)
|
||||
func GetDurationToNextNWeek(n int) (expiration time.Duration) {
|
||||
now := Now().StdTime()
|
||||
nextN := Now().AddWeeks(n).StartOfWeek().StdTime()
|
||||
return nextN.Sub(now)
|
||||
}
|
||||
|
||||
// 现在到下月刷新点的剩余时间间隔
|
||||
func GetDurationToNextMonth() (expiration time.Duration) {
|
||||
return GetDurationToNextNMonth(1)
|
||||
}
|
||||
|
||||
// 现在到下N月刷新点的剩余时间间隔 (n为1是下月)
|
||||
func GetDurationToNextNMonth(n int) (expiration time.Duration) {
|
||||
now := Now().StdTime()
|
||||
nextN := Now().AddMonths(n).StartOfMonth().StdTime()
|
||||
return nextN.Sub(now)
|
||||
}
|
||||
|
||||
// 现在到下季度刷新点的剩余时间间隔
|
||||
func GetDurationToNextQuarter() (expiration time.Duration) {
|
||||
return GetDurationToNextNQuarter(1)
|
||||
}
|
||||
|
||||
// 现在到下N季度刷新点的剩余时间间隔 (n为1是下年)
|
||||
func GetDurationToNextNQuarter(n int) (expiration time.Duration) {
|
||||
now := Now().StdTime()
|
||||
nextN := Now().AddQuarters(n).StartOfQuarter().StdTime()
|
||||
return nextN.Sub(now)
|
||||
}
|
||||
|
||||
// 现在到下年刷新点的剩余时间间隔
|
||||
func GetDurationToNextYear() (expiration time.Duration) {
|
||||
return GetDurationToNextNYear(1)
|
||||
}
|
||||
|
||||
// 现在到下N年刷新点的剩余时间间隔 (n为1是下年)
|
||||
func GetDurationToNextNYear(n int) (expiration time.Duration) {
|
||||
now := Now().StdTime()
|
||||
nextN := Now().AddYears(n).StartOfYear().StdTime()
|
||||
return nextN.Sub(now)
|
||||
}
|
114
xtime/duration_test.go
Normal file
114
xtime/duration_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDurationToNextHour(t *testing.T) {
|
||||
now := Now()
|
||||
target := Tomorrow()
|
||||
d := GetDurationToNextHour()
|
||||
fmt.Printf("now[%v] %s to next hour[%s] \n",
|
||||
now,
|
||||
d,
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextNHour(t *testing.T) {
|
||||
n := 3
|
||||
now := Now()
|
||||
target := Tomorrow()
|
||||
d := GetDurationToNextNHour(n)
|
||||
fmt.Printf("now[%v] %s to next %d hour[%s] \n",
|
||||
now,
|
||||
d,
|
||||
n,
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToTomorrow(t *testing.T) {
|
||||
now := Now()
|
||||
target := Tomorrow()
|
||||
d := GetDurationToTomorrow()
|
||||
fmt.Printf("now[%v](%s) %s to tomorrow[%s](%s) \n",
|
||||
FormatDate(now),
|
||||
now,
|
||||
d,
|
||||
FormatDate(target),
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextNDay(t *testing.T) {
|
||||
n := 3
|
||||
now := Now()
|
||||
target := NextNDay(n)
|
||||
d := GetDurationToNextNDay(n)
|
||||
fmt.Printf("now[%v](%s) %s to next %v day[%s](%s) \n",
|
||||
FormatDate(now),
|
||||
now,
|
||||
d,
|
||||
n,
|
||||
FormatDate(target),
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextWeek(t *testing.T) {
|
||||
now := Now()
|
||||
target := NextWeek()
|
||||
d := GetDurationToNextWeek()
|
||||
fmt.Printf("now[%v](%s) %s to next week[%s](%s) \n",
|
||||
FormatWeek(now),
|
||||
now,
|
||||
d,
|
||||
FormatWeek(target),
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextNWeek(t *testing.T) {
|
||||
n := 3
|
||||
now := Now()
|
||||
target := NextNWeek(n)
|
||||
d := GetDurationToNextNWeek(n)
|
||||
fmt.Printf("now[%v](%s) %s to next %v week[%s](%s) \n",
|
||||
FormatWeek(now),
|
||||
now,
|
||||
d,
|
||||
n,
|
||||
FormatWeek(target),
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextMonth(t *testing.T) {
|
||||
now := Now()
|
||||
target := NextMonth()
|
||||
d := GetDurationToNextMonth()
|
||||
fmt.Printf("now[%v](%s) %s to next month[%s](%s) \n",
|
||||
FormatMonth(now),
|
||||
now,
|
||||
d,
|
||||
FormatMonth(target),
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGetDurationToNextNMonth(t *testing.T) {
|
||||
n := 3
|
||||
now := Now()
|
||||
target := NextNMonth(n)
|
||||
d := GetDurationToNextNMonth(n)
|
||||
fmt.Printf("now[%v](%s) %s to next %v month[%s](%s) \n",
|
||||
FormatMonth(now),
|
||||
now,
|
||||
d,
|
||||
n,
|
||||
FormatMonth(target),
|
||||
target,
|
||||
)
|
||||
}
|
46
xtime/format.go
Normal file
46
xtime/format.go
Normal file
@ -0,0 +1,46 @@
|
||||
package xtime
|
||||
|
||||
import "github.com/golang-module/carbon/v2"
|
||||
|
||||
const (
|
||||
WeekLayout = "Y-W"
|
||||
MonthLayout = "Y-m"
|
||||
QuarterLayout = "Y-Q"
|
||||
YearLayout = "Y"
|
||||
)
|
||||
|
||||
// 2006-01-02
|
||||
|
||||
func FormatDate(c carbon.Carbon) string {
|
||||
return c.ToDateString()
|
||||
}
|
||||
|
||||
// 2006-55 (55周)
|
||||
|
||||
func FormatWeek(c carbon.Carbon) string {
|
||||
return c.Format(WeekLayout)
|
||||
}
|
||||
|
||||
// 2006-12
|
||||
|
||||
func FormatMonth(c carbon.Carbon) string {
|
||||
return c.Format(MonthLayout)
|
||||
}
|
||||
|
||||
// 2006-04 (4季度)
|
||||
|
||||
func FormatQuarter(c carbon.Carbon) string {
|
||||
return c.Format(QuarterLayout)
|
||||
}
|
||||
|
||||
// 2006
|
||||
|
||||
func FormatYear(c carbon.Carbon) string {
|
||||
return c.Format(YearLayout)
|
||||
}
|
||||
|
||||
// 20060102
|
||||
|
||||
func FormatDateNum(c carbon.Carbon) int {
|
||||
return c.Year()*10000 + c.Month()*100 + c.Day()
|
||||
}
|
32
xtime/format_test.go
Normal file
32
xtime/format_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
fmt.Printf(
|
||||
"FormatDate(Yesterday()): %v\nFormatDate(Today()): %v\nFormatDate(Tomorrow()): %v\nFormatDate(NextNDay(3)): %v\nFormatWeek(ThisWeek()): %v\nFormatWeek(LastWeek()): %v\nFormatWeek(NextWeek()): %v\nFormatWeek(NextNWeek(3)): %v\nFormatMonth(ThisMonth()): %v\nFormatMonth(LastMonth()): %v\nFormatMonth(NextMonth()): %v\nFormatMonth(NextNMonth(3)): %v\nFormatQuarter(ThisQuarter()): %v\nFormatQuarter(LastQuarter()): %v\nFormatQuarter(NextQuarter()): %v\nFormatQuarter(NextNQuarter(3)): %v\nFormatYear(ThisYear()): %v\nFormatYear(LastYear()): %v\nFormatYear(NextYear()): %v\nFormatYear(NextNYear(3)): %v\n",
|
||||
FormatDate(Yesterday()),
|
||||
FormatDate(Today()),
|
||||
FormatDate(Tomorrow()),
|
||||
FormatDate(NextNDay(3)),
|
||||
FormatWeek(ThisWeek()),
|
||||
FormatWeek(LastWeek()),
|
||||
FormatWeek(NextWeek()),
|
||||
FormatWeek(NextNWeek(3)),
|
||||
FormatMonth(ThisMonth()),
|
||||
FormatMonth(LastMonth()),
|
||||
FormatMonth(NextMonth()),
|
||||
FormatMonth(NextNMonth(3)),
|
||||
FormatQuarter(ThisQuarter()),
|
||||
FormatQuarter(LastQuarter()),
|
||||
FormatQuarter(NextQuarter()),
|
||||
FormatQuarter(NextNQuarter(3)),
|
||||
FormatYear(ThisYear()),
|
||||
FormatYear(LastYear()),
|
||||
FormatYear(NextYear()),
|
||||
FormatYear(NextNYear(3)),
|
||||
)
|
||||
}
|
11
xtime/os.go
Normal file
11
xtime/os.go
Normal file
@ -0,0 +1,11 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 设置系统时区
|
||||
|
||||
func SetTimeOffset(offset int) {
|
||||
time.Local = time.FixedZone("CST", offset)
|
||||
}
|
41
xtime/parser.go
Normal file
41
xtime/parser.go
Normal file
@ -0,0 +1,41 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"github.com/golang-module/carbon/v2"
|
||||
)
|
||||
|
||||
// 解析系统时区输出的字符串、时间戳,转为偏移时区
|
||||
// 注意如果你的时间戳和字符串已经是偏移时区输出的,就不应使用下列解析函数!
|
||||
|
||||
func Parse(value string, offsets ...int) (c carbon.Carbon) {
|
||||
c = carbon.Parse(value)
|
||||
c = set(c, offsets...)
|
||||
return c
|
||||
}
|
||||
|
||||
//goland:noinspection GoUnusedExportedFunction
|
||||
func FromUnix(sec int64, offsets ...int) carbon.Carbon {
|
||||
c := carbon.CreateFromTimestamp(sec)
|
||||
c = set(c, offsets...)
|
||||
return c
|
||||
}
|
||||
|
||||
func FromUnixMilli(msec int64, offsets ...int) carbon.Carbon {
|
||||
c := carbon.CreateFromTimestampMilli(msec)
|
||||
c = set(c, offsets...)
|
||||
return c
|
||||
}
|
||||
|
||||
//goland:noinspection GoUnusedExportedFunction
|
||||
func FromUnixMicro(msec int64, offsets ...int) carbon.Carbon {
|
||||
c := carbon.CreateFromTimestampMicro(msec)
|
||||
c = set(c, offsets...)
|
||||
return c
|
||||
}
|
||||
|
||||
//goland:noinspection GoUnusedExportedFunction
|
||||
func FromUnixNano(msec int64, offsets ...int) carbon.Carbon {
|
||||
c := carbon.CreateFromTimestampNano(msec)
|
||||
c = set(c, offsets...)
|
||||
return c
|
||||
}
|
16
xtime/parser_test.go
Normal file
16
xtime/parser_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
assert.Equal(t, Parse("2023-01-01 00:00:00").ToDateTimeString(), "2022-12-31 20:00:00")
|
||||
}
|
||||
|
||||
func TestFromMilli(t *testing.T) {
|
||||
ts := int64(1672502400000) // "2023-01-01 00:00:00"
|
||||
assert.Equal(t, FromUnixMilli(ts).ToDateTimeString(), "2022-12-31 20:00:00")
|
||||
}
|
209
xtime/timex.go
Normal file
209
xtime/timex.go
Normal file
@ -0,0 +1,209 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-module/carbon/v2"
|
||||
)
|
||||
|
||||
// 基于Carbon而不是Time
|
||||
// 默认将 SaoPauloTimezoneOffset 作为时区偏移
|
||||
// 默认将 WeekStartsAt 作为每周起始
|
||||
// 注意新加的函数都要基于Now()
|
||||
|
||||
// 设置时区
|
||||
// reverse:给Now()设置时传false,解析系统时区输出的字符串/时间戳时传true
|
||||
func setLocation(c carbon.Carbon, offsets ...int) carbon.Carbon {
|
||||
var loc *time.Location
|
||||
if len(offsets) > 0 {
|
||||
loc = time.FixedZone("CUS", offsets[len(offsets)-1])
|
||||
} else {
|
||||
loc = time.FixedZone("CST", sysTimezoneOffset)
|
||||
}
|
||||
|
||||
return c.SetLocation(loc)
|
||||
}
|
||||
|
||||
func set(c carbon.Carbon, offsets ...int) carbon.Carbon {
|
||||
// 设置周起始
|
||||
c = c.SetWeekStartsAt(WeekStartsAt)
|
||||
// 设置时区
|
||||
c = setLocation(c, offsets...)
|
||||
return c
|
||||
}
|
||||
|
||||
func Now(offsets ...int) (c carbon.Carbon) {
|
||||
c = carbon.Now().AddSeconds(nowOffset)
|
||||
c = set(c, offsets...)
|
||||
return
|
||||
}
|
||||
|
||||
func ConvertToCarbon[T time.Time | carbon.Carbon | int64](t T, offsets ...int) carbon.Carbon {
|
||||
var c carbon.Carbon
|
||||
switch v := any(t).(type) {
|
||||
case time.Time:
|
||||
c = set(carbon.CreateFromStdTime(v), offsets...)
|
||||
case carbon.Carbon:
|
||||
c = set(v, offsets...)
|
||||
case int64:
|
||||
c = set(carbon.CreateFromTimestamp(v), offsets...)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// IsTodayTimestamp 判断该时间戳是否为今天
|
||||
func IsTodayTimestamp[T int | int64](timestamp T, offsets ...int) bool {
|
||||
return IsToday(int64(timestamp), offsets...)
|
||||
}
|
||||
|
||||
// IsToday 判断时间是否为今天
|
||||
func IsToday[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
|
||||
c := ConvertToCarbon(t, offsets...)
|
||||
|
||||
return c.ToDateString() == Now().ToDateString()
|
||||
}
|
||||
|
||||
// IsBeforeNow 判断时间t是否在现在之前
|
||||
// example:
|
||||
//
|
||||
// now := time.Now()
|
||||
// before := time.Now().Add(-time.Hour)
|
||||
// IsBeforeNow(after) // true
|
||||
func IsBeforeNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
|
||||
return !IsAfterNow(t, offsets...)
|
||||
}
|
||||
|
||||
// IsAfterNow 判断时间t是否在现在之后
|
||||
// example:
|
||||
//
|
||||
// now := time.Now()
|
||||
// after := time.Now().Add(time.Hour)
|
||||
// IsAfterNow(after) // true
|
||||
func IsAfterNow[T time.Time | carbon.Carbon | int64](t T, offsets ...int) bool {
|
||||
switch v := any(t).(type) {
|
||||
case time.Time:
|
||||
return IsAfter(v, time.Now(), offsets...)
|
||||
case carbon.Carbon:
|
||||
return IsAfter(v, Now(), offsets...)
|
||||
case int64:
|
||||
return IsAfter(v, Now().Timestamp())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsBefore 判断时间t1是否在t2之前
|
||||
// example:
|
||||
// t1 := time.Now().Add(-time.Hour)
|
||||
// t2 := time.Now()
|
||||
// IsBefore(t1,t2) // true
|
||||
func IsBefore[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool {
|
||||
return !IsAfter(t1, t2, offsets...)
|
||||
}
|
||||
|
||||
// IsAfter 判断时间t1是否在t2之后
|
||||
// example:
|
||||
// t1 := time.Now().Add(time.Hour)
|
||||
// t2 := time.Now()
|
||||
// IsAfter(t1,t2) // true
|
||||
func IsAfter[T time.Time | carbon.Carbon | int64](t1, t2 T, offsets ...int) bool {
|
||||
c1, c2 := ConvertToCarbon(t1, offsets...), ConvertToCarbon(t2, offsets...)
|
||||
return c1.Gt(c2)
|
||||
}
|
||||
|
||||
// TimestampToTime 将时间戳转为 carbon.Carbon 类型
|
||||
func TimestampToTime[T int | int64](timestamp T, offsets ...int) carbon.Carbon {
|
||||
return set(carbon.CreateFromTimestamp(int64(timestamp)), offsets...)
|
||||
}
|
||||
|
||||
// IsInDuration 判断某个时间在某个时间段内
|
||||
func IsInDuration[T time.Time | carbon.Carbon | int64](t, start, end T, offsets ...int) bool {
|
||||
return IsAfter(t, start, offsets...) && IsBefore(t, end, offsets...)
|
||||
}
|
||||
|
||||
// FormatDuration 将起止时间,格式化为 format - format
|
||||
func FormatDuration[T time.Time | carbon.Carbon | int64](start, end T, layout string, offsets ...int) string {
|
||||
startC, endC := ConvertToCarbon(start, offsets...), ConvertToCarbon(end, offsets...)
|
||||
return fmt.Sprintf("%s - %s", startC.StdTime().Format(layout), endC.StdTime().Format(layout))
|
||||
}
|
||||
|
||||
func Yesterday(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).SubDay().StartOfDay()
|
||||
}
|
||||
|
||||
func Today(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).StartOfDay()
|
||||
}
|
||||
|
||||
func Tomorrow(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddDay().StartOfDay()
|
||||
}
|
||||
|
||||
func NextNDay(n int, offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddDays(n).StartOfDay()
|
||||
}
|
||||
|
||||
func LastWeek(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).SubWeek().StartOfWeek()
|
||||
}
|
||||
|
||||
func ThisWeek(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).StartOfWeek()
|
||||
}
|
||||
|
||||
func NextWeek(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddWeek().StartOfWeek()
|
||||
}
|
||||
|
||||
func NextNWeek(n int, offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddWeeks(n).StartOfWeek()
|
||||
}
|
||||
|
||||
func LastMonth(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).SubMonth().StartOfMonth()
|
||||
}
|
||||
|
||||
func ThisMonth(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).StartOfMonth()
|
||||
}
|
||||
|
||||
func NextMonth(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddMonth().StartOfMonth()
|
||||
}
|
||||
|
||||
func NextNMonth(n int, offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddMonths(n).StartOfMonth()
|
||||
}
|
||||
|
||||
func LastQuarter(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).SubQuarter().StartOfQuarter()
|
||||
}
|
||||
|
||||
func ThisQuarter(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).StartOfQuarter()
|
||||
}
|
||||
|
||||
func NextQuarter(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddQuarter().StartOfQuarter()
|
||||
}
|
||||
|
||||
func NextNQuarter(n int, offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddQuarters(n).StartOfQuarter()
|
||||
}
|
||||
|
||||
func LastYear(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).SubYear().StartOfYear()
|
||||
}
|
||||
|
||||
func ThisYear(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).StartOfYear()
|
||||
}
|
||||
|
||||
func NextYear(offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddYear().StartOfYear()
|
||||
}
|
||||
|
||||
func NextNYear(n int, offsets ...int) carbon.Carbon {
|
||||
return Now(offsets...).AddYears(n).StartOfYear()
|
||||
}
|
146
xtime/timex_test.go
Normal file
146
xtime/timex_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSetTimeOffset(t *testing.T) {
|
||||
loc, err := time.LoadLocation("Asia/Shanghai")
|
||||
assert.NoError(t, err)
|
||||
loc2, err := time.LoadLocation("America/New_York")
|
||||
assert.NoError(t, err)
|
||||
a := time.Now().In(loc)
|
||||
b := time.Now().In(loc2)
|
||||
t.Log(a.String())
|
||||
t.Log(b.String())
|
||||
Now()
|
||||
t.Log(a.Sub(b))
|
||||
}
|
||||
|
||||
func TestFormatDayNum(t *testing.T) {
|
||||
t1 := Parse("2023-01-01 00:00:00", 0)
|
||||
assert.Equal(t, FormatDateNum(t1), 20221231)
|
||||
t2 := Parse("2023-01-01 04:00:00", 0)
|
||||
assert.Equal(t, FormatDateNum(t2), 20230101)
|
||||
}
|
||||
|
||||
func TestNow(t *testing.T) {
|
||||
now := Now()
|
||||
sc := carbon.Now(carbon.SaoPaulo)
|
||||
|
||||
b := func(args ...int) []int { return args }
|
||||
assert.Equal(t, b(now.Time()), b(sc.Time()))
|
||||
|
||||
}
|
||||
|
||||
func TestIsToday(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
assert.True(t, IsToday(now))
|
||||
|
||||
assert.False(t, IsToday(now.Add(24*time.Hour)))
|
||||
assert.False(t, IsToday(now.Add(-24*time.Hour)))
|
||||
|
||||
}
|
||||
|
||||
func TestIsTodayTimestamp(t *testing.T) {
|
||||
now := Now()
|
||||
st := now.Timestamp()
|
||||
|
||||
assert.True(t, IsTodayTimestamp(st))
|
||||
|
||||
assert.False(t, IsTodayTimestamp(now.Yesterday().Timestamp()))
|
||||
assert.False(t, IsTodayTimestamp(now.AddDay().Timestamp()))
|
||||
}
|
||||
|
||||
func TestIsAfterNow(t *testing.T) {
|
||||
t.Run("std Time", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
after := now.Add(time.Hour)
|
||||
assert.True(t, IsAfterNow(after))
|
||||
now = time.Now()
|
||||
before := now.Add(-time.Hour)
|
||||
assert.False(t, IsAfterNow(before))
|
||||
})
|
||||
|
||||
t.Run("carbon", func(t *testing.T) {
|
||||
now := Now()
|
||||
after := now.AddHour()
|
||||
assert.True(t, IsAfterNow(after))
|
||||
now = Now()
|
||||
before := now.SubHour()
|
||||
assert.False(t, IsAfterNow(before))
|
||||
})
|
||||
t.Run("timestamp", func(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
after := now + 3600
|
||||
assert.True(t, IsAfterNow(after))
|
||||
now = time.Now().Unix()
|
||||
before := now - 3600
|
||||
assert.False(t, IsAfterNow(before))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsBeforeNow(t *testing.T) {
|
||||
t.Run("std Time", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
before := now.Add(-time.Hour)
|
||||
assert.True(t, IsBeforeNow(before))
|
||||
now = time.Now()
|
||||
after := now.Add(time.Hour)
|
||||
assert.False(t, IsBeforeNow(after))
|
||||
})
|
||||
|
||||
t.Run("carbon", func(t *testing.T) {
|
||||
now := Now()
|
||||
before := now.SubHour()
|
||||
assert.True(t, IsBeforeNow(before))
|
||||
now = Now()
|
||||
after := now.AddHour()
|
||||
assert.False(t, IsBeforeNow(after))
|
||||
})
|
||||
t.Run("timestamp", func(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
before := now - 3600
|
||||
assert.True(t, IsBeforeNow(before))
|
||||
now = time.Now().Unix()
|
||||
after := now + 3600
|
||||
assert.False(t, IsBeforeNow(after))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsInDuration(t *testing.T) {
|
||||
|
||||
t.Run("std Time", func(t *testing.T) {
|
||||
now := time.Now()
|
||||
start := now.Add(-time.Hour)
|
||||
end := now.Add(time.Hour)
|
||||
assert.True(t, IsInDuration(now, start, end))
|
||||
assert.False(t, IsInDuration(now.Add(10*time.Hour), start, end))
|
||||
assert.False(t, IsInDuration(now.Add(-10*time.Hour), start, end))
|
||||
})
|
||||
|
||||
t.Run("carbon", func(t *testing.T) {
|
||||
now := Now()
|
||||
start := now.SubHour()
|
||||
end := now.AddHour()
|
||||
assert.True(t, IsInDuration(now, start, end))
|
||||
assert.False(t, IsInDuration(now.AddHours(10), start, end))
|
||||
assert.False(t, IsInDuration(now.SubHours(10), start, end))
|
||||
})
|
||||
t.Run("timestamp", func(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
start := now - 3600
|
||||
end := now + 3600
|
||||
assert.True(t, IsInDuration(now, start, end))
|
||||
assert.False(t, IsInDuration(now+3600*10, start, end))
|
||||
assert.False(t, IsInDuration(now-3600*10, start, end))
|
||||
|
||||
})
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user