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?
- Envío de Datos Binarios
- Recepción de Datos Binarios
- Casos de Uso
- Patrones Avanzados
¿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¶
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()