2026-05-06 21:22:39 +08:00

280 lines
8.6 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"hyapp/pkg/tencentcos"
"hyapp/services/gateway-service/internal/appconfig"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/client"
"hyapp/services/gateway-service/internal/config"
"hyapp/services/gateway-service/internal/healthcheck"
httptransport "hyapp/services/gateway-service/internal/transport/http"
)
// App 装配 gateway 的 HTTP 入口。
type App struct {
server *http.Server
listener net.Listener
roomConn *grpc.ClientConn
userConn *grpc.ClientConn
walletConn *grpc.ClientConn
activityConn *grpc.ClientConn
appConfig *appconfig.MySQLReader
redisClose func() error
health *healthcheck.State
closeOnce sync.Once
}
// New 初始化 gateway 应用。
func New(cfg config.Config) (*App, error) {
if err := cfg.Normalize(); err != nil {
return nil, err
}
roomConn, err := grpc.Dial(cfg.RoomServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
_ = roomConn.Close()
return nil, err
}
walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
return nil, err
}
activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
return nil, err
}
var roomClient client.RoomClient = client.NewGRPCRoomClient(roomConn)
var roomGuardClient client.RoomGuardClient = client.NewGRPCRoomGuardClient(roomConn)
var roomQueryClient client.RoomQueryClient = client.NewGRPCRoomQueryClient(roomConn)
var userClient client.UserAuthClient = client.NewGRPCUserAuthClient(userConn)
var userIdentityClient client.UserIdentityClient = client.NewGRPCUserIdentityClient(userConn)
var userProfileClient client.UserProfileClient = client.NewGRPCUserProfileClient(userConn)
var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
appConfigReader, err := openAppConfigReader(cfg.AppConfig)
if err != nil {
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
return nil, err
}
handler := httptransport.NewHandlerWithConfig(roomClient, roomGuardClient, userClient, userIdentityClient, httptransport.TencentIMConfig{
SDKAppID: cfg.TencentIM.SDKAppID,
SecretKey: cfg.TencentIM.SecretKey,
UserSigTTL: cfg.TencentIM.UserSigTTL,
CallbackAuthToken: cfg.TencentIM.CallbackAuthToken,
}, userProfileClient)
handler.SetRoomQueryClient(roomQueryClient)
handler.SetUserDeviceClient(userDeviceClient)
handler.SetUserCountryQueryClient(userCountryQueryClient)
handler.SetUserHostClient(userHostClient)
handler.SetAppRegistryClient(appRegistryClient)
handler.SetWalletClient(walletClient)
handler.SetMessageInboxClient(messageClient)
if appConfigReader != nil {
handler.SetAppConfigReader(appConfigReader)
}
handler.SetTencentRTC(httptransport.TencentRTCConfig{
Enabled: cfg.TencentRTC.Enabled,
SDKAppID: cfg.TencentRTC.SDKAppID,
SecretKey: cfg.TencentRTC.SecretKey,
UserSigTTL: cfg.TencentRTC.UserSigTTL,
RoomIDType: cfg.TencentRTC.RoomIDType,
AppScene: cfg.TencentRTC.AppScene,
CallbackSignKey: cfg.TencentRTC.CallbackSignKey,
})
if cfg.TencentCOS.Enabled {
uploader, err := tencentcos.NewUploader(tencentcos.Config{
SecretID: cfg.TencentCOS.SecretID,
SecretKey: cfg.TencentCOS.SecretKey,
Bucket: cfg.TencentCOS.BucketName,
Region: cfg.TencentCOS.Region,
AccessURL: cfg.TencentCOS.AccessURL,
}, nil)
if err != nil {
if appConfigReader != nil {
_ = appConfigReader.Close()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
return nil, err
}
handler.SetObjectUploader(uploader)
}
rateLimitConfig := httptransport.AuthRateLimitConfig{
Enabled: cfg.AuthRateLimit.Enabled,
KeyPrefix: cfg.AuthRateLimit.KeyPrefix,
Window: time.Duration(cfg.AuthRateLimit.WindowSec) * time.Second,
IPLimit: cfg.AuthRateLimit.IPLimit,
DeviceIDLimit: cfg.AuthRateLimit.DeviceIDLimit,
ProviderIPLimit: cfg.AuthRateLimit.ProviderIPLimit,
DisplayUserIDIPLimit: cfg.AuthRateLimit.DisplayUserIDIPLimit,
DisplayUserIDLimit: cfg.AuthRateLimit.DisplayUserIDLimit,
SessionIDIPLimit: cfg.AuthRateLimit.SessionIDIPLimit,
}
redisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig)
if err != nil {
if appConfigReader != nil {
_ = appConfigReader.Close()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
return nil, err
}
verifier := auth.NewVerifier(cfg.JWTSecret)
healthState := healthcheck.NewState(nodeID(), cfg.JWTSecret, roomConn)
mux := http.NewServeMux()
healthHandler := healthcheck.NewHTTPHandler(healthState)
mux.HandleFunc("/healthz/live", healthHandler.Live)
mux.HandleFunc("/healthz/ready", healthHandler.Ready)
mux.Handle("/", handler.Routes(verifier))
listener, err := net.Listen("tcp", cfg.HTTPAddr)
if err != nil {
if redisClose != nil {
_ = redisClose()
}
if appConfigReader != nil {
_ = appConfigReader.Close()
}
_ = roomConn.Close()
_ = userConn.Close()
_ = walletConn.Close()
_ = activityConn.Close()
return nil, err
}
server := &http.Server{
Handler: mux,
}
return &App{
server: server,
listener: listener,
roomConn: roomConn,
userConn: userConn,
walletConn: walletConn,
activityConn: activityConn,
appConfig: appConfigReader,
redisClose: redisClose,
health: healthState,
}, nil
}
func openAppConfigReader(cfg config.AppConfigConfig) (*appconfig.MySQLReader, error) {
if strings.TrimSpace(cfg.MySQLDSN) == "" {
return nil, nil
}
return appconfig.OpenMySQLReader(cfg.MySQLDSN)
}
func configureAuthRateLimit(handler *httptransport.Handler, cfg config.AuthRateLimitConfig, rateLimitConfig httptransport.AuthRateLimitConfig) (func() error, error) {
if !rateLimitConfig.Enabled {
// 显式关闭频控时仍记录配置,避免 handler 使用默认开启策略。
handler.SetAuthRateLimit(rateLimitConfig)
return nil, nil
}
redisAddr := strings.TrimSpace(cfg.RedisAddr)
if redisAddr == "" {
return nil, errors.New("auth rate limit redis_addr is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
redisClient, err := httptransport.NewAuthRateLimitRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
if err != nil {
return nil, fmt.Errorf("connect auth rate limit redis: %w", err)
}
// 生产入口必须走 Redis 共享计数;内存 backend 只保留给单元测试和显式禁用频控。
handler.SetAuthRateLimitBackend(rateLimitConfig, httptransport.NewRedisAuthRateLimiter(redisClient, rateLimitConfig))
return redisClient.Close, nil
}
// Run 启动 HTTP 服务。
func (a *App) Run() error {
a.health.MarkHTTPServing()
err := a.server.Serve(a.listener)
a.health.MarkHTTPStopped()
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
// Close 关闭 HTTP 服务。
func (a *App) Close() error {
var err error
a.closeOnce.Do(func() {
a.health.MarkDraining()
if a.roomConn != nil {
err = errors.Join(err, a.roomConn.Close())
}
if a.userConn != nil {
err = errors.Join(err, a.userConn.Close())
}
if a.walletConn != nil {
err = errors.Join(err, a.walletConn.Close())
}
if a.activityConn != nil {
err = errors.Join(err, a.activityConn.Close())
}
if a.appConfig != nil {
err = errors.Join(err, a.appConfig.Close())
}
if a.redisClose != nil {
err = errors.Join(err, a.redisClose())
}
err = errors.Join(err, a.server.Close())
})
return err
}
func nodeID() string {
hostname, err := os.Hostname()
if err != nil || hostname == "" {
return "gateway-service"
}
return hostname
}