Skip to content

Binary Events

Socket.IO supports efficient transmission of binary data such as images, files, audio, and video. This guide covers advanced usage of binary events.

Table of Contents

Why Use Binary Events?

Advantages

  • Efficiency: Binary data is transmitted without Base64 encoding (saves ~33% bandwidth)
  • Speed: Less processing overhead on both client and server
  • Integrity: Binary data maintains its original format

When to Use Them

  • File transfers (images, PDFs, documents)
  • Audio/video streaming
  • Sensor data or IoT devices
  • Custom binary protocols
  • Any non-text data

Sending Binary Data

Basic API

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

Simple Example

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

// Send as binary event
client.Of("/").EmitBinary("image", imageData, map[string]interface{}{
  "filename": "photo.jpg",
  "size": len(imageData),
  "type": "image/jpeg",
})

With Namespace

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

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

With Complete Metadata

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 {
  // Read file
  data, err := os.ReadFile(filepath)
  if err != nil {
    return fmt.Errorf("error reading file: %w", err)
  }

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

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

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

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

Receiving Binary Data

Receive Simple Data

client.On("file", func(data ...interface{}) {
  // First argument is the binary buffer
  if binaryData, ok := data[0].([]byte); ok {
    fmt.Printf("File received: %d bytes\n", len(binaryData))

    // Save file
    err := os.WriteFile("downloaded.bin", binaryData, 0644)
    if err != nil {
      fmt.Println("Error saving:", err)
    }
  }
})

Receive with Metadata

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

  // First argument: binary data
  binaryData, ok := data[0].([]byte)
  if !ok {
    fmt.Println("Invalid binary format")
    return
  }

  // Second argument: metadata
  metadata, ok := data[1].(map[string]interface{})
  if !ok {
    fmt.Println("Invalid metadata")
    return
  }

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

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

  // Save file
  os.WriteFile(filename, binaryData, 0644)
})

Validate Integrity

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

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

  // Calculate checksum
  hash := sha256.Sum256(binaryData)
  actualChecksum := hex.EncodeToString(hash[:])

  // Validate
  if actualChecksum != expectedChecksum {
    fmt.Println("❌ Invalid checksum: corrupted file")
    return
  }

  fmt.Println("✅ Valid checksum")

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

Use Cases

Case 1: Image Upload

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

  // Validate size
  maxSize := 5 * 1024 * 1024 // 5 MB
  if len(imageData) > maxSize {
    return fmt.Errorf("image too large (max 5MB)")
  }

  // Detect format
  contentType := http.DetectContentType(imageData)
  if !strings.HasPrefix(contentType, "image/") {
    return fmt.Errorf("file is not an image")
  }

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

  // Send
  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) {
  // Decode image
  img, _, err := image.Decode(bytes.NewReader(data))
  if err != nil {
    return nil, err
  }

  // Compress as JPEG with quality 80
  var buf bytes.Buffer
  err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80})
  if err != nil {
    return nil, err
  }

  return buf.Bytes(), nil
}

Case 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)
}

// Usage: microphone streaming
func streamMicrophone(ns *socketio.Namespace) {
  streamer := NewAudioStreamer(ns)
  streamer.Start()
  defer streamer.Stop()

  // Read from microphone (simplified example)
  buffer := make([]byte, 4096)
  for {
    // Read audio (simulated)
    n, err := readAudioFromMic(buffer)
    if err != nil {
      break
    }

    // Send chunk
    streamer.Stream(buffer[:n])
  }
}

Case 3: Large File Download

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("Downloading: %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("Progress: %.1f%% (%d/%d)\n", progress, fd.received, fd.total)

    // Check if complete
    if fd.received == fd.total {
      close(fd.done)
    }
  })
}

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

  // Wait until complete
  <-fd.done

  // Assemble chunks
  return fd.saveFile()
}

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

  // Write chunks in order
  for i := 0; i < fd.total; i++ {
    chunk, exists := fd.chunks[i]
    if !exists {
      return fmt.Errorf("missing chunk %d", i)
    }

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

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

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

Advanced Patterns

Pattern: Chunked Upload with Progress

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 {
  // Read file
  data, err := os.ReadFile(filepath)
  if err != nil {
    return err
  }

  // Calculate chunks
  totalChunks := (len(data) + cu.chunkSize - 1) / cu.chunkSize

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

  // Send chunks
  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("error in chunk %d: %w", i, err)
    }

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

    // Small delay to avoid saturation
    time.Sleep(10 * time.Millisecond)
  }

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

  return nil
}

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

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

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

Pattern: Binary with Acknowledgment

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

  // EmitBinary doesn't support ACK directly, use Emit with manual encoding
  // Alternatively, use two events: one binary and another for ACK

  // 1. Send binary
  err := ns.EmitBinary("file-data", data, metadata)
  if err != nil {
    return err
  }

  // 2. Request confirmation
  ns.EmitWithAck("file-confirm", func(response ...interface{}) {
    if response == nil {
      done <- fmt.Errorf("timeout waiting for confirmation")
      return
    }

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

  return <-done
}

Pattern: Binary Compression

import (
  "compress/gzip"
  "bytes"
)

func uploadCompressed(ns *socketio.Namespace, data []byte, metadata map[string]interface{}) error {
  // Compress data
  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("Compression: %d → %d bytes (%.1f%%)\n",
    originalSize, compressedSize, ratio)

  // Add compression info to metadata
  metadata["compressed"] = true
  metadata["originalSize"] = originalSize
  metadata["compressedSize"] = compressedSize

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

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

  // Check if compressed
  if compressed, ok := metadata["compressed"].(bool); ok && compressed {
    // Decompress
    reader, err := gzip.NewReader(bytes.NewReader(compressedData))
    if err != nil {
      fmt.Println("Error decompressing:", err)
      return
    }
    defer reader.Close()

    var buf bytes.Buffer
    if _, err := buf.ReadFrom(reader); err != nil {
      fmt.Println("Error reading decompressed data:", err)
      return
    }

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

    // Process original data
    processFile(originalData, metadata)
  } else {
    // Process uncompressed data
    processFile(compressedData, metadata)
  }
})

Best Practices

1. Validate Size

// ✅ Good: validate size before sending
maxSize := 10 * 1024 * 1024 // 10 MB
if len(data) > maxSize {
  return fmt.Errorf("file too large (max %d MB)", maxSize/(1024*1024))
}

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

2. Use Checksums

// ✅ Good: always include checksum
hash := sha256.Sum256(data)
metadata["checksum"] = hex.EncodeToString(hash[:])

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

3. Handle Network Errors

// ✅ Good: retry on error
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("Retry %d/%d...\n", attempt+1, maxRetries)
      time.Sleep(time.Second * time.Duration(attempt+1))
    }
  }

  return fmt.Errorf("failed after %d attempts", maxRetries+1)
}

4. Use Chunks for Large Files

// ✅ Good: split large files into chunks
if len(data) > 1*1024*1024 { // > 1MB
  uploadInChunks(ns, data, 64*1024) // 64KB chunks
} else {
  ns.EmitBinary("upload", data, metadata)
}

5. Free Memory

// ✅ Good: free memory after use
data, err := os.ReadFile("large-file.bin")
if err != nil {
  return err
}

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

data = nil // Allow GC to free memory
runtime.GC()

See Also