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¶
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()