Skip to main content

Echo WebSocket Handler

Introduction

WebSockets enable two-way communication between clients and servers over a single, long-lived connection. Unlike traditional HTTP requests, WebSockets remain open, allowing for efficient real-time data transfer with minimal latency.

An Echo WebSocket Handler is one of the simplest WebSocket implementations you can create using the Echo framework. It "echoes" or reflects messages back to the client that sent them—an excellent starting point for understanding WebSocket communication patterns.

This guide will walk you through creating an echo WebSocket handler, explaining how it works, and demonstrating practical applications.

Prerequisites

Before we begin, ensure you have:

  • Go installed on your system
  • Basic knowledge of Go programming
  • Echo framework installed (go get github.com/labstack/echo/v4)
  • The Gorilla WebSocket package (go get github.com/gorilla/websocket)

Basic Echo WebSocket Handler

Let's start by creating a simple echo WebSocket handler using Echo:

go
package main

import (
"github.com/labstack/echo/v4"
"github.com/gorilla/websocket"
"net/http"
)

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow connections from any origin for this example
},
}

func handleWebSocket(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()

for {
// Read message from browser
msgType, msg, err := ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
c.Logger().Error(err)
}
break
}

// Print the message to the console
c.Logger().Infof("Received: %s", msg)

// Echo the message back to the client
err = ws.WriteMessage(msgType, msg)
if err != nil {
c.Logger().Error(err)
break
}
}

return nil
}

func main() {
e := echo.New()

e.GET("/ws", handleWebSocket)
e.Static("/", "public")

e.Logger.Fatal(e.Start(":8080"))
}

Understanding the Code Step by Step

  1. WebSocket Upgrader: The upgrader converts a standard HTTP connection to a WebSocket connection. We set CheckOrigin to allow connections from any origin (for development purposes).

  2. Handler Function: handleWebSocket is our Echo handler that:

    • Upgrades the connection from HTTP to WebSocket
    • Establishes a read/response loop
    • Closes the connection when finished using defer
  3. Message Loop: Inside the handler:

    • We read incoming messages with ws.ReadMessage()
    • Log the message for debugging
    • Send the exact same message back with ws.WriteMessage()
    • Handle connection closure and errors
  4. Echo Setup: In the main() function:

    • Create an Echo instance
    • Register our WebSocket handler at the /ws endpoint
    • Serve static files from the "public" directory
    • Start the server on port 8080

Frontend Implementation

To connect to our WebSocket server, we need a frontend client. Here's a simple HTML page with JavaScript:

html
<!DOCTYPE html>
<html>
<head>
<title>Echo WebSocket Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#messages {
border: 1px solid #ccc;
height: 300px;
margin-bottom: 20px;
overflow-y: scroll;
padding: 10px;
}
.message {
margin-bottom: 5px;
}
.sent {
color: blue;
}
.received {
color: green;
}
</style>
</head>
<body>
<h1>Echo WebSocket Demo</h1>
<div id="messages"></div>

<input type="text" id="message" placeholder="Type a message..." />
<button id="sendBtn">Send</button>
<div id="status">Disconnected</div>

<script>
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message');
const sendBtn = document.getElementById('sendBtn');
const statusDiv = document.getElementById('status');

let socket;

function connect() {
statusDiv.textContent = 'Connecting...';

// Create WebSocket connection
socket = new WebSocket('ws://localhost:8080/ws');

// Connection opened
socket.addEventListener('open', (event) => {
statusDiv.textContent = 'Connected';
sendBtn.disabled = false;
});

// Listen for messages
socket.addEventListener('message', (event) => {
const message = document.createElement('div');
message.classList.add('message', 'received');
message.textContent = `Received: ${event.data}`;
messagesDiv.appendChild(message);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
});

// Connection closed or error
socket.addEventListener('close', (event) => {
statusDiv.textContent = 'Disconnected';
sendBtn.disabled = true;

// Try to reconnect after 5 seconds
setTimeout(connect, 5000);
});

socket.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
statusDiv.textContent = 'Error: Check console for details';
});
}

// Send message when button is clicked
sendBtn.addEventListener('click', () => {
const text = messageInput.value;
if (text && socket && socket.readyState === WebSocket.OPEN) {
socket.send(text);

const message = document.createElement('div');
message.classList.add('message', 'sent');
message.textContent = `Sent: ${text}`;
messagesDiv.appendChild(message);
messagesDiv.scrollTop = messagesDiv.scrollHeight;

messageInput.value = '';
}
});

// Also send on Enter key
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendBtn.click();
}
});

// Connect on page load
connect();
</script>
</body>
</html>

Save this file as public/index.html to be served by Echo's static file server.

Expected Behavior

When you run the server and access http://localhost:8080 in your browser:

  1. The page will automatically attempt to establish a WebSocket connection
  2. Once connected, you can type messages in the input box
  3. When you send a message, you'll see it added to the message list
  4. The server will receive your message and echo it back
  5. You'll see the echoed message appear with a "Received" label

Enhanced Echo WebSocket Handler

Let's enhance our basic handler with more features:

go
package main

import (
"fmt"
"time"
"github.com/labstack/echo/v4"
"github.com/gorilla/websocket"
"net/http"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

type WebSocketMessage struct {
Type string `json:"type"`
Content string `json:"content"`
Time string `json:"time"`
}

func handleWebSocket(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()

// Send welcome message
welcome := WebSocketMessage{
Type: "server",
Content: "Welcome to the Echo WebSocket Server!",
Time: time.Now().Format(time.RFC3339),
}

err = ws.WriteJSON(welcome)
if err != nil {
c.Logger().Error(err)
return nil
}

// Set read deadline to detect stale connections
ws.SetReadDeadline(time.Now().Add(60 * time.Second))

// Configure the pong handler to extend read deadline
ws.SetPongHandler(func(string) error {
ws.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})

// Start a ping ticker to keep connection alive
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

// Handle ping in a separate goroutine
go func() {
for range ticker.C {
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
return
}
}
}()

for {
// Read message from client
var msg WebSocketMessage
err := ws.ReadJSON(&msg)
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
c.Logger().Error(err)
}
break
}

// Process message
c.Logger().Infof("Received: %+v", msg)

// Echo back with server timestamp
response := WebSocketMessage{
Type: "echo",
Content: fmt.Sprintf("Echo: %s", msg.Content),
Time: time.Now().Format(time.RFC3339),
}

err = ws.WriteJSON(response)
if err != nil {
c.Logger().Error(err)
break
}
}

return nil
}

func main() {
e := echo.New()

e.GET("/ws", handleWebSocket)
e.Static("/", "public")

e.Logger.Fatal(e.Start(":8080"))
}

Enhancements Explained

  1. Structured Messages: We use a WebSocketMessage struct for structured communication with type, content, and timestamp.

  2. Welcome Message: The server now sends a welcome message when clients connect.

  3. Connection Keep-Alive:

    • We set read deadlines to detect stale connections
    • Implement ping/pong to keep connections alive
    • This is essential for long-lived WebSocket connections
  4. JSON Handling: Using ReadJSON() and WriteJSON() for cleaner, structured data exchange.

Real-World Applications

Echo WebSocket Handlers have many practical applications:

1. Chat Application

Expand the echo handler to broadcast messages to all connected clients:

go
package main

import (
"sync"
"github.com/labstack/echo/v4"
"github.com/gorilla/websocket"
)

var (
clients = make(map[*websocket.Conn]bool)
mutex = &sync.Mutex{}
)

func broadcastHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}

// Register new client
mutex.Lock()
clients[ws] = true
mutex.Unlock()

defer func() {
ws.Close()
// Remove client when disconnected
mutex.Lock()
delete(clients, ws)
mutex.Unlock()
}()

for {
// Read message
_, msg, err := ws.ReadMessage()
if err != nil {
break
}

// Broadcast to all clients
mutex.Lock()
for client := range clients {
err := client.WriteMessage(websocket.TextMessage, msg)
if err != nil {
client.Close()
delete(clients, client)
}
}
mutex.Unlock()
}

return nil
}

2. Real-Time Dashboard Updates

WebSockets can push updates to dashboards or monitoring interfaces:

go
func dashboardHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
select {
case <-ticker.C:
// Collect system metrics
metrics := collectSystemMetrics()

// Send update
err := ws.WriteJSON(metrics)
if err != nil {
return nil
}

case <-c.Request().Context().Done():
return nil
}
}
}

func collectSystemMetrics() map[string]interface{} {
return map[string]interface{}{
"cpu_usage": rand.Float64() * 100,
"memory_usage": rand.Float64() * 100,
"requests": rand.Intn(1000),
"time": time.Now().Format(time.RFC3339),
}
}

3. Collaborative Editing

WebSockets can sync changes between multiple editors:

go
type DocumentChange struct {
DocumentID string `json:"documentId"`
Change string `json:"change"`
Position int `json:"position"`
UserID string `json:"userId"`
Timestamp int64 `json:"timestamp"`
}

var (
documents = make(map[string]string)
docMutex = &sync.RWMutex{}
)

func documentEditHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()

// Extract document ID from query param
docID := c.QueryParam("document")
if docID == "" {
ws.WriteJSON(map[string]string{"error": "No document ID provided"})
return nil
}

// Create document if doesn't exist
docMutex.Lock()
if _, exists := documents[docID]; !exists {
documents[docID] = ""
}
docMutex.Unlock()

// Handle client messages
for {
var change DocumentChange
err := ws.ReadJSON(&change)
if err != nil {
break
}

// Apply change to document
docMutex.Lock()
doc := documents[docID]
// Simple append for this example (real implementation would be more complex)
documents[docID] = doc + change.Change
docMutex.Unlock()

// Broadcast change to all clients editing this document
broadcastDocumentChange(docID, change)
}

return nil
}

Best Practices for Echo WebSocket Handlers

  1. Error Handling: Always handle connection errors gracefully

  2. Connection Management:

    • Keep track of active connections
    • Implement proper cleanup on disconnect
    • Use sync.Mutex when accessing shared resources
  3. Keep-Alive Mechanism:

    • Implement ping/pong to detect disconnections
    • Set appropriate timeouts
  4. Message Validation:

    • Validate incoming messages
    • Add proper error responses
  5. Rate Limiting:

    • Prevent clients from flooding your server
    • Implement backoff strategies
  6. Structured Messages:

    • Use defined message formats (JSON structures)
    • Include message types for easy handling
  7. Authentication and Authorization:

    • Authenticate users before establishing WebSocket connections
    • Check permissions for specific actions

Here's an example implementing rate limiting:

go
func rateLimitedHandler(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()

// Create rate limiter - 5 messages per second
limiter := rate.NewLimiter(5, 5)

for {
// Check rate limit before processing
if !limiter.Allow() {
ws.WriteJSON(map[string]string{
"error": "Rate limit exceeded. Please slow down.",
})
time.Sleep(time.Second)
continue
}

// Read and process message
_, msg, err := ws.ReadMessage()
if err != nil {
break
}

// Echo back
err = ws.WriteMessage(websocket.TextMessage, msg)
if err != nil {
break
}
}

return nil
}

Common Issues and Solutions

1. Connection Closing Unexpectedly

Problem: WebSocket connections end abruptly.

Solution: Implement ping/pong mechanisms and proper error handling:

go
// Set ping handler
ws.SetPingHandler(func(appData string) error {
err := ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second))
return err
})

// Start ping worker
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()

go func() {
for range ticker.C {
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil {
return
}
}
}()

2. Handling Large Messages

Problem: Large messages can cause memory issues.

Solution: Set appropriate buffer sizes and message limits:

go
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

// Set maximum message size
ws.SetReadLimit(10 * 1024 * 1024) // 10MB

Summary

The Echo WebSocket Handler provides a powerful way to create real-time, bidirectional communication in web applications. In this guide, we've covered:

  1. How to create a basic echo WebSocket handler
  2. Enhancing handlers with structured messages and connection management
  3. Real-world applications like chat, dashboards, and collaborative editing
  4. Best practices for WebSocket implementation
  5. Common issues and solutions

WebSockets open up possibilities for creating interactive, responsive applications that traditional HTTP requests cannot match. By starting with a simple echo handler and building upon it, you can develop complex real-time features for your web applications.

Additional Resources

Exercises

  1. Echo with Transformation: Modify the echo handler to transform messages (e.g., convert text to uppercase) before sending them back.

  2. Authenticated WebSockets: Add JWT authentication to your WebSocket handler to only allow authenticated users.

  3. Broadcasting with Rooms: Implement a chat system with different "rooms" where messages are only broadcast to users in the same room.

  4. Persistent History: Store the last 10 messages for each WebSocket room and send them to new users when they connect.

  5. Typing Indicators: Implement "user is typing" indicators that notify other users when someone is composing a message.

By practicing these exercises, you'll gain hands-on experience with Echo WebSocket handlers and develop your skills in creating real-time web applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)