Zum Inhalt

Binäre Events

Socket.IO unterstützt die effiziente Übertragung von Binärdaten wie Bildern, Dateien, Audio und Video. Dieser Leitfaden behandelt die erweiterte Verwendung von binären Events.

Inhaltsverzeichnis

Warum binäre Events verwenden?

Vorteile

  • Effizienz: Binärdaten werden ohne Base64-Kodierung übertragen (spart ~33% Bandbreite)
  • Geschwindigkeit: Weniger Verarbeitung auf beiden Seiten (Client und Server)
  • Integrität: Binärdaten behalten ihr Originalformat

Wann sie verwenden

  • Dateiübertragung (Bilder, PDFs, Dokumente)
  • Audio/Video-Streaming
  • Sensor- oder IoT-Gerätedaten
  • Benutzerdefinierte Binärprotokolle
  • Alle Daten, die nicht Text sind

Senden von Binärdaten

Basis-API

func (n *Namespace) EmitBinary(event string, binaryData []byte, data ...interface{}) error

Einfaches Beispiel

// Datei lesen
imageData, err := os.ReadFile("photo.jpg")
if err != nil {
  log.Fatal(err)
}

// Als binäres Event senden
client.Of("/").EmitBinary("image", imageData, map[string]interface{}{
  "filename": "photo.jpg",
  "size": len(imageData),
  "type": "image/jpeg",
})

Mit Namespace

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

// Datei in spezifischem Namespace senden
fileData, _ := os.ReadFile("document.pdf")
files.EmitBinary("upload", fileData, map[string]interface{}{
  "filename": "document.pdf",
  "uploadedBy": "user-123",
  "timestamp": time.Now().Unix(),
})

Mit vollständigen Metadaten

type FileMetadata struct {
  Filename    string    `json:"filename"`
  Size        int       `json:"size"`
  ContentType string    `json:"contentType"`
  Checksum    string    `json:"checksum"`
  UploadedBy  string    `json:"uploadedBy"`
  Timestamp   time.Time `json:"timestamp"`
}

func uploadFile(ns *socketio.Namespace, filepath string) error {
  // Datei lesen
  data, err := os.ReadFile(filepath)
  if err != nil {
    return fmt.Errorf("Fehler beim Lesen der Datei: %w", err)
  }

  // Checksum berechnen
  hash := sha256.Sum256(data)
  checksum := hex.EncodeToString(hash[:])

  // Metadaten
  metadata := FileMetadata{
    Filename:    path.Base(filepath),
    Size:        len(data),
    ContentType: detectContentType(data),
    Checksum:    checksum,
    UploadedBy:  getCurrentUser(),
    Timestamp:   time.Now(),
  }

  // Senden
  return ns.EmitBinary("upload", data, metadata)
}

func detectContentType(data []byte) string {
  return http.DetectContentType(data)
}

Empfangen von Binärdaten

Einfache Daten empfangen

client.On("file", func(data ...interface{}) {
  // Erstes Argument ist der Binärpuffer
  if binaryData, ok := data[0].([]byte); ok {
    fmt.Printf("Datei empfangen: %d Bytes\n", len(binaryData))

    // Datei speichern
    err := os.WriteFile("downloaded.bin", binaryData, 0644)
    if err != nil {
      fmt.Println("Fehler beim Speichern:", err)
    }
  }
})

Mit Metadaten empfangen

client.On("file-upload", func(data ...interface{}) {
  if len(data) < 2 {
    fmt.Println("Unvollständige Daten")
    return
  }

  // Erstes Argument: Binärdaten
  binaryData, ok := data[0].([]byte)
  if !ok {
    fmt.Println("Ungültiges Binärformat")
    return
  }

  // Zweites Argument: Metadaten
  metadata, ok := data[1].(map[string]interface{})
  if !ok {
    fmt.Println("Ungültige Metadaten")
    return
  }

  filename := metadata["filename"].(string)
  size := metadata["size"].(float64)

  fmt.Printf("Datei: %s (%d Bytes)\n", filename, int(size))

  // Datei speichern
  os.WriteFile(filename, binaryData, 0644)
})

Integrität validieren

client.On("secure-file", func(data ...interface{}) {
  binaryData := data[0].([]byte)
  metadata := data[1].(map[string]interface{})

  expectedChecksum := metadata["checksum"].(string)

  // Checksum berechnen
  hash := sha256.Sum256(binaryData)
  actualChecksum := hex.EncodeToString(hash[:])

  // Validieren
  if actualChecksum != expectedChecksum {
    fmt.Println("❌ Ungültige Checksum: Datei beschädigt")
    return
  }

  fmt.Println("✅ Checksum gültig")

  // Datei speichern
  filename := metadata["filename"].(string)
  os.WriteFile(filename, binaryData, 0644)
})

Anwendungsfälle

Fall 1: Bild-Upload

func uploadImage(ns *socketio.Namespace, imagePath string) error {
  // Bild lesen
  imageData, err := os.ReadFile(imagePath)
  if err != nil {
    return err
  }

  // Größe validieren
  maxSize := 5 * 1024 * 1024 // 5 MB
  if len(imageData) > maxSize {
    return fmt.Errorf("Bild zu groß (max 5MB)")
  }

  // Format erkennen
  contentType := http.DetectContentType(imageData)
  if !strings.HasPrefix(contentType, "image/") {
    return fmt.Errorf("Datei ist kein Bild")
  }

  // Bei Bedarf komprimieren
  if len(imageData) > 1*1024*1024 { // > 1MB
    imageData, err = compressImage(imageData)
    if err != nil {
      return err
    }
  }

  // Senden
  return ns.EmitBinary("image-upload", imageData, map[string]interface{}{
    "filename":    path.Base(imagePath),
    "size":        len(imageData),
    "contentType": contentType,
    "timestamp":   time.Now().Unix(),
  })
}

func compressImage(data []byte) ([]byte, error) {
  // Bild dekodieren
  img, _, err := image.Decode(bytes.NewReader(data))
  if err != nil {
    return nil, err
  }

  // Als JPEG mit Qualität 80 komprimieren
  var buf bytes.Buffer
  err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80})
  if err != nil {
    return nil, err
  }

  return buf.Bytes(), nil
}

Fall 2: Audio-Streaming

type AudioStreamer struct {
  ns     *socketio.Namespace
  chunks chan []byte
  stop   chan struct{}
}

func NewAudioStreamer(ns *socketio.Namespace) *AudioStreamer {
  return &AudioStreamer{
    ns:     ns,
    chunks: make(chan []byte, 10),
    stop:   make(chan struct{}),
  }
}

func (as *AudioStreamer) Start() {
  go func() {
    sequence := 0
    for {
      select {
      case <-as.stop:
        return
      case chunk := <-as.chunks:
        as.ns.EmitBinary("audio-chunk", chunk, map[string]interface{}{
          "sequence":  sequence,
          "size":      len(chunk),
          "timestamp": time.Now().UnixMilli(),
        })
        sequence++
      }
    }
  }()
}

func (as *AudioStreamer) Stream(chunk []byte) {
  as.chunks <- chunk
}

func (as *AudioStreamer) Stop() {
  close(as.stop)
  close(as.chunks)
}

// Verwendung: Mikrofon-Streaming
func streamMicrophone(ns *socketio.Namespace) {
  streamer := NewAudioStreamer(ns)
  streamer.Start()
  defer streamer.Stop()

  // Vom Mikrofon lesen (vereinfachtes Beispiel)
  buffer := make([]byte, 4096)
  for {
    // Audio lesen (simuliert)
    n, err := readAudioFromMic(buffer)
    if err != nil {
      break
    }

    // Chunk senden
    streamer.Stream(buffer[:n])
  }
}

Fall 3: Download großer Dateien

type FileDownloader struct {
  ns        *socketio.Namespace
  chunks    map[int][]byte
  total     int
  received  int
  filename  string
  mu        sync.Mutex
  done      chan struct{}
}

func NewFileDownloader(ns *socketio.Namespace) *FileDownloader {
  fd := &FileDownloader{
    ns:     ns,
    chunks: make(map[int][]byte),
    done:   make(chan struct{}),
  }

  fd.setupHandlers()
  return fd
}

func (fd *FileDownloader) setupHandlers() {
  fd.ns.On("file-info", func(data ...interface{}) {
    info := data[0].(map[string]interface{})
    fd.filename = info["filename"].(string)
    fd.total = int(info["chunks"].(float64))

    fmt.Printf("Download: %s (%d Chunks)\n", fd.filename, fd.total)
  })

  fd.ns.On("file-chunk", func(data ...interface{}) {
    binaryData := data[0].([]byte)
    metadata := data[1].(map[string]interface{})
    index := int(metadata["index"].(float64))

    fd.mu.Lock()
    fd.chunks[index] = binaryData
    fd.received++
    progress := (float64(fd.received) / float64(fd.total)) * 100
    fd.mu.Unlock()

    fmt.Printf("Fortschritt: %.1f%% (%d/%d)\n", progress, fd.received, fd.total)

    // Prüfen ob vollständig
    if fd.received == fd.total {
      close(fd.done)
    }
  })
}

func (fd *FileDownloader) Download(fileId string) error {
  // Datei anfordern
  fd.ns.Emit("request-file", fileId)

  // Warten bis vollständig
  <-fd.done

  // Chunks zusammenfügen
  return fd.saveFile()
}

func (fd *FileDownloader) saveFile() error {
  file, err := os.Create(fd.filename)
  if err != nil {
    return err
  }
  defer file.Close()

  // Chunks in Reihenfolge schreiben
  for i := 0; i < fd.total; i++ {
    chunk, exists := fd.chunks[i]
    if !exists {
      return fmt.Errorf("Chunk %d fehlt", i)
    }

    if _, err := file.Write(chunk); err != nil {
      return err
    }
  }

  fmt.Printf("✅ Datei gespeichert: %s\n", fd.filename)
  return nil
}

// Verwendung
downloader := NewFileDownloader(client.Of("/files"))
err := downloader.Download("file-123")
if err != nil {
  fmt.Println("Fehler beim Download:", err)
}

Erweiterte Muster

Muster: Chunked Upload mit Fortschritt

type ChunkedUploader struct {
  ns         *socketio.Namespace
  chunkSize  int
  onProgress func(float64)
}

func NewChunkedUploader(ns *socketio.Namespace, chunkSize int) *ChunkedUploader {
  return &ChunkedUploader{
    ns:        ns,
    chunkSize: chunkSize,
  }
}

func (cu *ChunkedUploader) OnProgress(callback func(float64)) {
  cu.onProgress = callback
}

func (cu *ChunkedUploader) Upload(filepath string) error {
  // Datei lesen
  data, err := os.ReadFile(filepath)
  if err != nil {
    return err
  }

  // Chunks berechnen
  totalChunks := (len(data) + cu.chunkSize - 1) / cu.chunkSize

  // Initiale Metadaten senden
  cu.ns.Emit("upload-start", map[string]interface{}{
    "filename": path.Base(filepath),
    "size":     len(data),
    "chunks":   totalChunks,
  })

  // Chunks senden
  for i := 0; i < totalChunks; i++ {
    start := i * cu.chunkSize
    end := start + cu.chunkSize
    if end > len(data) {
      end = len(data)
    }

    chunk := data[start:end]

    err := cu.ns.EmitBinary("upload-chunk", chunk, map[string]interface{}{
      "index": i,
      "total": totalChunks,
    })

    if err != nil {
      return fmt.Errorf("Fehler bei Chunk %d: %w", i, err)
    }

    // Fortschritt melden
    if cu.onProgress != nil {
      progress := (float64(i+1) / float64(totalChunks)) * 100
      cu.onProgress(progress)
    }

    // Kleine Verzögerung um nicht zu überlasten
    time.Sleep(10 * time.Millisecond)
  }

  // Abschließen
  cu.ns.Emit("upload-complete", map[string]interface{}{
    "filename": path.Base(filepath),
  })

  return nil
}

// Verwendung
uploader := NewChunkedUploader(client.Of("/files"), 64*1024) // 64KB Chunks

uploader.OnProgress(func(progress float64) {
  fmt.Printf("\rFortschritt: %.1f%%", progress)
})

err := uploader.Upload("large-file.bin")
if err != nil {
  fmt.Println("\nFehler:", err)
} else {
  fmt.Println("\n✅ Upload vollständig")
}

Muster: Binärdaten mit Acknowledgment

func uploadWithConfirmation(ns *socketio.Namespace, data []byte, metadata map[string]interface{}) error {
  done := make(chan error, 1)

  // EmitBinary unterstützt ACK nicht direkt, Emit mit manueller Kodierung verwenden
  // Alternativ zwei Events verwenden: eines für Binärdaten und eines für ACK

  // 1. Binärdaten senden
  err := ns.EmitBinary("file-data", data, metadata)
  if err != nil {
    return err
  }

  // 2. Bestätigung anfordern
  ns.EmitWithAck("file-confirm", func(response ...interface{}) {
    if response == nil {
      done <- fmt.Errorf("Timeout beim Warten auf Bestätigung")
      return
    }

    resp := response[0].(map[string]interface{})
    if resp["success"].(bool) {
      done <- nil
    } else {
      done <- fmt.Errorf("Fehler: %s", resp["error"])
    }
  }, metadata["filename"])

  return <-done
}

Muster: Binärkomprimierung

import (
  "compress/gzip"
  "bytes"
)

func uploadCompressed(ns *socketio.Namespace, data []byte, metadata map[string]interface{}) error {
  // Daten komprimieren
  var buf bytes.Buffer
  gzipWriter := gzip.NewWriter(&buf)

  if _, err := gzipWriter.Write(data); err != nil {
    return err
  }

  if err := gzipWriter.Close(); err != nil {
    return err
  }

  compressed := buf.Bytes()
  originalSize := len(data)
  compressedSize := len(compressed)
  ratio := float64(compressedSize) / float64(originalSize) * 100

  fmt.Printf("Komprimierung: %d → %d Bytes (%.1f%%)\n",
    originalSize, compressedSize, ratio)

  // Komprimierungsinfo zu Metadaten hinzufügen
  metadata["compressed"] = true
  metadata["originalSize"] = originalSize
  metadata["compressedSize"] = compressedSize

  return ns.EmitBinary("upload", compressed, metadata)
}

// Empfang und Dekomprimierung
client.On("compressed-file", func(data ...interface{}) {
  compressedData := data[0].([]byte)
  metadata := data[1].(map[string]interface{})

  // Prüfen ob komprimiert
  if compressed, ok := metadata["compressed"].(bool); ok && compressed {
    // Dekomprimieren
    reader, err := gzip.NewReader(bytes.NewReader(compressedData))
    if err != nil {
      fmt.Println("Fehler beim Dekomprimieren:", err)
      return
    }
    defer reader.Close()

    var buf bytes.Buffer
    if _, err := buf.ReadFrom(reader); err != nil {
      fmt.Println("Fehler beim Lesen dekomprimierter Daten:", err)
      return
    }

    originalData := buf.Bytes()
    fmt.Printf("Dekomprimiert: %d Bytes\n", len(originalData))

    // Originaldaten verarbeiten
    processFile(originalData, metadata)
  } else {
    // Unkomprimierte Daten verarbeiten
    processFile(compressedData, metadata)
  }
})

Best Practices

1. Größe validieren

// ✅ Gut: Größe vor dem Senden validieren
maxSize := 10 * 1024 * 1024 // 10 MB
if len(data) > maxSize {
  return fmt.Errorf("Datei zu groß (max %d MB)", maxSize/(1024*1024))
}

ns.EmitBinary("upload", data, metadata)

2. Checksums verwenden

// ✅ Gut: immer Checksum einschließen
hash := sha256.Sum256(data)
metadata["checksum"] = hex.EncodeToString(hash[:])

ns.EmitBinary("upload", data, metadata)

3. Netzwerkfehler behandeln

// ✅ Gut: bei Fehler wiederholen
func uploadWithRetry(ns *socketio.Namespace, data []byte, maxRetries int) error {
  for attempt := 0; attempt <= maxRetries; attempt++ {
    err := ns.EmitBinary("upload", data, metadata)
    if err == nil {
      return nil
    }

    if attempt < maxRetries {
      fmt.Printf("Wiederholung %d/%d...\n", attempt+1, maxRetries)
      time.Sleep(time.Second * time.Duration(attempt+1))
    }
  }

  return fmt.Errorf("nach %d Versuchen fehlgeschlagen", maxRetries+1)
}

4. Chunks für große Dateien verwenden

// ✅ Gut: große Dateien in Chunks aufteilen
if len(data) > 1*1024*1024 { // > 1MB
  uploadInChunks(ns, data, 64*1024) // 64KB Chunks
} else {
  ns.EmitBinary("upload", data, metadata)
}

5. Speicher freigeben

// ✅ Gut: Speicher nach Verwendung freigeben
data, err := os.ReadFile("large-file.bin")
if err != nil {
  return err
}

ns.EmitBinary("upload", data, metadata)

data = nil // GC erlauben, Speicher freizugeben
runtime.GC()

Siehe auch