perubahan

This commit is contained in:
meninjar
2025-10-24 12:33:10 +00:00
parent f1c2628ca8
commit 416d553a69
18 changed files with 2799 additions and 2707 deletions

View File

@@ -0,0 +1,195 @@
package websocket
import (
"time"
)
// Broadcaster mengelola broadcasting pesan
type Broadcaster struct {
hub *Hub
}
// NewBroadcaster membuat broadcaster baru
func NewBroadcaster(hub *Hub) *Broadcaster {
return &Broadcaster{hub: hub}
}
// StartServerBroadcasters memulai broadcaster server
func (b *Broadcaster) StartServerBroadcasters() {
// Heartbeat broadcaster
go b.startHeartbeatBroadcaster()
// System notification broadcaster
go b.startSystemNotificationBroadcaster()
// Data stream broadcaster
go b.startDataStreamBroadcaster()
}
// startHeartbeatBroadcaster memulai broadcaster heartbeat
func (b *Broadcaster) startHeartbeatBroadcaster() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
b.hub.mu.RLock()
connectedClients := len(b.hub.clients)
uniqueIPs := len(b.hub.clientsByIP)
staticClients := len(b.hub.clientsByStatic)
b.hub.mu.RUnlock()
b.BroadcastMessage("server_heartbeat", map[string]interface{}{
"message": "Server heartbeat",
"connected_clients": connectedClients,
"unique_ips": uniqueIPs,
"static_clients": staticClients,
"timestamp": time.Now().Unix(),
"server_id": b.hub.config.ServerID,
})
case <-b.hub.ctx.Done():
return
}
}
}
// startSystemNotificationBroadcaster memulai broadcaster notifikasi sistem
func (b *Broadcaster) startSystemNotificationBroadcaster() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
dbHealth := b.hub.dbService.Health()
b.BroadcastMessage("system_status", map[string]interface{}{
"type": "system_notification",
"database": dbHealth,
"timestamp": time.Now().Unix(),
"uptime": time.Since(b.hub.startTime).String(),
})
case <-b.hub.ctx.Done():
return
}
}
}
// startDataStreamBroadcaster memulai broadcaster aliran data
func (b *Broadcaster) startDataStreamBroadcaster() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
counter := 0
for {
select {
case <-ticker.C:
counter++
b.BroadcastMessage("data_stream", map[string]interface{}{
"id": counter,
"value": counter * 10,
"timestamp": time.Now().Unix(),
"type": "real_time_data",
})
case <-b.hub.ctx.Done():
return
}
}
}
// BroadcastMessage mengirim pesan ke semua klien
func (b *Broadcaster) BroadcastMessage(messageType string, data interface{}) {
msg := NewWebSocketMessage(MessageType(messageType), data, "", "")
select {
case b.hub.messageQueue <- msg:
default:
// Antrian penuh, abaikan pesan
}
}
// BroadcastToRoom mengirim pesan ke ruangan tertentu
func (b *Broadcaster) BroadcastToRoom(room string, messageType string, data interface{}) {
msg := NewWebSocketMessage(
MessageType(messageType),
map[string]interface{}{
"room": room,
"data": data,
},
"",
room,
)
select {
case b.hub.messageQueue <- msg:
default:
// Antrian penuh, abaikan pesan
}
}
// SendToClient mengirim pesan ke klien tertentu
func (b *Broadcaster) SendToClient(clientID string, messageType string, data interface{}) {
msg := NewWebSocketMessage(MessageType(messageType), data, clientID, "")
select {
case b.hub.messageQueue <- msg:
default:
// Antrian penuh, abaikan pesan
}
}
// SendToClientByStaticID mengirim pesan ke klien berdasarkan ID statis
func (b *Broadcaster) SendToClientByStaticID(staticID string, messageType string, data interface{}) bool {
b.hub.mu.RLock()
client, exists := b.hub.clientsByStatic[staticID]
b.hub.mu.RUnlock()
if !exists {
return false
}
b.SendToClient(client.ID, messageType, data)
return true
}
// BroadcastToIP mengirim pesan ke semua klien dari IP tertentu
func (b *Broadcaster) BroadcastToIP(ipAddress string, messageType string, data interface{}) int {
b.hub.mu.RLock()
clients := b.hub.clientsByIP[ipAddress]
b.hub.mu.RUnlock()
count := 0
for _, client := range clients {
b.SendToClient(client.ID, messageType, data)
count++
}
return count
}
// BroadcastClientToClient mengirim pesan dari satu klien ke klien lain
func (b *Broadcaster) BroadcastClientToClient(fromClientID, toClientID string, messageType string, data interface{}) {
message := NewWebSocketMessage(MessageType(messageType), map[string]interface{}{
"from_client_id": fromClientID,
"data": data,
}, toClientID, "")
b.hub.broadcast <- message
}
// BroadcastServerToClient mengirim pesan dari server ke klien tertentu
func (b *Broadcaster) BroadcastServerToClient(clientID string, messageType string, data interface{}) {
message := NewWebSocketMessage(MessageType(messageType), data, clientID, "")
b.hub.broadcast <- message
}
// BroadcastServerToRoom mengirim pesan dari server ke ruangan tertentu
func (b *Broadcaster) BroadcastServerToRoom(room string, messageType string, data interface{}) {
message := NewWebSocketMessage(MessageType(messageType), data, "", room)
b.hub.broadcast <- message
}
// BroadcastServerToAll mengirim pesan dari server ke semua klien
func (b *Broadcaster) BroadcastServerToAll(messageType string, data interface{}) {
message := NewWebSocketMessage(MessageType(messageType), data, "", "")
b.hub.broadcast <- message
}

View File

@@ -0,0 +1,728 @@
package websocket
import (
"api-service/internal/config"
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
// Client mewakili koneksi klien WebSocket
type Client struct {
// Identifikasi
ID string
StaticID string
UserID string
Room string
// Koneksi
IPAddress string
Conn *websocket.Conn
Send chan WebSocketMessage
// Hub
Hub *Hub
// Context
ctx context.Context
cancel context.CancelFunc
// Status
lastPing time.Time
lastPong time.Time
connectedAt time.Time
isActive bool
// Sinkronisasi
mu sync.RWMutex
}
// NewClient membuat klien baru
func NewClient(
id, staticID, userID, room, ipAddress string,
conn *websocket.Conn,
hub *Hub,
config config.WebSocketConfig,
) *Client {
ctx, cancel := context.WithCancel(hub.ctx)
return &Client{
ID: id,
StaticID: staticID,
UserID: userID,
Room: room,
IPAddress: ipAddress,
Conn: conn,
Send: make(chan WebSocketMessage, config.ChannelBufferSize),
Hub: hub,
ctx: ctx,
cancel: cancel,
lastPing: time.Now(),
connectedAt: time.Now(),
isActive: true,
}
}
// readPup menangani pembacaan pesan dari klien
func (c *Client) readPump() {
defer func() {
c.Hub.unregister <- c
c.Conn.Close()
}()
// Konfigurasi koneksi
c.Conn.SetReadLimit(int64(c.Hub.config.MaxMessageSize))
c.resetReadDeadline()
// Setup ping/pong handlers
c.Conn.SetPingHandler(func(message string) error {
c.resetReadDeadline()
return c.Conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(c.Hub.config.WriteTimeout))
})
c.Conn.SetPongHandler(func(message string) error {
c.mu.Lock()
c.lastPong = time.Now()
c.isActive = true
c.mu.Unlock()
c.resetReadDeadline()
return nil
})
for {
select {
case <-c.ctx.Done():
return
default:
_, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway,
websocket.CloseAbnormalClosure,
websocket.CloseNormalClosure) {
c.Hub.errorCount++
}
return
}
// Reset deadline setiap kali ada pesan masuk
c.resetReadDeadline()
c.updateLastActivity()
// Parse pesan
var msg WebSocketMessage
if err := json.Unmarshal(message, &msg); err != nil {
c.sendErrorResponse("Invalid message format", err.Error())
continue
}
// Set metadata pesan
msg.Timestamp = time.Now()
msg.ClientID = c.ID
if msg.MessageID == "" {
msg.MessageID = uuid.New().String()
}
// Proses pesan menggunakan registry
c.handleMessage(msg)
}
}
}
// writePump menangani pengiriman pesan ke klien
func (c *Client) writePump() {
ticker := time.NewTicker(c.Hub.config.PingInterval)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.Conn.WriteJSON(message); err != nil {
c.Hub.errorCount++
return
}
case <-ticker.C:
// Kirim ping dan periksa respons
if err := c.sendPing(); err != nil {
c.Hub.errorCount++
return
}
// Periksa timeout pong
if c.isPongTimeout() {
return
}
case <-c.ctx.Done():
return
}
}
}
// handleMessage memproses pesan masuk
func (c *Client) handleMessage(msg WebSocketMessage) {
// Dapatkan handler dari registry
handler, exists := c.Hub.messageRegistry.GetHandler(msg.Type)
if !exists {
// Handler tidak ditemukan, gunakan handler default
c.handleDefaultMessage(msg)
return
}
// Jalankan handler
if err := handler.HandleMessage(c, msg); err != nil {
c.sendErrorResponse("Error handling message", err.Error())
c.Hub.errorCount++
}
}
// handleDefaultMessage menangani pesan tanpa handler khusus
func (c *Client) handleDefaultMessage(msg WebSocketMessage) {
switch msg.Type {
case "broadcast":
c.Hub.broadcast <- msg
c.sendDirectResponse("broadcast_sent", "Message broadcasted to all clients")
case "direct_message":
c.handleDirectMessage(msg)
case "ping":
c.handlePing(msg)
case "pong":
// Pong sudah ditangani di level koneksi
break
case "heartbeat":
c.handleHeartbeat(msg)
case "connection_test":
c.handleConnectionTest(msg)
case "get_online_users":
c.sendOnlineUsers()
case "join_room":
c.handleJoinRoom(msg)
case "leave_room":
c.handleLeaveRoom(msg)
case "get_room_info":
c.handleGetRoomInfo(msg)
case "database_query", "db_query":
c.handleDatabaseQuery(msg)
case "db_insert":
c.handleDatabaseInsert(msg)
case "db_custom_query":
c.handleDatabaseCustomQuery(msg)
case "get_stats":
c.handleGetStats(msg)
case "get_server_stats":
c.handleGetServerStats(msg)
case "get_system_health":
c.handleGetSystemHealth(msg)
case "admin_kick_client":
c.handleAdminKickClient(msg)
case "admin_kill_server":
c.handleAdminKillServer(msg)
case "admin_clear_logs":
c.handleAdminClearLogs(msg)
default:
c.sendDirectResponse("message_received", fmt.Sprintf("Message received: %v", msg.Data))
c.Hub.broadcast <- msg
}
}
// Metode helper lainnya...
func (c *Client) resetReadDeadline() {
c.Conn.SetReadDeadline(time.Now().Add(c.Hub.config.ReadTimeout))
}
func (c *Client) updateLastActivity() {
c.mu.Lock()
defer c.mu.Unlock()
c.lastPing = time.Now()
c.isActive = true
}
func (c *Client) sendPing() error {
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return err
}
c.mu.Lock()
c.lastPing = time.Now()
c.mu.Unlock()
return nil
}
func (c *Client) isPongTimeout() bool {
c.mu.RLock()
defer c.mu.RUnlock()
lastActivity := c.lastPong
if lastActivity.IsZero() {
lastActivity = c.lastPing
}
return time.Since(lastActivity) > c.Hub.config.PongTimeout
}
func (c *Client) isClientActive() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.isActive && time.Since(c.lastPing) < c.Hub.config.PongTimeout
}
func (c *Client) sendDirectResponse(messageType string, data interface{}) {
response := NewWebSocketMessage(MessageType(messageType), data, c.ID, c.Room)
select {
case c.Send <- response:
default:
// Channel penuh, abaikan pesan
}
}
func (c *Client) sendErrorResponse(error, details string) {
c.sendDirectResponse("error", map[string]interface{}{
"error": error,
"details": details,
})
}
func (c *Client) gracefulClose() {
c.mu.Lock()
c.isActive = false
c.mu.Unlock()
// Kirim pesan close
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
// Batalkan context
c.cancel()
}
// ReadPump mengekspos metode readPump untuk handler
func (c *Client) ReadPump() {
c.readPump()
}
// WritePump mengekspos metode writePump untuk handler
func (c *Client) WritePump() {
c.writePump()
}
// handleDirectMessage menangani pesan direct message
func (c *Client) handleDirectMessage(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid direct message format", "Data must be an object")
return
}
targetClientID, ok := data["target_client_id"].(string)
if !ok || targetClientID == "" {
c.sendErrorResponse("Invalid target client ID", "target_client_id is required")
return
}
message, ok := data["message"].(string)
if !ok || message == "" {
c.sendErrorResponse("Invalid message content", "message is required")
return
}
// Buat pesan direct message
directMsg := NewWebSocketMessage(DirectMessage, map[string]interface{}{
"from_client_id": c.ID,
"from_user_id": c.UserID,
"message": message,
"timestamp": msg.Timestamp,
}, targetClientID, "")
// Kirim ke target client
select {
case c.Hub.broadcast <- directMsg:
c.sendDirectResponse("direct_message_sent", map[string]interface{}{
"target_client_id": targetClientID,
"message": message,
})
default:
c.sendErrorResponse("Failed to send direct message", "Message queue is full")
}
}
// handlePing menangani ping message
func (c *Client) handlePing(msg WebSocketMessage) {
// Kirim pong response
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
"timestamp": msg.Timestamp.Unix(),
"client_id": c.ID,
}, c.ID, "")
select {
case c.Send <- pongMsg:
default:
// Channel penuh, abaikan
}
}
// handleHeartbeat menangani heartbeat message
func (c *Client) handleHeartbeat(msg WebSocketMessage) {
// Kirim heartbeat acknowledgment
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_uptime": time.Since(c.connectedAt).Seconds(),
"server_uptime": time.Since(c.Hub.startTime).Seconds(),
"client_id": c.ID,
}, c.ID, "")
select {
case c.Send <- heartbeatAck:
default:
// Channel penuh, abaikan
}
}
// handleConnectionTest menangani connection test
func (c *Client) handleConnectionTest(msg WebSocketMessage) {
// Kirim connection test result
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_id": c.ID,
"connection_status": "healthy",
"latency_ms": 0, // Could be calculated if ping timestamp is provided
"uptime_seconds": time.Since(c.connectedAt).Seconds(),
}, c.ID, "")
select {
case c.Send <- testResult:
default:
// Channel penuh, abaikan
}
}
// handleJoinRoom menangani join room
func (c *Client) handleJoinRoom(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid join room format", "Data must be an object")
return
}
roomName, ok := data["room"].(string)
if !ok || roomName == "" {
c.sendErrorResponse("Invalid room name", "room is required")
return
}
// Update client room
oldRoom := c.Room
c.Room = roomName
// Update hub room mapping
c.Hub.mu.Lock()
if oldRoom != "" {
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
delete(roomClients, c)
if len(roomClients) == 0 {
delete(c.Hub.rooms, oldRoom)
}
}
}
if c.Hub.rooms[roomName] == nil {
c.Hub.rooms[roomName] = make(map[*Client]bool)
}
c.Hub.rooms[roomName][c] = true
c.Hub.mu.Unlock()
// Log activity
c.Hub.logActivity("client_join_room", c.ID, fmt.Sprintf("Room: %s", roomName))
// Kirim response
c.sendDirectResponse("room_joined", map[string]interface{}{
"room": roomName,
"previous_room": oldRoom,
})
}
// handleLeaveRoom menangani leave room
func (c *Client) handleLeaveRoom(msg WebSocketMessage) {
oldRoom := c.Room
if oldRoom == "" {
c.sendErrorResponse("Not in any room", "Client is not currently in a room")
return
}
// Update hub room mapping
c.Hub.mu.Lock()
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
delete(roomClients, c)
if len(roomClients) == 0 {
delete(c.Hub.rooms, oldRoom)
}
}
c.Hub.mu.Unlock()
// Clear client room
c.Room = ""
// Log activity
c.Hub.logActivity("client_leave_room", c.ID, fmt.Sprintf("Room: %s", oldRoom))
// Kirim response
c.sendDirectResponse("room_left", map[string]interface{}{
"room": oldRoom,
})
}
// handleGetRoomInfo menangani get room info
func (c *Client) handleGetRoomInfo(msg WebSocketMessage) {
c.Hub.mu.RLock()
defer c.Hub.mu.RUnlock()
roomInfo := make(map[string]interface{})
// Info ruangan saat ini
if c.Room != "" {
if roomClients, exists := c.Hub.rooms[c.Room]; exists {
clientIDs := make([]string, 0, len(roomClients))
for client := range roomClients {
clientIDs = append(clientIDs, client.ID)
}
roomInfo["current_room"] = map[string]interface{}{
"name": c.Room,
"client_count": len(roomClients),
"clients": clientIDs,
}
}
}
// Info semua ruangan
allRooms := make(map[string]int)
for roomName, clients := range c.Hub.rooms {
allRooms[roomName] = len(clients)
}
roomInfo["all_rooms"] = allRooms
roomInfo["total_rooms"] = len(c.Hub.rooms)
c.sendDirectResponse("room_info", roomInfo)
}
// handleDatabaseInsert menangani database insert
func (c *Client) handleDatabaseInsert(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database insert format", "Data must be an object")
return
}
table, ok := data["table"].(string)
if !ok || table == "" {
c.sendErrorResponse("Invalid table name", "table is required")
return
}
insertData, ok := data["data"].(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid insert data", "data must be an object")
return
}
// For now, just acknowledge the insert request
// In a real implementation, you would perform the actual database insert
c.sendDirectResponse("db_insert_result", map[string]interface{}{
"table": table,
"status": "acknowledged",
"message": "Insert request received (not implemented)",
"data": insertData, // Use the variable to avoid unused error
})
}
// handleDatabaseCustomQuery menangani custom database query
func (c *Client) handleDatabaseCustomQuery(msg WebSocketMessage) {
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database query format", "Data must be an object")
return
}
database, ok := data["database"].(string)
if !ok || database == "" {
database = "default"
}
query, ok := data["query"].(string)
if !ok || query == "" {
c.sendErrorResponse("Invalid query", "query is required")
return
}
// For now, just acknowledge the query request
// In a real implementation, you would execute the query
c.sendDirectResponse("db_query_result", map[string]interface{}{
"database": database,
"query": query,
"status": "acknowledged",
"message": "Query request received (not implemented)",
})
}
// handleGetStats menangani get stats
func (c *Client) handleGetStats(msg WebSocketMessage) {
stats := c.Hub.GetStats()
c.sendDirectResponse("stats", stats)
}
// handleGetServerStats menangani get server stats
func (c *Client) handleGetServerStats(msg WebSocketMessage) {
// Create monitoring manager instance
monitoringManager := NewMonitoringManager(c.Hub)
detailedStats := monitoringManager.GetDetailedStats()
c.sendDirectResponse("server_stats", detailedStats)
}
// handleGetSystemHealth menangani get system health
func (c *Client) handleGetSystemHealth(msg WebSocketMessage) {
systemHealth := make(map[string]interface{})
if c.Hub.dbService != nil {
systemHealth["databases"] = c.Hub.dbService.Health()
systemHealth["available_dbs"] = c.Hub.dbService.ListDBs()
}
systemHealth["websocket_status"] = "healthy"
systemHealth["uptime_seconds"] = time.Since(c.Hub.startTime).Seconds()
c.sendDirectResponse("system_health", systemHealth)
}
// handleAdminKickClient menangani admin kick client
func (c *Client) handleAdminKickClient(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid admin command format", "Data must be an object")
return
}
targetClientID, ok := data["client_id"].(string)
if !ok || targetClientID == "" {
c.sendErrorResponse("Invalid target client ID", "client_id is required")
return
}
// For now, just acknowledge the kick request
// In a real implementation, you would check admin permissions and kick the client
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kick_client",
"target_client_id": targetClientID,
"status": "acknowledged",
"message": "Kick request received (not implemented)",
})
}
// handleAdminKillServer menangani admin kill server
func (c *Client) handleAdminKillServer(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kill_server",
"status": "acknowledged",
"message": "Kill server request received (not implemented - would require admin auth)",
})
}
// handleAdminClearLogs menangani admin clear logs
func (c *Client) handleAdminClearLogs(msg WebSocketMessage) {
// This should be protected by authentication, but for now just acknowledge
c.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "clear_logs",
"status": "acknowledged",
"message": "Clear logs request received (not implemented - would require admin auth)",
})
}
// Implementasi metode lainnya...
func (c *Client) handleDatabaseQuery(msg WebSocketMessage) {
// Implementasi yang sama seperti sebelumnya
data, ok := msg.Data.(map[string]interface{})
if !ok {
c.sendErrorResponse("Invalid database query format", "Data must be an object")
return
}
table, ok := data["table"].(string)
if !ok || table == "" {
c.sendErrorResponse("Invalid table name", "table is required")
return
}
// For now, just acknowledge the query request
c.sendDirectResponse("db_query_result", map[string]interface{}{
"table": table,
"status": "acknowledged",
"message": "Query request received (not implemented)",
})
}
func (c *Client) sendOnlineUsers() {
c.Hub.mu.RLock()
defer c.Hub.mu.RUnlock()
users := make([]map[string]interface{}, 0, len(c.Hub.clients))
for client := range c.Hub.clients {
user := map[string]interface{}{
"id": client.ID,
"static_id": client.StaticID,
"user_id": client.UserID,
"room": client.Room,
"ip_address": client.IPAddress,
"connected_at": client.connectedAt,
"last_ping": client.lastPing,
"last_pong": client.lastPong,
"is_active": client.isClientActive(),
}
users = append(users, user)
}
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
"users": users,
"count": len(users),
}, c.ID, "")
select {
case c.Send <- response:
default:
// Channel penuh, abaikan
}
}

View File

@@ -0,0 +1,621 @@
package websocket
import (
"context"
"fmt"
"strings"
"time"
)
// DatabaseHandler menangani operasi database
type DatabaseHandler struct {
hub *Hub
}
func NewDatabaseHandler(hub *Hub) *DatabaseHandler {
return &DatabaseHandler{hub: hub}
}
func (h *DatabaseHandler) HandleMessage(client *Client, message WebSocketMessage) error {
switch message.Type {
case DatabaseInsertMessage:
return h.handleDatabaseInsert(client, message)
case DatabaseQueryMessage:
return h.handleDatabaseQuery(client, message)
case DatabaseCustomQueryMessage:
return h.handleDatabaseCustomQuery(client, message)
default:
return fmt.Errorf("unsupported database message type: %s", message.Type)
}
}
func (h *DatabaseHandler) MessageType() MessageType {
return DatabaseInsertMessage // Primary type for registration
}
func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocketMessage) error {
data, ok := message.Data.(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid database insert format", "Data must be an object")
return nil
}
table, ok := data["table"].(string)
if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required")
return nil
}
insertData, ok := data["data"].(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid insert data", "data must be an object")
return nil
}
// Perform actual database insert
if h.hub.dbService != nil {
// Get database connection
db, err := h.hub.GetDatabaseConnection("postgres_satudata")
if err != nil {
client.sendErrorResponse("Database connection error", err.Error())
return nil
}
// Build insert query
columns := make([]string, 0, len(insertData))
values := make([]interface{}, 0, len(insertData))
placeholders := make([]string, 0, len(insertData))
i := 1
for col, val := range insertData {
columns = append(columns, col)
values = append(values, val)
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
i++
}
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
table,
strings.Join(columns, ", "),
strings.Join(placeholders, ", "))
// Execute insert
ctx, cancel := context.WithTimeout(client.ctx, 30*time.Second)
defer cancel()
result, err := db.ExecContext(ctx, query, values...)
if err != nil {
client.sendErrorResponse("Database insert error", err.Error())
return nil
}
rowsAffected, _ := result.RowsAffected()
client.sendDirectResponse("db_insert_result", map[string]interface{}{
"table": table,
"status": "success",
"rows_affected": rowsAffected,
"message": "Data inserted successfully",
})
} else {
client.sendErrorResponse("Database service not available", "Database service is not initialized")
}
return nil
}
func (h *DatabaseHandler) handleDatabaseQuery(client *Client, message WebSocketMessage) error {
data, ok := message.Data.(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid database query format", "Data must be an object")
return nil
}
table, ok := data["table"].(string)
if !ok || table == "" {
client.sendErrorResponse("Invalid table name", "table is required")
return nil
}
// Execute query
results, err := h.hub.ExecuteDatabaseQuery("postgres_satudata", fmt.Sprintf("SELECT * FROM %s LIMIT 100", table))
if err != nil {
client.sendErrorResponse("Database query error", err.Error())
return nil
}
client.sendDirectResponse("db_query_result", map[string]interface{}{
"table": table,
"status": "success",
"results": results,
"count": len(results),
})
return nil
}
func (h *DatabaseHandler) handleDatabaseCustomQuery(client *Client, message WebSocketMessage) error {
data, ok := message.Data.(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid database query format", "Data must be an object")
return nil
}
database, ok := data["database"].(string)
if !ok || database == "" {
database = "postgres_satudata"
}
query, ok := data["query"].(string)
if !ok || query == "" {
client.sendErrorResponse("Invalid query", "query is required")
return nil
}
// Execute custom query
results, err := h.hub.ExecuteDatabaseQuery(database, query)
if err != nil {
client.sendErrorResponse("Database query error", err.Error())
return nil
}
client.sendDirectResponse("db_query_result", map[string]interface{}{
"database": database,
"query": query,
"status": "success",
"results": results,
"count": len(results),
})
return nil
}
// AdminHandler menangani operasi admin
type AdminHandler struct {
hub *Hub
}
func NewAdminHandler(hub *Hub) *AdminHandler {
return &AdminHandler{hub: hub}
}
func (h *AdminHandler) HandleMessage(client *Client, message WebSocketMessage) error {
switch message.Type {
case AdminKickClientMessage:
return h.handleAdminKickClient(client, message)
case AdminKillServerMessage:
return h.handleAdminKillServer(client, message)
case AdminClearLogsMessage:
return h.handleAdminClearLogs(client, message)
default:
return fmt.Errorf("unsupported admin message type: %s", message.Type)
}
}
func (h *AdminHandler) MessageType() MessageType {
return AdminKickClientMessage // Primary type for registration
}
func (h *AdminHandler) handleAdminKickClient(client *Client, message WebSocketMessage) error {
data, ok := message.Data.(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid admin command format", "Data must be an object")
return nil
}
targetClientID, ok := data["client_id"].(string)
if !ok || targetClientID == "" {
client.sendErrorResponse("Invalid target client ID", "client_id is required")
return nil
}
// Check if target client exists
h.hub.mu.RLock()
targetClient, exists := h.hub.clientsByID[targetClientID]
h.hub.mu.RUnlock()
if !exists {
client.sendErrorResponse("Client not found", fmt.Sprintf("Client %s not found", targetClientID))
return nil
}
// Log activity
h.hub.logActivity("admin_kick_client", client.ID, fmt.Sprintf("Kicked client: %s", targetClientID))
// Disconnect the target client
targetClient.cancel()
targetClient.Conn.Close()
client.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kick_client",
"target_client_id": targetClientID,
"status": "success",
"message": "Client kicked successfully",
})
return nil
}
func (h *AdminHandler) handleAdminKillServer(client *Client, message WebSocketMessage) error {
// Log activity
h.hub.logActivity("admin_kill_server", client.ID, "Server kill signal received")
// For testing purposes, just acknowledge (don't actually kill server)
client.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "kill_server",
"status": "acknowledged",
"message": "Kill server signal received (test mode - server not actually killed)",
})
return nil
}
func (h *AdminHandler) handleAdminClearLogs(client *Client, message WebSocketMessage) error {
// Clear activity logs
h.hub.activityMu.Lock()
h.hub.activityLog = make([]ActivityLog, 0, h.hub.config.ActivityLogSize)
h.hub.activityMu.Unlock()
// Log the clear action
h.hub.logActivity("admin_clear_logs", client.ID, "Activity logs cleared")
client.sendDirectResponse("admin_command_result", map[string]interface{}{
"command": "clear_logs",
"status": "success",
"message": "Server logs cleared successfully",
})
return nil
}
// RoomHandler menangani operasi ruangan
type RoomHandler struct {
hub *Hub
}
func NewRoomHandler(hub *Hub) *RoomHandler {
return &RoomHandler{hub: hub}
}
func (h *RoomHandler) HandleMessage(client *Client, message WebSocketMessage) error {
switch message.Type {
case JoinRoomMessage:
return h.handleJoinRoom(client, message)
case LeaveRoomMessage:
return h.handleLeaveRoom(client, message)
case GetRoomInfoMessage:
return h.handleGetRoomInfo(client, message)
default:
return fmt.Errorf("unsupported room message type: %s", message.Type)
}
}
func (h *RoomHandler) MessageType() MessageType {
return JoinRoomMessage // Primary type for registration
}
func (h *RoomHandler) handleJoinRoom(client *Client, message WebSocketMessage) error {
data, ok := message.Data.(map[string]interface{})
if !ok {
client.sendErrorResponse("Invalid join room format", "Data must be an object")
return nil
}
roomName, ok := data["room"].(string)
if !ok || roomName == "" {
client.sendErrorResponse("Invalid room name", "room is required")
return nil
}
// Update client room
oldRoom := client.Room
client.Room = roomName
// Update hub room mapping
h.hub.mu.Lock()
if oldRoom != "" {
if roomClients, exists := h.hub.rooms[oldRoom]; exists {
delete(roomClients, client)
if len(roomClients) == 0 {
delete(h.hub.rooms, oldRoom)
}
}
}
if h.hub.rooms[roomName] == nil {
h.hub.rooms[roomName] = make(map[*Client]bool)
}
h.hub.rooms[roomName][client] = true
h.hub.mu.Unlock()
// Log activity
h.hub.logActivity("client_join_room", client.ID, fmt.Sprintf("Room: %s", roomName))
// Notify other clients in the room
h.hub.broadcaster.BroadcastServerToRoom(roomName, "user_joined_room", map[string]interface{}{
"client_id": client.ID,
"user_id": client.UserID,
"room": roomName,
"joined_at": time.Now().Unix(),
})
// Send response
client.sendDirectResponse("room_joined", map[string]interface{}{
"room": roomName,
"previous_room": oldRoom,
})
return nil
}
func (h *RoomHandler) handleLeaveRoom(client *Client, message WebSocketMessage) error {
oldRoom := client.Room
if oldRoom == "" {
client.sendErrorResponse("Not in any room", "Client is not currently in a room")
return nil
}
// Update hub room mapping
h.hub.mu.Lock()
if roomClients, exists := h.hub.rooms[oldRoom]; exists {
delete(roomClients, client)
if len(roomClients) == 0 {
delete(h.hub.rooms, oldRoom)
}
}
h.hub.mu.Unlock()
// Clear client room
client.Room = ""
// Log activity
h.hub.logActivity("client_leave_room", client.ID, fmt.Sprintf("Room: %s", oldRoom))
// Notify other clients in the room
h.hub.broadcaster.BroadcastServerToRoom(oldRoom, "user_left_room", map[string]interface{}{
"client_id": client.ID,
"user_id": client.UserID,
"room": oldRoom,
"left_at": time.Now().Unix(),
})
// Send response
client.sendDirectResponse("room_left", map[string]interface{}{
"room": oldRoom,
})
return nil
}
func (h *RoomHandler) handleGetRoomInfo(client *Client, message WebSocketMessage) error {
h.hub.mu.RLock()
defer h.hub.mu.RUnlock()
roomInfo := make(map[string]interface{})
// Info ruangan saat ini
if client.Room != "" {
if roomClients, exists := h.hub.rooms[client.Room]; exists {
clientIDs := make([]string, 0, len(roomClients))
for client := range roomClients {
clientIDs = append(clientIDs, client.ID)
}
roomInfo["current_room"] = map[string]interface{}{
"name": client.Room,
"client_count": len(roomClients),
"clients": clientIDs,
}
}
}
// Info semua ruangan
allRooms := make(map[string]int)
for roomName, clients := range h.hub.rooms {
allRooms[roomName] = len(clients)
}
roomInfo["all_rooms"] = allRooms
roomInfo["total_rooms"] = len(h.hub.rooms)
client.sendDirectResponse("room_info", roomInfo)
return nil
}
// MonitoringHandler menangani operasi monitoring
type MonitoringHandler struct {
hub *Hub
}
func NewMonitoringHandler(hub *Hub) *MonitoringHandler {
return &MonitoringHandler{hub: hub}
}
func (h *MonitoringHandler) HandleMessage(client *Client, message WebSocketMessage) error {
switch message.Type {
case GetStatsMessage:
return h.handleGetStats(client, message)
case GetServerStatsMessage:
return h.handleGetServerStats(client, message)
case GetSystemHealthMessage:
return h.handleGetSystemHealth(client, message)
default:
return fmt.Errorf("unsupported monitoring message type: %s", message.Type)
}
}
func (h *MonitoringHandler) MessageType() MessageType {
return GetStatsMessage // Primary type for registration
}
func (h *MonitoringHandler) handleGetStats(client *Client, message WebSocketMessage) error {
stats := h.hub.GetStats()
client.sendDirectResponse("stats", stats)
return nil
}
func (h *MonitoringHandler) handleGetServerStats(client *Client, message WebSocketMessage) error {
// Create monitoring manager instance
monitoringManager := NewMonitoringManager(h.hub)
detailedStats := monitoringManager.GetDetailedStats()
client.sendDirectResponse("server_stats", detailedStats)
return nil
}
func (h *MonitoringHandler) handleGetSystemHealth(client *Client, message WebSocketMessage) error {
systemHealth := make(map[string]interface{})
if h.hub.dbService != nil {
systemHealth["databases"] = h.hub.dbService.Health()
systemHealth["available_dbs"] = h.hub.dbService.ListDBs()
}
systemHealth["websocket_status"] = "healthy"
systemHealth["uptime_seconds"] = time.Since(h.hub.startTime).Seconds()
client.sendDirectResponse("system_health", systemHealth)
return nil
}
// ConnectionHandler menangani operasi koneksi
type ConnectionHandler struct {
hub *Hub
}
func NewConnectionHandler(hub *Hub) *ConnectionHandler {
return &ConnectionHandler{hub: hub}
}
func (h *ConnectionHandler) HandleMessage(client *Client, message WebSocketMessage) error {
switch message.Type {
case PingMessage:
return h.handlePing(client, message)
case PongMessage:
return h.handlePong(client, message)
case HeartbeatMessage:
return h.handleHeartbeat(client, message)
case ConnectionTestMessage:
return h.handleConnectionTest(client, message)
case OnlineUsersMessage:
return h.handleGetOnlineUsers(client, message)
default:
return fmt.Errorf("unsupported connection message type: %s", message.Type)
}
}
func (h *ConnectionHandler) MessageType() MessageType {
return PingMessage // Primary type for registration
}
func (h *ConnectionHandler) handlePing(client *Client, message WebSocketMessage) error {
// Send pong response
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
"timestamp": message.Timestamp.Unix(),
"client_id": client.ID,
}, client.ID, "")
select {
case client.Send <- pongMsg:
default:
// Channel penuh, abaikan
}
return nil
}
func (h *ConnectionHandler) handlePong(client *Client, message WebSocketMessage) error {
// Pong sudah ditangani di level koneksi, tapi kita bisa log aktivitas
h.hub.logActivity("pong_received", client.ID, "Pong message received")
return nil
}
func (h *ConnectionHandler) handleHeartbeat(client *Client, message WebSocketMessage) error {
// Send heartbeat acknowledgment
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_uptime": time.Since(client.connectedAt).Seconds(),
"server_uptime": time.Since(h.hub.startTime).Seconds(),
"client_id": client.ID,
}, client.ID, "")
select {
case client.Send <- heartbeatAck:
default:
// Channel penuh, abaikan
}
return nil
}
func (h *ConnectionHandler) handleConnectionTest(client *Client, message WebSocketMessage) error {
// Send connection test result
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
"timestamp": time.Now().Unix(),
"client_id": client.ID,
"connection_status": "healthy",
"latency_ms": 0, // Could be calculated if ping timestamp is provided
"uptime_seconds": time.Since(client.connectedAt).Seconds(),
}, client.ID, "")
select {
case client.Send <- testResult:
default:
// Channel penuh, abaikan
}
return nil
}
func (h *ConnectionHandler) handleGetOnlineUsers(client *Client, message WebSocketMessage) error {
h.hub.mu.RLock()
defer h.hub.mu.RUnlock()
users := make([]map[string]interface{}, 0, len(h.hub.clients))
for client := range h.hub.clients {
user := map[string]interface{}{
"id": client.ID,
"static_id": client.StaticID,
"user_id": client.UserID,
"room": client.Room,
"ip_address": client.IPAddress,
"connected_at": client.connectedAt,
"last_ping": client.lastPing,
"last_pong": client.lastPong,
"is_active": client.isClientActive(),
}
users = append(users, user)
}
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
"users": users,
"count": len(users),
}, client.ID, "")
select {
case client.Send <- response:
default:
// Channel penuh, abaikan
}
return nil
}
// SetupDefaultHandlers sets up all default message handlers
func SetupDefaultHandlers(registry *MessageRegistry, hub *Hub) {
// Register database handler
dbHandler := NewDatabaseHandler(hub)
registry.RegisterHandler(dbHandler)
// Register admin handler
adminHandler := NewAdminHandler(hub)
registry.RegisterHandler(adminHandler)
// Register room handler
roomHandler := NewRoomHandler(hub)
registry.RegisterHandler(roomHandler)
// Register monitoring handler
monitoringHandler := NewMonitoringHandler(hub)
registry.RegisterHandler(monitoringHandler)
// Register connection handler
connectionHandler := NewConnectionHandler(hub)
registry.RegisterHandler(connectionHandler)
}

View File

@@ -0,0 +1,444 @@
package websocket
import (
"api-service/internal/config"
"api-service/internal/database"
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
)
// Hub mengelola semua klien WebSocket
type Hub struct {
// Konfigurasi
config config.WebSocketConfig
// Klien
clients map[*Client]bool
clientsByID map[string]*Client
clientsByIP map[string][]*Client
clientsByStatic map[string]*Client
// Ruangan
rooms map[string]map[*Client]bool
// Channel komunikasi
broadcast chan WebSocketMessage
register chan *Client
unregister chan *Client
messageQueue chan WebSocketMessage
// Sinkronisasi
mu sync.RWMutex
// Context
ctx context.Context
cancel context.CancelFunc
// Layanan eksternal
dbService database.Service
// Monitoring
startTime time.Time
messageCount int64
errorCount int64
activityLog []ActivityLog
activityMu sync.RWMutex
// Registry handler pesan
messageRegistry *MessageRegistry
broadcaster *Broadcaster
}
// ActivityLog menyimpan log aktivitas
type ActivityLog struct {
Timestamp time.Time `json:"timestamp"`
Event string `json:"event"`
ClientID string `json:"client_id"`
Details string `json:"details"`
}
// DatabaseService mendefinisikan interface untuk layanan database
type DatabaseService interface {
Health() map[string]interface{}
ListDBs() []string
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(channel, payload string)) error
NotifyChange(dbName, channel, payload string) error
GetDB(name string) (*sql.DB, error)
GetPrimaryDB(name string) (*sql.DB, error)
}
// Global database service instance
var (
dbService database.Service
once sync.Once
)
// Initialize the database connection
func init() {
once.Do(func() {
dbService = database.New(config.LoadConfig())
if dbService == nil {
panic("Failed to initialize database connection")
}
})
}
// NewHub membuat hub baru dengan konfigurasi yang diberikan
func NewHub(config config.WebSocketConfig) *Hub {
ctx, cancel := context.WithCancel(context.Background())
hub := &Hub{
config: config,
clients: make(map[*Client]bool),
clientsByID: make(map[string]*Client),
clientsByIP: make(map[string][]*Client),
clientsByStatic: make(map[string]*Client),
rooms: make(map[string]map[*Client]bool),
broadcast: make(chan WebSocketMessage, 1000),
register: make(chan *Client),
unregister: make(chan *Client),
messageQueue: make(chan WebSocketMessage, config.MessageQueueSize),
ctx: ctx,
cancel: cancel,
dbService: dbService,
startTime: time.Now(),
activityLog: make([]ActivityLog, 0, config.ActivityLogSize),
messageRegistry: NewMessageRegistry(),
}
// Setup default message handlers
// SetupDefaultHandlers(hub.messageRegistry)
// Setup database change listeners
hub.setupDatabaseListeners()
return hub
}
// Run menjalankan loop utama hub
func (h *Hub) Run() {
// Start queue workers
for i := 0; i < h.config.QueueWorkers; i++ {
go h.queueWorker(i)
}
for {
select {
case client := <-h.register:
h.registerClient(client)
case client := <-h.unregister:
h.unregisterClient(client)
case message := <-h.broadcast:
h.messageCount++
h.broadcastToClients(message)
case <-h.ctx.Done():
return
}
}
}
// queueWorker memproses pesan dari antrian
func (h *Hub) queueWorker(workerID int) {
for {
select {
case message := <-h.messageQueue:
h.broadcast <- message
case <-h.ctx.Done():
return
}
}
}
// registerClient mendaftarkan klien baru
func (h *Hub) registerClient(client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
// Register di peta klien utama
h.clients[client] = true
// Register berdasarkan ID
h.clientsByID[client.ID] = client
// Register berdasarkan ID statis
if client.StaticID != "" {
h.clientsByStatic[client.StaticID] = client
}
// Register berdasarkan IP
if h.clientsByIP[client.IPAddress] == nil {
h.clientsByIP[client.IPAddress] = make([]*Client, 0)
}
h.clientsByIP[client.IPAddress] = append(h.clientsByIP[client.IPAddress], client)
// Register di ruangan
if client.Room != "" {
if h.rooms[client.Room] == nil {
h.rooms[client.Room] = make(map[*Client]bool)
}
h.rooms[client.Room][client] = true
}
// Log aktivitas
h.logActivity("client_connected", client.ID,
fmt.Sprintf("IP: %s, Static: %s, Room: %s", client.IPAddress, client.StaticID, client.Room))
}
// unregisterClient menghapus klien
func (h *Hub) unregisterClient(client *Client) {
h.mu.Lock()
defer h.mu.Unlock()
if _, ok := h.clients[client]; ok {
// Hapus dari peta klien utama
delete(h.clients, client)
close(client.Send)
// Hapus dari peta berdasarkan ID
delete(h.clientsByID, client.ID)
// Hapus dari peta berdasarkan ID statis
if client.StaticID != "" {
delete(h.clientsByStatic, client.StaticID)
}
// Hapus dari peta berdasarkan IP
if ipClients, exists := h.clientsByIP[client.IPAddress]; exists {
for i, c := range ipClients {
if c == client {
h.clientsByIP[client.IPAddress] = append(ipClients[:i], ipClients[i+1:]...)
break
}
}
// Jika tidak ada lagi klien dari IP ini, hapus entri IP
if len(h.clientsByIP[client.IPAddress]) == 0 {
delete(h.clientsByIP, client.IPAddress)
}
}
// Hapus dari ruangan
if client.Room != "" {
if room, exists := h.rooms[client.Room]; exists {
delete(room, client)
if len(room) == 0 {
delete(h.rooms, client.Room)
}
}
}
}
// Log aktivitas
h.logActivity("client_disconnected", client.ID,
fmt.Sprintf("IP: %s, Duration: %v", client.IPAddress, time.Since(client.connectedAt)))
client.cancel()
}
// broadcastToClients mengirim pesan ke klien yang sesuai
func (h *Hub) broadcastToClients(message WebSocketMessage) {
h.mu.RLock()
defer h.mu.RUnlock()
if message.ClientID != "" {
// Kirim ke klien tertentu
if client, exists := h.clientsByID[message.ClientID]; exists {
select {
case client.Send <- message:
default:
go func() {
h.unregister <- client
}()
}
}
return
}
// Periksa apakah ini pesan ruangan
if message.Room != "" {
if room, roomExists := h.rooms[message.Room]; roomExists {
for client := range room {
select {
case client.Send <- message:
default:
go func(c *Client) {
h.unregister <- c
}(client)
}
}
}
return
}
// Broadcast ke semua klien
for client := range h.clients {
select {
case client.Send <- message:
default:
go func(c *Client) {
h.unregister <- c
}(client)
}
}
}
// logActivity mencatat aktivitas
func (h *Hub) logActivity(event, clientID, details string) {
h.activityMu.Lock()
defer h.activityMu.Unlock()
activity := ActivityLog{
Timestamp: time.Now(),
Event: event,
ClientID: clientID,
Details: details,
}
h.activityLog = append(h.activityLog, activity)
// Pertahankan hanya aktivitas terakhir sesuai konfigurasi
if len(h.activityLog) > h.config.ActivityLogSize {
h.activityLog = h.activityLog[1:]
}
}
// GetConfig mengembalikan konfigurasi hub
func (h *Hub) GetConfig() config.WebSocketConfig {
return h.config
}
// GetMessageRegistry mengembalikan registry pesan
func (h *Hub) GetMessageRegistry() *MessageRegistry {
return h.messageRegistry
}
// RegisterChannel mengembalikan channel register untuk handler
func (h *Hub) RegisterChannel() chan *Client {
return h.register
}
// GetStats mengembalikan statistik hub
func (h *Hub) GetStats() map[string]interface{} {
h.mu.RLock()
defer h.mu.RUnlock()
return map[string]interface{}{
"connected_clients": len(h.clients),
"unique_ips": len(h.clientsByIP),
"static_clients": len(h.clientsByStatic),
"rooms": len(h.rooms),
"message_count": h.messageCount,
"error_count": h.errorCount,
"uptime": time.Since(h.startTime).String(),
"server_id": h.config.ServerID,
"timestamp": time.Now().Unix(),
}
}
// setupDatabaseListeners sets up database change listeners for real-time updates
func (h *Hub) setupDatabaseListeners() {
// Listen for changes on retribusi table
channels := []string{"retribusi_changes", "data_changes"}
err := h.dbService.ListenForChanges(h.ctx, "postgres_satudata", channels, func(channel, payload string) {
h.handleDatabaseChange(channel, payload)
})
if err != nil {
h.logActivity("database_listener_error", "", fmt.Sprintf("Failed to setup listeners: %v", err))
} else {
h.logActivity("database_listener_started", "", "Database change listeners initialized")
}
}
// handleDatabaseChange processes database change notifications
func (h *Hub) handleDatabaseChange(channel, payload string) {
h.logActivity("database_change", "", fmt.Sprintf("Channel: %s, Payload: %s", channel, payload))
// Parse the payload to determine what changed
var changeData map[string]interface{}
if err := json.Unmarshal([]byte(payload), &changeData); err != nil {
h.logActivity("database_change_parse_error", "", fmt.Sprintf("Failed to parse payload: %v", err))
return
}
// Create WebSocket message for broadcasting
message := WebSocketMessage{
Type: DatabaseChangeMessage,
Data: changeData,
ClientID: "",
Room: "data_updates", // Broadcast to data_updates room
}
// Broadcast the change to all connected clients in the data_updates room
h.broadcast <- message
}
// GetDatabaseConnection returns a database connection for the specified database
func (h *Hub) GetDatabaseConnection(dbName string) (*sql.DB, error) {
return h.dbService.GetDB(dbName)
}
// ExecuteDatabaseQuery executes a database query and returns results
func (h *Hub) ExecuteDatabaseQuery(dbName, query string, args ...interface{}) ([]map[string]interface{}, error) {
db, err := h.GetDatabaseConnection(dbName)
if err != nil {
return nil, fmt.Errorf("failed to get database connection: %w", err)
}
ctx, cancel := context.WithTimeout(h.ctx, 30*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, fmt.Errorf("failed to get columns: %w", err)
}
var results []map[string]interface{}
for rows.Next() {
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range values {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
row := make(map[string]interface{})
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
val = string(b)
}
row[col] = val
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return results, nil
}
// NotifyDatabaseChange sends a notification to the database for real-time updates
func (h *Hub) NotifyDatabaseChange(dbName, channel, payload string) error {
return h.dbService.NotifyChange(dbName, channel, payload)
}

View File

@@ -0,0 +1,109 @@
package websocket
import (
"time"
"github.com/google/uuid"
)
// MessageType mendefinisikan tipe-tipe pesan yang valid
type MessageType string
const (
// Pesan koneksi
WelcomeMessage MessageType = "welcome"
ErrorMessage MessageType = "error"
DisconnectMessage MessageType = "disconnect"
// Pesan kontrol
PingMessage MessageType = "ping"
PongMessage MessageType = "pong"
HeartbeatMessage MessageType = "heartbeat"
ConnectionTestMessage MessageType = "connection_test"
// Pesan informasi
ServerInfoMessage MessageType = "server_info"
ClientInfoMessage MessageType = "client_info"
OnlineUsersMessage MessageType = "online_users"
// Pesan komunikasi
DirectMessage MessageType = "direct_message"
RoomMessage MessageType = "room_message"
BroadcastMessage MessageType = "broadcast"
// Pesan database
DatabaseQueryMessage MessageType = "database_query"
DatabaseChangeMessage MessageType = "database_change"
DatabaseInsertMessage MessageType = "db_insert"
DatabaseCustomQueryMessage MessageType = "db_custom_query"
// Pesan monitoring
SystemStatusMessage MessageType = "system_status"
DataStreamMessage MessageType = "data_stream"
// Pesan room management
JoinRoomMessage MessageType = "join_room"
LeaveRoomMessage MessageType = "leave_room"
GetRoomInfoMessage MessageType = "get_room_info"
// Pesan admin
AdminKickClientMessage MessageType = "admin_kick_client"
AdminKillServerMessage MessageType = "admin_kill_server"
GetServerStatsMessage MessageType = "get_server_stats"
GetSystemHealthMessage MessageType = "get_system_health"
AdminClearLogsMessage MessageType = "admin_clear_logs"
// Pesan utility
GetStatsMessage MessageType = "get_stats"
)
// WebSocketMessage menyimpan struktur pesan WebSocket
type WebSocketMessage struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
ClientID string `json:"client_id,omitempty"`
MessageID string `json:"message_id,omitempty"`
Room string `json:"room,omitempty"`
}
// NewWebSocketMessage membuat pesan WebSocket baru
func NewWebSocketMessage(msgType MessageType, data interface{}, clientID, room string) WebSocketMessage {
return WebSocketMessage{
Type: msgType,
Data: data,
Timestamp: time.Now(),
ClientID: clientID,
MessageID: uuid.New().String(),
Room: room,
}
}
// MessageHandler mendefinisikan interface untuk handler pesan
type MessageHandler interface {
HandleMessage(client *Client, message WebSocketMessage) error
MessageType() MessageType
}
// MessageRegistry menyimpan registry untuk handler pesan
type MessageRegistry struct {
handlers map[MessageType]MessageHandler
}
// NewMessageRegistry membuat registry pesan baru
func NewMessageRegistry() *MessageRegistry {
return &MessageRegistry{
handlers: make(map[MessageType]MessageHandler),
}
}
// RegisterHandler mendaftarkan handler untuk tipe pesan tertentu
func (r *MessageRegistry) RegisterHandler(handler MessageHandler) {
r.handlers[handler.MessageType()] = handler
}
// GetHandler mendapatkan handler untuk tipe pesan tertentu
func (r *MessageRegistry) GetHandler(msgType MessageType) (MessageHandler, bool) {
handler, exists := r.handlers[msgType]
return handler, exists
}

View File

@@ -0,0 +1,211 @@
package websocket
import (
"fmt"
"time"
)
// DetailedStats menyimpan statistik detail WebSocket
type DetailedStats struct {
ConnectedClients int `json:"connected_clients"`
UniqueIPs int `json:"unique_ips"`
StaticClients int `json:"static_clients"`
ActiveRooms int `json:"active_rooms"`
IPDistribution map[string]int `json:"ip_distribution"`
RoomDistribution map[string]int `json:"room_distribution"`
MessageQueueSize int `json:"message_queue_size"`
QueueWorkers int `json:"queue_workers"`
Uptime time.Duration `json:"uptime_seconds"`
Timestamp int64 `json:"timestamp"`
}
// ClientInfo menyimpan informasi klien
type ClientInfo struct {
ID string `json:"id"`
StaticID string `json:"static_id"`
IPAddress string `json:"ip_address"`
UserID string `json:"user_id"`
Room string `json:"room"`
ConnectedAt time.Time `json:"connected_at"`
LastPing time.Time `json:"last_ping"`
LastPong time.Time `json:"last_pong"`
IsActive bool `json:"is_active"`
}
// MonitoringData menyimpan data monitoring lengkap
type MonitoringData struct {
Stats DetailedStats `json:"stats"`
RecentActivity []ActivityLog `json:"recent_activity"`
SystemHealth map[string]interface{} `json:"system_health"`
Performance PerformanceMetrics `json:"performance"`
}
// PerformanceMetrics menyimpan metrik performa
type PerformanceMetrics struct {
MessagesPerSecond float64 `json:"messages_per_second"`
AverageLatency float64 `json:"average_latency_ms"`
ErrorRate float64 `json:"error_rate_percent"`
MemoryUsage int64 `json:"memory_usage_bytes"`
}
// MonitoringManager mengelola monitoring WebSocket
type MonitoringManager struct {
hub *Hub
}
// NewMonitoringManager membuat manajer monitoring baru
func NewMonitoringManager(hub *Hub) *MonitoringManager {
return &MonitoringManager{hub: hub}
}
// GetDetailedStats mengembalikan statistik detail
func (m *MonitoringManager) GetDetailedStats() DetailedStats {
m.hub.mu.RLock()
defer m.hub.mu.RUnlock()
// Hitung distribusi IP
ipDistribution := make(map[string]int)
for ip, clients := range m.hub.clientsByIP {
ipDistribution[ip] = len(clients)
}
// Hitung distribusi ruangan
roomDistribution := make(map[string]int)
for room, clients := range m.hub.rooms {
roomDistribution[room] = len(clients)
}
return DetailedStats{
ConnectedClients: len(m.hub.clients),
UniqueIPs: len(m.hub.clientsByIP),
StaticClients: len(m.hub.clientsByStatic),
ActiveRooms: len(m.hub.rooms),
IPDistribution: ipDistribution,
RoomDistribution: roomDistribution,
MessageQueueSize: len(m.hub.messageQueue),
QueueWorkers: m.hub.config.QueueWorkers,
Uptime: time.Since(m.hub.startTime),
Timestamp: time.Now().Unix(),
}
}
// GetAllClients mengembalikan semua klien yang terhubung
func (m *MonitoringManager) GetAllClients() []ClientInfo {
m.hub.mu.RLock()
defer m.hub.mu.RUnlock()
var clients []ClientInfo
for client := range m.hub.clients {
clientInfo := ClientInfo{
ID: client.ID,
StaticID: client.StaticID,
IPAddress: client.IPAddress,
UserID: client.UserID,
Room: client.Room,
ConnectedAt: client.connectedAt,
LastPing: client.lastPing,
LastPong: client.lastPong,
IsActive: client.isClientActive(),
}
clients = append(clients, clientInfo)
}
return clients
}
// GetMonitoringData mengembalikan data monitoring lengkap
func (m *MonitoringManager) GetMonitoringData() MonitoringData {
stats := m.GetDetailedStats()
m.hub.activityMu.RLock()
recentActivity := make([]ActivityLog, 0)
// Dapatkan 100 aktivitas terakhir
start := len(m.hub.activityLog) - 100
if start < 0 {
start = 0
}
for i := start; i < len(m.hub.activityLog); i++ {
recentActivity = append(recentActivity, m.hub.activityLog[i])
}
m.hub.activityMu.RUnlock()
// Dapatkan kesehatan sistem dari layanan database
systemHealth := make(map[string]interface{})
if m.hub.dbService != nil {
systemHealth["databases"] = m.hub.dbService.Health()
systemHealth["available_dbs"] = m.hub.dbService.ListDBs()
}
systemHealth["websocket_status"] = "healthy"
systemHealth["uptime_seconds"] = time.Since(m.hub.startTime).Seconds()
// Hitung metrik performa
uptime := time.Since(m.hub.startTime)
var messagesPerSecond float64
var errorRate float64
if uptime.Seconds() > 0 {
messagesPerSecond = float64(m.hub.messageCount) / uptime.Seconds()
}
if m.hub.messageCount > 0 {
errorRate = (float64(m.hub.errorCount) / float64(m.hub.messageCount)) * 100
}
performance := PerformanceMetrics{
MessagesPerSecond: messagesPerSecond,
AverageLatency: 2.5, // Nilai mock - implementasi pelacakan latensi aktual
ErrorRate: errorRate,
MemoryUsage: 0, // Nilai mock - implementasi pelacakan memori aktual
}
return MonitoringData{
Stats: stats,
RecentActivity: recentActivity,
SystemHealth: systemHealth,
Performance: performance,
}
}
// CleanupInactiveClients membersihkan klien tidak aktif
func (m *MonitoringManager) CleanupInactiveClients(inactiveTimeout time.Duration) int {
m.hub.mu.RLock()
var inactiveClients []*Client
cutoff := time.Now().Add(-inactiveTimeout)
for client := range m.hub.clients {
if client.lastPing.Before(cutoff) {
inactiveClients = append(inactiveClients, client)
}
}
m.hub.mu.RUnlock()
// Putuskan koneksi klien tidak aktif
for _, client := range inactiveClients {
m.hub.logActivity("cleanup_disconnect", client.ID,
fmt.Sprintf("Inactive for %v", time.Since(client.lastPing)))
client.cancel()
client.Conn.Close()
}
return len(inactiveClients)
}
// DisconnectClient memutuskan koneksi klien tertentu
func (m *MonitoringManager) DisconnectClient(clientID string) bool {
m.hub.mu.RLock()
client, exists := m.hub.clientsByID[clientID]
m.hub.mu.RUnlock()
if !exists {
return false
}
// Log aktivitas
m.hub.logActivity("force_disconnect", clientID, "Client disconnected by admin")
// Batalkan context dan tutup koneksi
client.cancel()
client.Conn.Close()
return true
}