Welcome to mirror list, hosted at ThFree Co, Russian Federation.

hub.go « websocket « web - github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 1455d1fa99f477fb827eeaac51b12c22bc7e2335 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
// Package websocket provides WebSocket hub for real-time updates and notifications.
package websocket

import (
	"context"
	"encoding/json"
	"runtime"
	"sync"
	"time"

	"github.com/mhsanaei/3x-ui/v2/logger"
)

// MessageType represents the type of WebSocket message
type MessageType string

const (
	MessageTypeStatus       MessageType = "status"       // Server status update
	MessageTypeTraffic      MessageType = "traffic"      // Traffic statistics update
	MessageTypeInbounds     MessageType = "inbounds"     // Inbounds list update
	MessageTypeNotification MessageType = "notification" // System notification
	MessageTypeXrayState    MessageType = "xray_state"   // Xray state change
	MessageTypeOutbounds    MessageType = "outbounds"    // Outbounds list update
	MessageTypeInvalidate   MessageType = "invalidate"   // Lightweight signal telling frontend to re-fetch data via REST
)

// Message represents a WebSocket message
type Message struct {
	Type    MessageType `json:"type"`
	Payload any         `json:"payload"`
	Time    int64       `json:"time"`
}

// Client represents a WebSocket client connection
type Client struct {
	ID        string
	Send      chan []byte
	Hub       *Hub
	Topics    map[MessageType]bool // Subscribed topics
	closeOnce sync.Once            // Ensures Send channel is closed exactly once
}

// Hub maintains the set of active clients and broadcasts messages to them
type Hub struct {
	// Registered clients
	clients map[*Client]bool

	// Inbound messages from clients
	broadcast chan []byte

	// Register requests from clients
	register chan *Client

	// Unregister requests from clients
	unregister chan *Client

	// Mutex for thread-safe operations
	mu sync.RWMutex

	// Context for graceful shutdown
	ctx    context.Context
	cancel context.CancelFunc

	// Worker pool for parallel broadcasting
	workerPoolSize int
}

// NewHub creates a new WebSocket hub
func NewHub() *Hub {
	ctx, cancel := context.WithCancel(context.Background())

	// Calculate optimal worker pool size (CPU cores * 2, but max 100)
	workerPoolSize := runtime.NumCPU() * 2
	if workerPoolSize > 100 {
		workerPoolSize = 100
	}
	if workerPoolSize < 10 {
		workerPoolSize = 10
	}

	return &Hub{
		clients:        make(map[*Client]bool),
		broadcast:      make(chan []byte, 2048), // Increased from 256 to 2048 for high load
		register:       make(chan *Client, 100), // Buffered channel for fast registration
		unregister:     make(chan *Client, 100), // Buffered channel for fast unregistration
		ctx:            ctx,
		cancel:         cancel,
		workerPoolSize: workerPoolSize,
	}
}

// Run starts the hub's main loop
func (h *Hub) Run() {
	defer func() {
		if r := recover(); r != nil {
			logger.Error("WebSocket hub panic recovered:", r)
			// Restart the hub loop
			go h.Run()
		}
	}()

	for {
		select {
		case <-h.ctx.Done():
			// Graceful shutdown: close all clients
			h.mu.Lock()
			for client := range h.clients {
				client.closeOnce.Do(func() {
					close(client.Send)
				})
			}
			h.clients = make(map[*Client]bool)
			h.mu.Unlock()
			logger.Info("WebSocket hub stopped gracefully")
			return

		case client := <-h.register:
			if client == nil {
				continue
			}
			h.mu.Lock()
			h.clients[client] = true
			count := len(h.clients)
			h.mu.Unlock()
			logger.Debugf("WebSocket client connected: %s (total: %d)", client.ID, count)

		case client := <-h.unregister:
			if client == nil {
				continue
			}
			h.mu.Lock()
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				client.closeOnce.Do(func() {
					close(client.Send)
				})
			}
			count := len(h.clients)
			h.mu.Unlock()
			logger.Debugf("WebSocket client disconnected: %s (total: %d)", client.ID, count)

		case message := <-h.broadcast:
			if message == nil {
				continue
			}
			// Optimization: quickly copy client list and release lock
			h.mu.RLock()
			clientCount := len(h.clients)
			if clientCount == 0 {
				h.mu.RUnlock()
				continue
			}

			// Pre-allocate memory for client list
			clients := make([]*Client, 0, clientCount)
			for client := range h.clients {
				clients = append(clients, client)
			}
			h.mu.RUnlock()

			// Parallel broadcast using worker pool
			h.broadcastParallel(clients, message)
		}
	}
}

// broadcastParallel sends message to all clients in parallel for maximum performance
func (h *Hub) broadcastParallel(clients []*Client, message []byte) {
	if len(clients) == 0 {
		return
	}

	// For small number of clients, use simple parallel sending
	if len(clients) < h.workerPoolSize {
		var wg sync.WaitGroup
		for _, client := range clients {
			wg.Add(1)
			go func(c *Client) {
				defer wg.Done()
				defer func() {
					if r := recover(); r != nil {
						// Channel may be closed, safely ignore
						logger.Debugf("WebSocket broadcast panic recovered for client %s: %v", c.ID, r)
					}
				}()
				select {
				case c.Send <- message:
				default:
					// Client's send buffer is full, disconnect
					logger.Debugf("WebSocket client %s send buffer full, disconnecting", c.ID)
					h.Unregister(c)
				}
			}(client)
		}
		wg.Wait()
		return
	}

	// For large number of clients, use worker pool for optimal performance
	clientChan := make(chan *Client, len(clients))
	for _, client := range clients {
		clientChan <- client
	}
	close(clientChan)

	// Use a local WaitGroup to avoid blocking hub shutdown
	var wg sync.WaitGroup
	wg.Add(h.workerPoolSize)
	for i := 0; i < h.workerPoolSize; i++ {
		go func() {
			defer wg.Done()
			for client := range clientChan {
				func() {
					defer func() {
						if r := recover(); r != nil {
							// Channel may be closed, safely ignore
							logger.Debugf("WebSocket broadcast panic recovered for client %s: %v", client.ID, r)
						}
					}()
					select {
					case client.Send <- message:
					default:
						// Client's send buffer is full, disconnect
						logger.Debugf("WebSocket client %s send buffer full, disconnecting", client.ID)
						h.Unregister(client)
					}
				}()
			}
		}()
	}

	// Wait for all workers to finish
	wg.Wait()
}

// Broadcast sends a message to all connected clients
func (h *Hub) Broadcast(messageType MessageType, payload any) {
	if h == nil {
		return
	}
	if payload == nil {
		logger.Warning("Attempted to broadcast nil payload")
		return
	}

	// Skip all work if no clients are connected
	if h.GetClientCount() == 0 {
		return
	}

	msg := Message{
		Type:    messageType,
		Payload: payload,
		Time:    getCurrentTimestamp(),
	}

	data, err := json.Marshal(msg)
	if err != nil {
		logger.Error("Failed to marshal WebSocket message:", err)
		return
	}

	// If message exceeds size limit, send a lightweight invalidate notification
	// instead of dropping it entirely — the frontend will re-fetch via REST API
	const maxMessageSize = 10 * 1024 * 1024 // 10MB
	if len(data) > maxMessageSize {
		logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
		h.broadcastInvalidate(messageType)
		return
	}

	// Non-blocking send with timeout to prevent delays
	select {
	case h.broadcast <- data:
	case <-time.After(100 * time.Millisecond):
		logger.Warning("WebSocket broadcast channel is full, dropping message")
	case <-h.ctx.Done():
		// Hub is shutting down
	}
}

// BroadcastToTopic sends a message only to clients subscribed to the specific topic
func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {
	if h == nil {
		return
	}
	if payload == nil {
		logger.Warning("Attempted to broadcast nil payload to topic")
		return
	}

	// Skip all work if no clients are connected
	if h.GetClientCount() == 0 {
		return
	}

	msg := Message{
		Type:    messageType,
		Payload: payload,
		Time:    getCurrentTimestamp(),
	}

	data, err := json.Marshal(msg)
	if err != nil {
		logger.Error("Failed to marshal WebSocket message:", err)
		return
	}

	// If message exceeds size limit, send a lightweight invalidate notification
	const maxMessageSize = 10 * 1024 * 1024 // 10MB
	if len(data) > maxMessageSize {
		logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
		h.broadcastInvalidate(messageType)
		return
	}

	h.mu.RLock()
	// Filter clients by topics and quickly release lock
	subscribedClients := make([]*Client, 0)
	for client := range h.clients {
		if len(client.Topics) == 0 || client.Topics[messageType] {
			subscribedClients = append(subscribedClients, client)
		}
	}
	h.mu.RUnlock()

	// Parallel send to subscribed clients
	if len(subscribedClients) > 0 {
		h.broadcastParallel(subscribedClients, data)
	}
}

// GetClientCount returns the number of connected clients
func (h *Hub) GetClientCount() int {
	h.mu.RLock()
	defer h.mu.RUnlock()
	return len(h.clients)
}

// Register registers a new client with the hub
func (h *Hub) Register(client *Client) {
	if h == nil || client == nil {
		return
	}
	select {
	case h.register <- client:
	case <-h.ctx.Done():
		// Hub is shutting down
	}
}

// Unregister unregisters a client from the hub
func (h *Hub) Unregister(client *Client) {
	if h == nil || client == nil {
		return
	}
	select {
	case h.unregister <- client:
	case <-h.ctx.Done():
		// Hub is shutting down
	}
}

// Stop gracefully stops the hub and closes all connections
func (h *Hub) Stop() {
	if h == nil {
		return
	}
	if h.cancel != nil {
		h.cancel()
	}
}

// broadcastInvalidate sends a lightweight invalidate message to all clients,
// telling them to re-fetch the specified data type via REST API.
// This is used when the full payload exceeds the WebSocket message size limit.
func (h *Hub) broadcastInvalidate(originalType MessageType) {
	msg := Message{
		Type:    MessageTypeInvalidate,
		Payload: map[string]string{"type": string(originalType)},
		Time:    getCurrentTimestamp(),
	}

	data, err := json.Marshal(msg)
	if err != nil {
		logger.Error("Failed to marshal invalidate message:", err)
		return
	}

	// Non-blocking send with timeout
	select {
	case h.broadcast <- data:
	case <-time.After(100 * time.Millisecond):
		logger.Warning("WebSocket broadcast channel is full, dropping invalidate message")
	case <-h.ctx.Done():
	}
}

// getCurrentTimestamp returns current Unix timestamp in milliseconds
func getCurrentTimestamp() int64 {
	return time.Now().UnixMilli()
}