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?
- Senden von Binärdaten
- Empfangen von Binärdaten
- Anwendungsfälle
- Erweiterte Muster
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¶
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()