package centrifugo import ( "bytes" "context" "encoding/json" "fmt" "io" "net" "net/http" "time" ) // Client Centrifugo 客戶端 type Client struct { apiURL string apiKey string client *http.Client } // ClientConfig 客戶端配置 type ClientConfig struct { // APIURL Centrifugo API 地址(必填) APIURL string // APIKey API 密鑰(必填) APIKey string // Timeout 整體請求超時時間(預設 10 秒) Timeout time.Duration // MaxIdleConns 最大閒置連線數(預設 100) MaxIdleConns int // MaxIdleConnsPerHost 每個 host 最大閒置連線數(預設 20,適合高併發) MaxIdleConnsPerHost int // IdleConnTimeout 閒置連線超時時間(預設 90 秒) IdleConnTimeout time.Duration // DialTimeout 建立連線超時時間(預設 5 秒) DialTimeout time.Duration // TLSHandshakeTimeout TLS 握手超時時間(預設 5 秒) TLSHandshakeTimeout time.Duration // ResponseHeaderTimeout 等待響應頭超時時間(預設 10 秒) ResponseHeaderTimeout time.Duration } // DefaultConfig 返回預設配置 func DefaultConfig(apiURL, apiKey string) ClientConfig { return ClientConfig{ APIURL: apiURL, APIKey: apiKey, Timeout: 10 * time.Second, MaxIdleConns: 100, MaxIdleConnsPerHost: 20, // 提高以支援高併發 IdleConnTimeout: 90 * time.Second, DialTimeout: 5 * time.Second, TLSHandshakeTimeout: 5 * time.Second, ResponseHeaderTimeout: 10 * time.Second, } } // HighPerformanceConfig 返回高效能配置(適合高併發場景) func HighPerformanceConfig(apiURL, apiKey string) ClientConfig { return ClientConfig{ APIURL: apiURL, APIKey: apiKey, Timeout: 5 * time.Second, // 更短的超時 MaxIdleConns: 200, // 更多閒置連線 MaxIdleConnsPerHost: 50, // 更多每 host 連線 IdleConnTimeout: 120 * time.Second, DialTimeout: 3 * time.Second, TLSHandshakeTimeout: 3 * time.Second, ResponseHeaderTimeout: 5 * time.Second, } } // NewClient 創建新的 Centrifugo 客戶端(使用預設配置) func NewClient(apiURL, apiKey string) *Client { return NewClientWithConfig(DefaultConfig(apiURL, apiKey)) } // NewClientWithConfig 創建使用自定義配置的 Centrifugo 客戶端 func NewClientWithConfig(config ClientConfig) *Client { // 設定預設值 if config.Timeout == 0 { config.Timeout = 10 * time.Second } if config.MaxIdleConns == 0 { config.MaxIdleConns = 100 } if config.MaxIdleConnsPerHost == 0 { config.MaxIdleConnsPerHost = 20 } if config.IdleConnTimeout == 0 { config.IdleConnTimeout = 90 * time.Second } if config.DialTimeout == 0 { config.DialTimeout = 5 * time.Second } if config.TLSHandshakeTimeout == 0 { config.TLSHandshakeTimeout = 5 * time.Second } if config.ResponseHeaderTimeout == 0 { config.ResponseHeaderTimeout = 10 * time.Second } transport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: config.DialTimeout, KeepAlive: 30 * time.Second, // TCP keep-alive }).DialContext, MaxIdleConns: config.MaxIdleConns, MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, IdleConnTimeout: config.IdleConnTimeout, TLSHandshakeTimeout: config.TLSHandshakeTimeout, ResponseHeaderTimeout: config.ResponseHeaderTimeout, ForceAttemptHTTP2: true, // 嘗試使用 HTTP/2 } return &Client{ apiURL: config.APIURL, apiKey: config.APIKey, client: &http.Client{ Timeout: config.Timeout, Transport: transport, }, } } // NewClientWithHTTP 創建使用自定義 HTTP 客戶端的 Centrifugo 客戶端 func NewClientWithHTTP(apiURL, apiKey string, httpClient *http.Client) *Client { return &Client{ apiURL: apiURL, apiKey: apiKey, client: httpClient, } } // ==================== Request/Response 結構 ==================== // PublishRequest 發布請求 type PublishRequest struct { Channel string `json:"channel"` Data interface{} `json:"data"` } // BroadcastRequest 批量發布請求 type BroadcastRequest struct { Channels []string `json:"channels"` Data interface{} `json:"data"` } // SubscribeRequest 訂閱請求 type SubscribeRequest struct { User string `json:"user"` Channel string `json:"channel"` } // UnsubscribeRequest 取消訂閱請求 type UnsubscribeRequest struct { User string `json:"user"` Channel string `json:"channel"` } // DisconnectRequest 斷開連線請求 type DisconnectRequest struct { User string `json:"user"` } // DisconnectWithCodeRequest 帶代碼的斷開連線請求 type DisconnectWithCodeRequest struct { User string `json:"user"` Disconnect DisconnectInfo `json:"disconnect,omitempty"` } // DisconnectInfo 斷開連線資訊 type DisconnectInfo struct { Code uint32 `json:"code"` Reason string `json:"reason"` } // PresenceRequest 在線狀態請求 type PresenceRequest struct { Channel string `json:"channel"` } // PresenceStatsRequest 在線統計請求 type PresenceStatsRequest struct { Channel string `json:"channel"` } // HistoryRequest 歷史訊息請求 type HistoryRequest struct { Channel string `json:"channel"` Limit int `json:"limit,omitempty"` Reverse bool `json:"reverse,omitempty"` } // ChannelsRequest 頻道列表請求 type ChannelsRequest struct { Pattern string `json:"pattern,omitempty"` } // InfoRequest 伺服器資訊請求 type InfoRequest struct{} // APIResponse Centrifugo API 通用響應 type APIResponse struct { Error *APIError `json:"error,omitempty"` Result interface{} `json:"result,omitempty"` } // APIError Centrifugo API 錯誤 type APIError struct { Code int `json:"code"` Message string `json:"message"` } func (e *APIError) Error() string { return fmt.Sprintf("centrifugo error %d: %s", e.Code, e.Message) } // PublishResult 發布結果 type PublishResult struct { Offset uint64 `json:"offset,omitempty"` Epoch string `json:"epoch,omitempty"` } // PresenceResult 在線狀態結果 type PresenceResult struct { Presence map[string]ClientInfo `json:"presence"` } // ClientInfo 客戶端資訊 type ClientInfo struct { User string `json:"user"` Client string `json:"client"` ConnInfo json.RawMessage `json:"conn_info,omitempty"` ChanInfo json.RawMessage `json:"chan_info,omitempty"` } // PresenceStatsResult 在線統計結果 type PresenceStatsResult struct { NumClients int `json:"num_clients"` NumUsers int `json:"num_users"` } // HistoryResult 歷史訊息結果 type HistoryResult struct { Publications []Publication `json:"publications"` Offset uint64 `json:"offset,omitempty"` Epoch string `json:"epoch,omitempty"` } // Publication 發布的訊息 type Publication struct { Offset uint64 `json:"offset,omitempty"` Data json.RawMessage `json:"data"` Info *ClientInfo `json:"info,omitempty"` } // ChannelsResult 頻道列表結果 type ChannelsResult struct { Channels map[string]ChannelInfo `json:"channels"` } // ChannelInfo 頻道資訊 type ChannelInfo struct { NumClients int `json:"num_clients"` } // InfoResult 伺服器資訊結果 type InfoResult struct { Nodes []NodeInfo `json:"nodes"` } // NodeInfo 節點資訊 type NodeInfo struct { UID string `json:"uid"` Name string `json:"name"` Version string `json:"version"` NumClients int `json:"num_clients"` NumUsers int `json:"num_users"` NumChannels int `json:"num_channels"` Uptime int `json:"uptime"` } // ==================== 發布相關方法 ==================== // Publish 發布訊息到指定頻道 func (c *Client) Publish(ctx context.Context, channel string, data []byte) (*PublishResult, error) { req := PublishRequest{ Channel: channel, Data: json.RawMessage(data), } return c.publish(ctx, req) } // PublishJSON 發布 JSON 訊息到指定頻道 func (c *Client) PublishJSON(ctx context.Context, channel string, data interface{}) (*PublishResult, error) { req := PublishRequest{ Channel: channel, Data: data, } return c.publish(ctx, req) } func (c *Client) publish(ctx context.Context, req PublishRequest) (*PublishResult, error) { var result PublishResult if err := c.callAPI(ctx, "publish", req, &result); err != nil { return nil, err } return &result, nil } // Broadcast 批量發布訊息到多個頻道 func (c *Client) Broadcast(ctx context.Context, channels []string, data []byte) error { req := BroadcastRequest{ Channels: channels, Data: json.RawMessage(data), } return c.callAPI(ctx, "broadcast", req, nil) } // BroadcastJSON 批量發布 JSON 訊息到多個頻道 func (c *Client) BroadcastJSON(ctx context.Context, channels []string, data interface{}) error { req := BroadcastRequest{ Channels: channels, Data: data, } return c.callAPI(ctx, "broadcast", req, nil) } // ==================== 訂閱管理方法 ==================== // Subscribe 訂閱用戶到頻道 func (c *Client) Subscribe(ctx context.Context, user, channel string) error { req := SubscribeRequest{ User: user, Channel: channel, } return c.callAPI(ctx, "subscribe", req, nil) } // Unsubscribe 取消用戶訂閱 func (c *Client) Unsubscribe(ctx context.Context, user, channel string) error { req := UnsubscribeRequest{ User: user, Channel: channel, } return c.callAPI(ctx, "unsubscribe", req, nil) } // Disconnect 強制斷開用戶連線 func (c *Client) Disconnect(ctx context.Context, user string) error { req := DisconnectRequest{ User: user, } return c.callAPI(ctx, "disconnect", req, nil) } // DisconnectWithCode 強制斷開用戶連線(帶斷開代碼和原因) func (c *Client) DisconnectWithCode(ctx context.Context, user string, code uint32, reason string) error { req := DisconnectWithCodeRequest{ User: user, Disconnect: DisconnectInfo{ Code: code, Reason: reason, }, } return c.callAPI(ctx, "disconnect", req, nil) } // ==================== 在線狀態方法 ==================== // Presence 獲取頻道在線用戶 func (c *Client) Presence(ctx context.Context, channel string) (*PresenceResult, error) { req := PresenceRequest{ Channel: channel, } var result PresenceResult if err := c.callAPI(ctx, "presence", req, &result); err != nil { return nil, err } return &result, nil } // PresenceStats 獲取頻道在線統計 func (c *Client) PresenceStats(ctx context.Context, channel string) (*PresenceStatsResult, error) { req := PresenceStatsRequest{ Channel: channel, } var result PresenceStatsResult if err := c.callAPI(ctx, "presence_stats", req, &result); err != nil { return nil, err } return &result, nil } // ==================== 歷史訊息方法 ==================== // History 獲取頻道歷史訊息 func (c *Client) History(ctx context.Context, channel string, limit int) (*HistoryResult, error) { req := HistoryRequest{ Channel: channel, Limit: limit, } var result HistoryResult if err := c.callAPI(ctx, "history", req, &result); err != nil { return nil, err } return &result, nil } // HistoryReverse 獲取頻道歷史訊息(倒序) func (c *Client) HistoryReverse(ctx context.Context, channel string, limit int) (*HistoryResult, error) { req := HistoryRequest{ Channel: channel, Limit: limit, Reverse: true, } var result HistoryResult if err := c.callAPI(ctx, "history", req, &result); err != nil { return nil, err } return &result, nil } // ==================== 頻道管理方法 ==================== // Channels 獲取所有活躍頻道 func (c *Client) Channels(ctx context.Context) (*ChannelsResult, error) { req := ChannelsRequest{} var result ChannelsResult if err := c.callAPI(ctx, "channels", req, &result); err != nil { return nil, err } return &result, nil } // ChannelsWithPattern 獲取匹配模式的活躍頻道 func (c *Client) ChannelsWithPattern(ctx context.Context, pattern string) (*ChannelsResult, error) { req := ChannelsRequest{ Pattern: pattern, } var result ChannelsResult if err := c.callAPI(ctx, "channels", req, &result); err != nil { return nil, err } return &result, nil } // ==================== 伺服器狀態方法 ==================== // Info 獲取伺服器狀態資訊 func (c *Client) Info(ctx context.Context) (*InfoResult, error) { req := InfoRequest{} var result InfoResult if err := c.callAPI(ctx, "info", req, &result); err != nil { return nil, err } return &result, nil } // Ping 檢查伺服器是否健康 func (c *Client) Ping(ctx context.Context) error { _, err := c.Info(ctx) return err } // ==================== 內部方法 ==================== // callAPI 調用 Centrifugo API func (c *Client) callAPI(ctx context.Context, method string, params interface{}, result interface{}) error { body, err := json.Marshal(params) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } url := fmt.Sprintf("%s/api/%s", c.apiURL, method) httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") if c.apiKey != "" { httpReq.Header.Set("Authorization", "apikey "+c.apiKey) } resp, err := c.client.Do(httpReq) if err != nil { return fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { return fmt.Errorf("centrifugo returned status %d: %s", resp.StatusCode, string(respBody)) } // 解析響應 var apiResp APIResponse if result != nil { apiResp.Result = result } if err := json.Unmarshal(respBody, &apiResp); err != nil { return fmt.Errorf("failed to unmarshal response: %w", err) } if apiResp.Error != nil { return apiResp.Error } return nil }