Saltar a contenido

Eventos Binarios

Socket.IO soporta transmisión eficiente de datos binarios como imágenes, archivos, audio y video. Esta guía cubre el uso avanzado de eventos binarios.

Tabla de Contenidos

¿Por qué Usar Eventos Binarios?

Ventajas

  • Eficiencia: Los datos binarios se transmiten sin codificación Base64 (ahorra ~33% de ancho de banda)
  • Velocidad: Menos procesamiento en ambos lados (cliente y servidor)
  • Integridad: Los datos binarios mantienen su formato original

Cuándo Usarlos

  • Transferencia de archivos (imágenes, PDFs, documentos)
  • Streaming de audio/video
  • Datos de sensores o dispositivos IoT
  • Protocolos binarios personalizados
  • Cualquier dato que no sea texto

Envío de Datos Binarios

API Básica

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

Ejemplo Simple

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

// Enviar como evento binario
client.Of("/").EmitBinary("image", imageData, map[string]interface{}{
  "filename": "photo.jpg",
  "size": len(imageData),
  "type": "image/jpeg",
})

Con Namespace

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

// Enviar archivo en namespace específico
fileData, _ := os.ReadFile("document.pdf")
files.EmitBinary("upload", fileData, map[string]interface{}{
  "filename": "document.pdf",
  "uploadedBy": "user-123",
  "timestamp": time.Now().Unix(),
})

Con Metadata Completa

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

  // Calcular 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(),
  }

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

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

Recepción de Datos Binarios

Recibir Datos Simples

client.On("file", func(data ...interface{}) {
  // Primer argumento es el buffer binario
  if binaryData, ok := data[0].([]byte); ok {
    fmt.Printf("Archivo recibido: %d bytes\n", len(binaryData))

    // Guardar archivo
    err := os.WriteFile("downloaded.bin", binaryData, 0644)
    if err != nil {
      fmt.Println("Error guardando:", err)
    }
  }
})

Recibir con Metadata

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

  // Primer argumento: datos binarios
  binaryData, ok := data[0].([]byte)
  if !ok {
    fmt.Println("Formato binario inválido")
    return
  }

  // Segundo argumento: metadata
  metadata, ok := data[1].(map[string]interface{})
  if !ok {
    fmt.Println("Metadata inválida")
    return
  }

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

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

  // Guardar archivo
  os.WriteFile(filename, binaryData, 0644)
})

Validar Integridad

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

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

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

  // Validar
  if actualChecksum != expectedChecksum {
    fmt.Println("❌ Checksum inválido: archivo corrupto")
    return
  }

  fmt.Println("✅ Checksum válido")

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

Casos de Uso

Caso 1: Subida de Imágenes

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

  // Validar tamaño
  maxSize := 5 * 1024 * 1024 // 5 MB
  if len(imageData) > maxSize {
    return fmt.Errorf("imagen muy grande (máx 5MB)")
  }

  // Detectar formato
  contentType := http.DetectContentType(imageData)
  if !strings.HasPrefix(contentType, "image/") {
    return fmt.Errorf("archivo no es una imagen")
  }

  // Comprimir si es necesario
  if len(imageData) > 1*1024*1024 { // > 1MB
    imageData, err = compressImage(imageData)
    if err != nil {
      return err
    }
  }

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

  // Comprimir como JPEG con calidad 80
  var buf bytes.Buffer
  err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80})
  if err != nil {
    return nil, err
  }

  return buf.Bytes(), nil
}

Caso 2: Streaming de Audio

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

// Uso: streaming de micrófono
func streamMicrophone(ns *socketio.Namespace) {
  streamer := NewAudioStreamer(ns)
  streamer.Start()
  defer streamer.Stop()

  // Leer del micrófono (ejemplo simplificado)
  buffer := make([]byte, 4096)
  for {
    // Leer audio (simulado)
    n, err := readAudioFromMic(buffer)
    if err != nil {
      break
    }

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

Caso 3: Descarga de Archivos Grandes

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

    // Verificar si está completo
    if fd.received == fd.total {
      close(fd.done)
    }
  })
}

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

  // Esperar hasta completar
  <-fd.done

  // Ensamblar chunks
  return fd.saveFile()
}

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

  // Escribir chunks en orden
  for i := 0; i < fd.total; i++ {
    chunk, exists := fd.chunks[i]
    if !exists {
      return fmt.Errorf("chunk %d faltante", i)
    }

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

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

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

Patrones Avanzados

Patrón: Chunked Upload con Progreso

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

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

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

  // Enviar 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 en chunk %d: %w", i, err)
    }

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

    // Pequeño delay para no saturar
    time.Sleep(10 * time.Millisecond)
  }

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

  return nil
}

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

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

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

Patrón: Binary con Acknowledgment

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

  // EmitBinary no soporta ACK directamente, usar Emit con encoding manual
  // Alternativamente, usar dos eventos: uno binario y otro para ACK

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

  // 2. Solicitar confirmación
  ns.EmitWithAck("file-confirm", func(response ...interface{}) {
    if response == nil {
      done <- fmt.Errorf("timeout esperando confirmación")
      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
}

Patrón: Binary Compression

import (
  "compress/gzip"
  "bytes"
)

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

  // Agregar info de compresión a metadata
  metadata["compressed"] = true
  metadata["originalSize"] = originalSize
  metadata["compressedSize"] = compressedSize

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

// Recepción y descompresión
client.On("compressed-file", func(data ...interface{}) {
  compressedData := data[0].([]byte)
  metadata := data[1].(map[string]interface{})

  // Verificar si está comprimido
  if compressed, ok := metadata["compressed"].(bool); ok && compressed {
    // Descomprimir
    reader, err := gzip.NewReader(bytes.NewReader(compressedData))
    if err != nil {
      fmt.Println("Error descomprimiendo:", err)
      return
    }
    defer reader.Close()

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

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

    // Procesar datos originales
    processFile(originalData, metadata)
  } else {
    // Procesar datos sin comprimir
    processFile(compressedData, metadata)
  }
})

Mejores Prácticas

1. Validar Tamaño

// ✅ Bueno: validar tamaño antes de enviar
maxSize := 10 * 1024 * 1024 // 10 MB
if len(data) > maxSize {
  return fmt.Errorf("archivo muy grande (máx %d MB)", maxSize/(1024*1024))
}

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

2. Usar Checksums

// ✅ Bueno: siempre incluir checksum
hash := sha256.Sum256(data)
metadata["checksum"] = hex.EncodeToString(hash[:])

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

3. Manejar Errores de Red

// ✅ Bueno: reintentar en caso de 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("Reintento %d/%d...\n", attempt+1, maxRetries)
      time.Sleep(time.Second * time.Duration(attempt+1))
    }
  }

  return fmt.Errorf("falló después de %d intentos", maxRetries+1)
}

4. Usar Chunks para Archivos Grandes

// ✅ Bueno: dividir archivos grandes en chunks
if len(data) > 1*1024*1024 { // > 1MB
  uploadInChunks(ns, data, 64*1024) // Chunks de 64KB
} else {
  ns.EmitBinary("upload", data, metadata)
}

5. Liberar Memoria

// ✅ Bueno: liberar memoria después de usar
data, err := os.ReadFile("large-file.bin")
if err != nil {
  return err
}

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

data = nil // Permitir GC liberar memoria
runtime.GC()

Ver También