package app import ( "context" "errors" "fmt" "net" "net/http" "os" "strings" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "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 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 } 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 userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn) 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.SetUserCountryQueryClient(userCountryQueryClient) 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, }) 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 { _ = roomConn.Close() _ = userConn.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() } _ = roomConn.Close() _ = userConn.Close() return nil, err } server := &http.Server{ Handler: mux, } return &App{ server: server, listener: listener, roomConn: roomConn, userConn: userConn, redisClose: redisClose, health: healthState, }, nil } 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.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 }