Zum Inhalt

Erweiterte Verwendung von Namespaces

Namespaces sind eine leistungsstarke Funktion von Socket.IO, die es ermöglicht, mehrere Kommunikationskanäle über eine einzige WebSocket-Verbindung zu multiplexen.

Inhaltsverzeichnis

Was sind Namespaces?

Namespaces sind logisch getrennte Kommunikationskanäle, die sich dieselbe physische WebSocket-Verbindung teilen. Jeder Namespace:

  • Hat seinen eigenen Satz von Events
  • Verwaltet seine eigene Verbindung/Trennung
  • Kann unterschiedliche Handler haben
  • Teilt sich die zugrundeliegende TCP/WebSocket-Verbindung

Standard-Namespace

Der Standard-Namespace ist /. Alle Events ohne spezifischen Namespace gehen hierhin:

// Diese drei Codes sind äquivalent
client.On("event", handler)
client.Of("/").On("event", handler)
defaultNs := client.Of("/")
defaultNs.On("event", handler)

Anwendungsfälle

1. Trennung nach Funktionalität

Trennen Sie verschiedene Bereiche Ihrer Anwendung in unterschiedliche Namespaces:

// Öffentlicher Chat
publicChat := client.Of("/chat")
publicChat.On("message", handlePublicMessage)

// Systembenachrichtigungen
notifications := client.Of("/notifications")
notifications.On("alert", handleAlert)

// Echtzeit-Daten-API
dataStream := client.Of("/stream")
dataStream.On("update", handleDataUpdate)

2. Trennung nach Zugriffsebene

Verwenden Sie Namespaces für unterschiedliche Berechtigungsebenen:

// Öffentlicher Namespace (alle können zugreifen)
public := client.Of("/public")
public.On("announcement", handleAnnouncement)

// Namespace für authentifizierte Benutzer
authenticated := client.Of("/authenticated")
authenticated.OnConnect(func() {
  authenticated.Emit("auth", map[string]interface{}{
    "token": userToken,
  })
})
authenticated.On("private-data", handlePrivateData)

// Administrator-Namespace
admin := client.Of("/admin")
admin.OnConnect(func() {
  admin.Emit("auth", map[string]interface{}{
    "token": adminToken,
    "role": "admin",
  })
})
admin.On("admin-event", handleAdminEvent)

3. Virtuelle Räume

Verwenden Sie Namespaces als separate virtuelle Räume:

// Verschiedene Chat-Räume
lobby := client.Of("/chat/lobby")
lobby.On("message", handleLobbyMessage)

gameRoom := client.Of("/chat/game-room-1")
gameRoom.On("message", handleGameRoomMessage)

privateRoom := client.Of("/chat/private-123")
privateRoom.On("message", handlePrivateMessage)

4. Microservices

Jeder Microservice kann seinen eigenen Namespace haben:

// Benutzer-Service
users := client.Of("/services/users")
users.On("user-update", handleUserUpdate)

// Zahlungs-Service
payments := client.Of("/services/payments")
payments.On("payment-status", handlePaymentStatus)

// Benachrichtigungs-Service
notifications := client.Of("/services/notifications")
notifications.On("notify", handleNotification)

Architektur

Unabhängiger Lebenszyklus

Jeder Namespace hat seinen eigenen Lebenszyklus:

chat := client.Of("/chat")
api := client.Of("/api")

chat.OnConnect(func() {
  fmt.Println("Chat verbunden")
  chat.Emit("join", "general")
})

chat.OnDisconnect(func(reason string) {
  fmt.Println("Chat getrennt:", reason)
})

api.OnConnect(func() {
  fmt.Println("API verbunden")
  api.Emit("subscribe", []string{"users", "posts"})
})

api.OnDisconnect(func(reason string) {
  fmt.Println("API getrennt:", reason)
})

Isolierte Events

Events in einem Namespace beeinflussen andere Namespaces nicht:

chat := client.Of("/chat")
api := client.Of("/api")

// Dieser Handler empfängt nur Events von /chat
chat.On("message", func(data ...interface{}) {
  fmt.Println("Chat-Nachricht:", data[0])
})

// Dieser Handler empfängt nur Events von /api
api.On("message", func(data ...interface{}) {
  fmt.Println("API-Nachricht:", data[0])
})

// Emission in /chat beeinflusst /api nicht
chat.Emit("message", "Hallo vom Chat")
api.Emit("message", "Hallo von der API")

Implementierungsmuster

Muster: Namespace-Manager

Verwalten Sie mehrere Namespaces zentral:

type NamespaceManager struct {
  client     *socketio.Socket
  namespaces map[string]*socketio.Namespace
}

func NewNamespaceManager(url string) *NamespaceManager {
  return &NamespaceManager{
    client:     socketio.New(url),
    namespaces: make(map[string]*socketio.Namespace),
  }
}

func (m *NamespaceManager) Get(path string) *socketio.Namespace {
  if ns, exists := m.namespaces[path]; exists {
    return ns
  }

  ns := m.client.Of(path)
  m.namespaces[path] = ns
  return ns
}

func (m *NamespaceManager) Close() error {
  return m.client.Close()
}

// Verwendung
manager := NewNamespaceManager("ws://localhost:3000")
defer manager.Close()

chat := manager.Get("/chat")
chat.On("message", handleChatMessage)

api := manager.Get("/api")
api.On("data", handleAPIData)

Muster: Namespace-Wrapper

Umschließen Sie Namespaces mit zusätzlicher Logik:

type ChatNamespace struct {
  ns       *socketio.Namespace
  username string
  room     string
}

func NewChatNamespace(client *socketio.Socket, username string) *ChatNamespace {
  cn := &ChatNamespace{
    ns:       client.Of("/chat"),
    username: username,
  }

  cn.setupHandlers()
  return cn
}

func (cn *ChatNamespace) setupHandlers() {
  cn.ns.OnConnect(func() {
    fmt.Printf("%s mit Chat verbunden\n", cn.username)
  })

  cn.ns.On("message", func(data ...interface{}) {
    user := data[0].(string)
    msg := data[1].(string)
    fmt.Printf("[%s] %s: %s\n", cn.room, user, msg)
  })
}

func (cn *ChatNamespace) JoinRoom(room string) {
  cn.room = room
  cn.ns.Emit("join-room", room)
}

func (cn *ChatNamespace) SendMessage(message string) {
  cn.ns.Emit("message", cn.username, message)
}

func (cn *ChatNamespace) LeaveRoom() {
  if cn.room != "" {
    cn.ns.Emit("leave-room", cn.room)
    cn.room = ""
  }
}

// Verwendung
client := socketio.New("ws://localhost:3000")
chat := NewChatNamespace(client, "Max")

chat.JoinRoom("general")
chat.SendMessage("Hallo zusammen!")

Muster: Namespace mit Zustand

Halten Sie namespaces-spezifischen Zustand vor:

type APINamespace struct {
  ns          *socketio.Namespace
  subscriptions []string
  cache       map[string]interface{}
  mu          sync.RWMutex
}

func NewAPINamespace(client *socketio.Socket) *APINamespace {
  api := &APINamespace{
    ns:    client.Of("/api"),
    cache: make(map[string]interface{}),
  }

  api.setupHandlers()
  return api
}

func (a *APINamespace) setupHandlers() {
  a.ns.OnConnect(func() {
    // Automatisch neu abonnieren bei Wiederverbindung
    if len(a.subscriptions) > 0 {
      a.ns.Emit("subscribe", a.subscriptions)
    }
  })

  a.ns.On("update", func(data ...interface{}) {
    key := data[0].(string)
    value := data[1]

    a.mu.Lock()
    a.cache[key] = value
    a.mu.Unlock()

    fmt.Printf("Cache aktualisiert: %s\n", key)
  })
}

func (a *APINamespace) Subscribe(topics []string) {
  a.subscriptions = append(a.subscriptions, topics...)
  a.ns.Emit("subscribe", topics)
}

func (a *APINamespace) Get(key string) (interface{}, bool) {
  a.mu.RLock()
  defer a.mu.RUnlock()

  value, exists := a.cache[key]
  return value, exists
}

// Verwendung
client := socketio.New("ws://localhost:3000")
api := NewAPINamespace(client)

api.Subscribe([]string{"users", "posts", "comments"})

// Cache verwenden
if value, ok := api.Get("users"); ok {
  fmt.Println("Benutzer:", value)
}

Muster: Dynamische Namespaces

Erstellen und zerstören Sie Namespaces dynamisch:

type DynamicNamespaceManager struct {
  client     *socketio.Socket
  namespaces sync.Map // map[string]*socketio.Namespace
}

func NewDynamicNamespaceManager(url string) *DynamicNamespaceManager {
  return &DynamicNamespaceManager{
    client: socketio.New(url),
  }
}

func (m *DynamicNamespaceManager) Join(path string) *socketio.Namespace {
  if ns, loaded := m.namespaces.LoadOrStore(path, m.client.Of(path)); loaded {
    return ns.(*socketio.Namespace)
  }

  ns := m.client.Of(path)
  m.namespaces.Store(path, ns)

  fmt.Printf("Namespace erstellt: %s\n", path)
  return ns
}

func (m *DynamicNamespaceManager) Leave(path string) {
  if ns, ok := m.namespaces.LoadAndDelete(path); ok {
    fmt.Printf("Namespace entfernt: %s\n", path)
    // Hinweis: Es gibt keine Close()-Methode für Namespace,
    // einfach den Namespace nicht mehr verwenden
    _ = ns
  }
}

func (m *DynamicNamespaceManager) List() []string {
  var paths []string
  m.namespaces.Range(func(key, value interface{}) bool {
    paths = append(paths, key.(string))
    return true
  })
  return paths
}

// Verwendung
manager := NewDynamicNamespaceManager("ws://localhost:3000")

// Benutzer tritt einem Raum bei
roomNs := manager.Join("/rooms/room-123")
roomNs.On("message", handleRoomMessage)

// Benutzer verlässt den Raum
manager.Leave("/rooms/room-123")

// Aktive Räume anzeigen
fmt.Println("Aktive Räume:", manager.List())

Best Practices

1. Klare Organisation

Verwenden Sie klare und konsistente Namenskonventionen:

// ✅ Gut: klare und hierarchische Struktur
client.Of("/chat/public")
client.Of("/chat/private")
client.Of("/api/v1")
client.Of("/api/v2")
client.Of("/admin/users")
client.Of("/admin/settings")

// ❌ Schlecht: inkonsistente Namen
client.Of("/chat")
client.Of("/PRIVATE-CHAT")
client.Of("/api_v1")
client.Of("/AdminPanel")

2. Verbindungsverwaltung pro Namespace

Jeder Namespace sollte seine eigene Verbindung verwalten:

chat := client.Of("/chat")
api := client.Of("/api")

// ✅ Gut: jeder Namespace verwaltet seinen Zustand
chat.OnConnect(func() {
  chat.Emit("join", "general")
})

api.OnConnect(func() {
  api.Emit("authenticate", token)
})

// ❌ Schlecht: annehmen, dass alle gleichzeitig verbinden
client.OnConnect(func() {
  // Dies wird nur für den Standard-Namespace ausgeführt
  chat.Emit("join", "general")  // Kann fehlschlagen, wenn /chat nicht verbunden ist
})

3. Namespace-Explosion vermeiden

Erstellen Sie nicht unnötig zu viele Namespaces:

// ✅ Gut: Namespaces nach Kategorie
chat := client.Of("/chat")
chat.Emit("join-room", "general")
chat.Emit("join-room", "random")

// ❌ Schlecht: ein Namespace pro Raum
general := client.Of("/chat/general")
random := client.Of("/chat/random")
tech := client.Of("/chat/tech")
// ... 100 weitere

4. Namespaces dokumentieren

Dokumentieren Sie, was jeder Namespace tut:

// Namespace /chat: Echtzeit-Messaging
// Events: message, user-joined, user-left, typing
// Authentifizierung: Erforderlich
chat := client.Of("/chat")

// Namespace /api: Echtzeit-Daten
// Events: update, delete, create
// Authentifizierung: JWT-Token
api := client.Of("/api")

// Namespace /admin: Administratorbereich
// Events: stats, user-action, system-alert
// Authentifizierung: Admin-Token
admin := client.Of("/admin")

5. Testing pro Namespace

Testen Sie jeden Namespace unabhängig:

func TestChatNamespace(t *testing.T) {
  client := socketio.New("ws://localhost:3000")
  defer client.Close()

  chat := client.Of("/chat")

  var received bool
  chat.On("welcome", func(data ...interface{}) {
    received = true
  })

  // Auf Verbindung warten
  time.Sleep(100 * time.Millisecond)

  if !received {
    t.Error("Welcome-Event nicht empfangen")
  }
}

6. Namespace-Cleanup

Bereinigen Sie Namespaces, wenn Sie sie nicht mehr benötigen:

type RoomManager struct {
  client *socketio.Socket
  rooms  sync.Map
}

func (rm *RoomManager) JoinRoom(roomId string) {
  ns := rm.client.Of("/rooms/" + roomId)
  rm.rooms.Store(roomId, ns)

  ns.OnConnect(func() {
    fmt.Printf("Mit Raum %s verbunden\n", roomId)
  })
}

func (rm *RoomManager) LeaveRoom(roomId string) {
  if ns, ok := rm.rooms.LoadAndDelete(roomId); ok {
    // Der Namespace wird keine Events mehr empfangen
    fmt.Printf("Raum %s wird verlassen\n", roomId)
    _ = ns
  }
}

func (rm *RoomManager) Cleanup() {
  rm.rooms.Range(func(key, value interface{}) bool {
    rm.rooms.Delete(key)
    return true
  })
}

Vollständiges Beispiel: Multi-Raum-Chat-System

package main

import (
  "fmt"
  "sync"
  socketio "github.com/arcaela/socket.io-client-go"
)

type ChatSystem struct {
  client     *socketio.Socket
  user       string
  rooms      map[string]*socketio.Namespace
  roomsMutex sync.RWMutex
}

func NewChatSystem(url, username string) *ChatSystem {
  return &ChatSystem{
    client: socketio.New(url),
    user:   username,
    rooms:  make(map[string]*socketio.Namespace),
  }
}

func (cs *ChatSystem) JoinRoom(roomName string) error {
  cs.roomsMutex.Lock()
  defer cs.roomsMutex.Unlock()

  if _, exists := cs.rooms[roomName]; exists {
    return fmt.Errorf("Sie sind bereits im Raum %s", roomName)
  }

  // Namespace für den Raum erstellen
  room := cs.client.Of("/chat/" + roomName)

  // Handler konfigurieren
  room.OnConnect(func() {
    fmt.Printf("✅ Mit Raum verbunden: %s\n", roomName)
    room.Emit("join", cs.user)
  })

  room.OnDisconnect(func(reason string) {
    fmt.Printf("❌ Von Raum %s getrennt: %s\n", roomName, reason)
  })

  room.On("message", func(data ...interface{}) {
    user := data[0].(string)
    msg := data[1].(string)
    fmt.Printf("[%s] %s: %s\n", roomName, user, msg)
  })

  room.On("user-joined", func(data ...interface{}) {
    user := data[0].(string)
    fmt.Printf("[%s] 👤 %s ist beigetreten\n", roomName, user)
  })

  room.On("user-left", func(data ...interface{}) {
    user := data[0].(string)
    fmt.Printf("[%s] 👋 %s hat verlassen\n", roomName, user)
  })

  cs.rooms[roomName] = room
  return nil
}

func (cs *ChatSystem) LeaveRoom(roomName string) error {
  cs.roomsMutex.Lock()
  defer cs.roomsMutex.Unlock()

  room, exists := cs.rooms[roomName]
  if !exists {
    return fmt.Errorf("Sie sind nicht im Raum %s", roomName)
  }

  room.Emit("leave", cs.user)
  delete(cs.rooms, roomName)
  fmt.Printf("Sie haben den Raum verlassen: %s\n", roomName)

  return nil
}

func (cs *ChatSystem) SendMessage(roomName, message string) error {
  cs.roomsMutex.RLock()
  room, exists := cs.rooms[roomName]
  cs.roomsMutex.RUnlock()

  if !exists {
    return fmt.Errorf("Sie sind nicht im Raum %s", roomName)
  }

  return room.Emit("message", cs.user, message)
}

func (cs *ChatSystem) ListRooms() []string {
  cs.roomsMutex.RLock()
  defer cs.roomsMutex.RUnlock()

  rooms := make([]string, 0, len(cs.rooms))
  for name := range cs.rooms {
    rooms = append(rooms, name)
  }
  return rooms
}

func (cs *ChatSystem) Close() error {
  return cs.client.Close()
}

func main() {
  chat := NewChatSystem("ws://localhost:3000", "Max")
  defer chat.Close()

  // Mehreren Räumen beitreten
  chat.JoinRoom("general")
  chat.JoinRoom("tech")
  chat.JoinRoom("random")

  // Nachrichten senden
  chat.SendMessage("general", "Hallo zusammen!")
  chat.SendMessage("tech", "Benutzt jemand Go?")

  // Aktive Räume anzeigen
  fmt.Println("Aktive Räume:", chat.ListRooms())

  // Einen Raum verlassen
  chat.LeaveRoom("random")

  select {}
}

Siehe auch