hub.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. // Package websocket provides a WebSocket hub for real-time updates and notifications.
  2. package websocket
  3. import (
  4. "context"
  5. "encoding/json"
  6. "sync"
  7. "time"
  8. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  9. )
  10. // MessageType identifies the kind of WebSocket message.
  11. type MessageType string
  12. const (
  13. MessageTypeStatus MessageType = "status"
  14. MessageTypeTraffic MessageType = "traffic"
  15. MessageTypeInbounds MessageType = "inbounds"
  16. MessageTypeOutbounds MessageType = "outbounds"
  17. MessageTypeNodes MessageType = "nodes"
  18. MessageTypeNotification MessageType = "notification"
  19. MessageTypeXrayState MessageType = "xray_state"
  20. MessageTypeClientStats MessageType = "client_stats"
  21. MessageTypeClients MessageType = "clients"
  22. MessageTypeInvalidate MessageType = "invalidate"
  23. maxMessageSize = 10 * 1024 * 1024 // 10MB
  24. enqueueTimeout = 100 * time.Millisecond
  25. clientSendQueue = 512 // ~50s of buffering for a momentarily slow browser.
  26. hubBroadcastQueue = 2048 // Headroom for cron-storm + admin-mutation bursts.
  27. hubOpsQueue = 128 // Backlog for register+unregister bursts (page reloads, disconnect storms).
  28. minBroadcastInterval = 250 * time.Millisecond
  29. hubRestartAttempts = 3
  30. )
  31. type clientOpKind int
  32. const (
  33. opRegister clientOpKind = iota
  34. opUnregister
  35. )
  36. type clientOp struct {
  37. kind clientOpKind
  38. c *Client
  39. }
  40. // NewClient builds a Client ready for hub registration.
  41. func NewClient(id string) *Client {
  42. return &Client{
  43. ID: id,
  44. Send: make(chan []byte, clientSendQueue),
  45. }
  46. }
  47. // Message is the wire format sent to clients.
  48. type Message struct {
  49. Type MessageType `json:"type"`
  50. Payload any `json:"payload"`
  51. Time int64 `json:"time"`
  52. }
  53. // Client represents a single WebSocket connection.
  54. type Client struct {
  55. ID string
  56. Send chan []byte
  57. closeOnce sync.Once
  58. }
  59. // Hub fan-outs messages to all connected clients.
  60. type Hub struct {
  61. clients map[*Client]struct{}
  62. broadcast chan []byte
  63. ops chan clientOp
  64. mu sync.RWMutex
  65. ctx context.Context
  66. cancel context.CancelFunc
  67. throttleMu sync.Mutex
  68. lastBroadcast map[MessageType]time.Time
  69. }
  70. // NewHub creates a hub. Call Run in a goroutine to start its event loop.
  71. func NewHub() *Hub {
  72. ctx, cancel := context.WithCancel(context.Background())
  73. return &Hub{
  74. clients: make(map[*Client]struct{}),
  75. broadcast: make(chan []byte, hubBroadcastQueue),
  76. ops: make(chan clientOp, hubOpsQueue),
  77. ctx: ctx,
  78. cancel: cancel,
  79. lastBroadcast: make(map[MessageType]time.Time),
  80. }
  81. }
  82. // Traffic messages carry independent partial updates from Xray, TUIC, and node
  83. // polling jobs. Throttling by message type would silently discard one source
  84. // when two jobs publish within the throttle window.
  85. var throttledMessageTypes = map[MessageType]struct{}{
  86. MessageTypeInbounds: {},
  87. MessageTypeOutbounds: {},
  88. MessageTypeClientStats: {},
  89. }
  90. func (h *Hub) shouldThrottle(msgType MessageType) bool {
  91. if _, gated := throttledMessageTypes[msgType]; !gated {
  92. return false
  93. }
  94. h.throttleMu.Lock()
  95. defer h.throttleMu.Unlock()
  96. now := time.Now()
  97. if last, ok := h.lastBroadcast[msgType]; ok && now.Sub(last) < minBroadcastInterval {
  98. return true
  99. }
  100. h.lastBroadcast[msgType] = now
  101. return false
  102. }
  103. // Run drives the hub. The inner loop is wrapped in a panic-recovery harness
  104. // that retries up to hubRestartAttempts times with backoff so a transient
  105. // panic doesn't permanently kill real-time updates for commercial deployments.
  106. // After the cap, the hub stays down and the frontend falls back to REST polling.
  107. func (h *Hub) Run() {
  108. for attempt := range hubRestartAttempts {
  109. stopped := h.runOnce()
  110. if stopped {
  111. return
  112. }
  113. if attempt < hubRestartAttempts-1 {
  114. wait := time.Duration(1<<attempt) * time.Second // 1s, 2s, 4s
  115. logger.Errorf("WebSocket hub crashed, restarting in %s (%d/%d)", wait, attempt+1, hubRestartAttempts-1)
  116. select {
  117. case <-time.After(wait):
  118. case <-h.ctx.Done():
  119. return
  120. }
  121. }
  122. }
  123. logger.Error("WebSocket hub stopped after exhausting restart attempts")
  124. }
  125. // runOnce drives the event loop once and returns true if the hub stopped
  126. // cleanly (context cancelled). On panic, recover logs and returns false so
  127. // Run can decide whether to retry.
  128. func (h *Hub) runOnce() (stopped bool) {
  129. defer func() {
  130. if r := recover(); r != nil {
  131. logger.Errorf("WebSocket hub panic recovered: %v", r)
  132. stopped = false
  133. }
  134. }()
  135. for {
  136. select {
  137. case <-h.ctx.Done():
  138. h.shutdown()
  139. return true
  140. case op := <-h.ops:
  141. if op.c == nil {
  142. continue
  143. }
  144. switch op.kind {
  145. case opRegister:
  146. h.mu.Lock()
  147. h.clients[op.c] = struct{}{}
  148. n := len(h.clients)
  149. h.mu.Unlock()
  150. logger.Debugf("WebSocket client connected: %s (total: %d)", op.c.ID, n)
  151. case opUnregister:
  152. h.removeClient(op.c)
  153. }
  154. case msg := <-h.broadcast:
  155. h.fanout(msg)
  156. }
  157. }
  158. }
  159. // shutdown closes all client send channels and clears the registry.
  160. func (h *Hub) shutdown() {
  161. h.mu.Lock()
  162. for c := range h.clients {
  163. c.closeOnce.Do(func() { close(c.Send) })
  164. }
  165. h.clients = make(map[*Client]struct{})
  166. h.mu.Unlock()
  167. logger.Info("WebSocket hub stopped")
  168. }
  169. // removeClient deletes a client and closes its send channel exactly once.
  170. func (h *Hub) removeClient(c *Client) {
  171. h.mu.Lock()
  172. if _, ok := h.clients[c]; ok {
  173. delete(h.clients, c)
  174. c.closeOnce.Do(func() { close(c.Send) })
  175. }
  176. n := len(h.clients)
  177. h.mu.Unlock()
  178. logger.Debugf("WebSocket client disconnected: %s (total: %d)", c.ID, n)
  179. }
  180. // fanout delivers msg to every client. Each send is non-blocking — a client
  181. // whose buffer is full is collected for direct removal at the end. We do NOT
  182. // route slow-client unregistrations through the unregister channel: under
  183. // burst load (panel restart, network blip) that channel can fill up while the
  184. // hub itself is the consumer, causing a self-deadlock.
  185. func (h *Hub) fanout(msg []byte) {
  186. if msg == nil {
  187. return
  188. }
  189. h.mu.RLock()
  190. if len(h.clients) == 0 {
  191. h.mu.RUnlock()
  192. return
  193. }
  194. targets := make([]*Client, 0, len(h.clients))
  195. for c := range h.clients {
  196. targets = append(targets, c)
  197. }
  198. h.mu.RUnlock()
  199. var dead []*Client
  200. for _, c := range targets {
  201. if !trySend(c, msg) {
  202. dead = append(dead, c)
  203. }
  204. }
  205. if len(dead) == 0 {
  206. return
  207. }
  208. h.mu.Lock()
  209. for _, c := range dead {
  210. if _, ok := h.clients[c]; ok {
  211. delete(h.clients, c)
  212. c.closeOnce.Do(func() { close(c.Send) })
  213. logger.Debugf("WebSocket client %s send buffer full, disconnected", c.ID)
  214. }
  215. }
  216. h.mu.Unlock()
  217. }
  218. // trySend performs a non-blocking write to the client's Send channel.
  219. // Returns false if the client should be evicted (full buffer or closed channel).
  220. // A defer-recover guards against the rare race where the channel was closed
  221. // concurrently — sending on a closed channel always panics, even with select+default.
  222. func trySend(c *Client, msg []byte) (ok bool) {
  223. defer func() {
  224. if r := recover(); r != nil {
  225. ok = false
  226. }
  227. }()
  228. select {
  229. case c.Send <- msg:
  230. return true
  231. default:
  232. return false
  233. }
  234. }
  235. // Broadcast serializes payload and queues it for delivery to all clients.
  236. // If the serialized message exceeds maxMessageSize, an invalidate signal is
  237. // queued instead so the frontend re-fetches via REST. Broadcasts of throttled
  238. // message types (see throttledMessageTypes) within minBroadcastInterval of
  239. // the previous one are dropped — the next legitimate mutation will push the
  240. // fresh state.
  241. func (h *Hub) Broadcast(messageType MessageType, payload any) {
  242. if h == nil || payload == nil || h.GetClientCount() == 0 {
  243. return
  244. }
  245. if h.shouldThrottle(messageType) {
  246. return
  247. }
  248. data, err := json.Marshal(Message{
  249. Type: messageType,
  250. Payload: payload,
  251. Time: time.Now().UnixMilli(),
  252. })
  253. if err != nil {
  254. logger.Error("WebSocket marshal failed:", err)
  255. return
  256. }
  257. if len(data) > maxMessageSize {
  258. logger.Debugf("WebSocket payload %d bytes exceeds limit, sending invalidate for %s", len(data), messageType)
  259. h.broadcastInvalidate(messageType)
  260. return
  261. }
  262. h.enqueue(data)
  263. }
  264. // broadcastInvalidate queues a lightweight signal telling clients to re-fetch
  265. // the named data type via REST.
  266. func (h *Hub) broadcastInvalidate(originalType MessageType) {
  267. data, err := json.Marshal(Message{
  268. Type: MessageTypeInvalidate,
  269. Payload: map[string]string{"type": string(originalType)},
  270. Time: time.Now().UnixMilli(),
  271. })
  272. if err != nil {
  273. logger.Error("WebSocket invalidate marshal failed:", err)
  274. return
  275. }
  276. h.enqueue(data)
  277. }
  278. // enqueue submits raw bytes to the broadcast channel. Dropped on backpressure
  279. // (channel full for >100ms) or shutdown.
  280. func (h *Hub) enqueue(data []byte) {
  281. select {
  282. case h.broadcast <- data:
  283. case <-time.After(enqueueTimeout):
  284. logger.Warning("WebSocket broadcast channel full, dropping message")
  285. case <-h.ctx.Done():
  286. }
  287. }
  288. // GetClientCount returns the number of connected clients.
  289. func (h *Hub) GetClientCount() int {
  290. if h == nil {
  291. return 0
  292. }
  293. h.mu.RLock()
  294. defer h.mu.RUnlock()
  295. return len(h.clients)
  296. }
  297. // Register adds a client to the hub.
  298. func (h *Hub) Register(c *Client) {
  299. if h == nil || c == nil {
  300. return
  301. }
  302. select {
  303. case h.ops <- clientOp{kind: opRegister, c: c}:
  304. case <-h.ctx.Done():
  305. }
  306. }
  307. // Unregister removes a client from the hub. Sends through the same ordered
  308. // ops channel as Register so a register-then-unregister sequence from one
  309. // goroutine is processed in program order — otherwise an unregister could
  310. // land in the map before its register and silently no-op, leaking the entry.
  311. //
  312. // On a saturated ops channel (disconnect storm) we fall back to a bounded
  313. // timeout drop rather than direct removal: a direct delete on a not-yet-
  314. // registered client is precisely the ordering bug we fix here. Stragglers
  315. // get evicted by fanout when their Send buffer fills.
  316. func (h *Hub) Unregister(c *Client) {
  317. if h == nil || c == nil {
  318. return
  319. }
  320. select {
  321. case h.ops <- clientOp{kind: opUnregister, c: c}:
  322. case <-time.After(enqueueTimeout):
  323. logger.Warningf("WebSocket ops channel full, dropping unregister for %s", c.ID)
  324. case <-h.ctx.Done():
  325. }
  326. }
  327. // Stop signals the hub to shut down and close all client connections.
  328. func (h *Hub) Stop() {
  329. if h != nil && h.cancel != nil {
  330. h.cancel()
  331. }
  332. }