DEV Community

Taverne Tech
Taverne Tech

Posted on • Edited on

WebSockets in Go: Building Lightning-Fast Chat ⚡

In This Article

  1. Setting Up Your Go WebSocket Foundation
  2. Building the Chat Server Brain
  3. Creating the Frontend Magic

Introduction

Remember the days when web pages were as static as a Victorian portrait? Well, buckle up buttercup, because we're about to make them chattier than your neighbor's cat! 🐱

If HTTP requests were letters sent by carrier pigeon, WebSockets would be a direct telephone hotline to your server. Today, we're diving into the wonderful world of real-time communication using Go and WebSockets to build a chat application that updates faster than you can say "typing indicator."

Why Go for this task? Because Go handles concurrent connections like a Swiss army knife handles... well, everything! With goroutines weighing in at just 2KB of initial stack size, you can handle millions of concurrent connections on a single machine. That's more connections than a networking conference! 🤝

1. Setting Up Your Go WebSocket Foundation 🔧

Think of WebSockets as upgrading from smoke signals to a dedicated phone line. Once established, both client and server can send messages bidirectionally without the overhead of HTTP headers flying around like confetti at a New Year's party.

Here's the magic setup using the fantastic gorilla/websocket package:

package main

import (
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // Be careful in production!
    },
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    // Upgrade HTTP connection to WebSocket
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Failed to upgrade connection: %v", err)
        return
    }
    defer conn.Close()

    log.Println("Client connected! 🎉")

    // Handle messages in a goroutine
    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Printf("Error reading message: %v", err)
            break
        }

        log.Printf("Received: %s", message)

        // Echo the message back
        err = conn.WriteMessage(messageType, message)
        if err != nil {
            log.Printf("Error writing message: %v", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/ws", handleWebSocket)
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Fun fact: The WebSocket handshake involves a magic string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" that gets concatenated with a client key and hashed with SHA-1. It's like a secret handshake, but nerdier! 🤓

2. Building the Chat Server Brain 🎯

Now let's create the hub pattern - think of it as the switchboard operator in an old-timey hotel, connecting calls and making sure everyone gets their messages. This pattern is essential for managing multiple connections and broadcasting messages to all connected clients.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "github.com/gorilla/websocket"
)

type Message struct {
    Username string `json:"username"`
    Content  string `json:"content"`
    Type     string `json:"type"` // "message", "join", "leave"
}

type Client struct {
    hub      *Hub
    conn     *websocket.Conn
    send     chan []byte
    username string
}

type Hub struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
}

func newHub() *Hub {
    return &Hub{
        clients:    make(map[*Client]bool),
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}

func (h *Hub) run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
            log.Printf("Client %s joined! Total: %d", client.username, len(h.clients))

            // Notify others about new user
            joinMsg := Message{
                Username: client.username,
                Content:  "joined the chat",
                Type:     "join",
            }
            if data, err := json.Marshal(joinMsg); err == nil {
                h.broadcast <- data
            }

        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
                log.Printf("Client %s left! Total: %d", client.username, len(h.clients))

                // Notify others about user leaving
                leaveMsg := Message{
                    Username: client.username,
                    Content:  "left the chat",
                    Type:     "leave",
                }
                if data, err := json.Marshal(leaveMsg); err == nil {
                    h.broadcast <- data
                }
            }

        case message := <-h.broadcast:
            // Send message to all connected clients
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()

    for {
        var msg Message
        err := c.conn.ReadJSON(&msg)
        if err != nil {
            log.Printf("Error reading JSON: %v", err)
            break
        }

        msg.Username = c.username
        msg.Type = "message"

        if data, err := json.Marshal(msg); err == nil {
            c.hub.broadcast <- data
        }
    }
}

func (c *Client) writePump() {
    defer c.conn.Close()

    for {
        select {
        case message, ok := <-c.send:
            if !ok {
                c.conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }
            c.conn.WriteMessage(websocket.TextMessage, message)
        }
    }
}

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

func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Failed to upgrade: %v", err)
        return
    }

    username := r.URL.Query().Get("username")
    if username == "" {
        username = "Anonymous"
    }

    client := &Client{
        hub:      hub,
        conn:     conn,
        send:     make(chan []byte, 256),
        username: username,
    }

    client.hub.register <- client

    // Start goroutines for reading and writing
    go client.writePump()
    go client.readPump()
}

func main() {
    hub := newHub()
    go hub.run()

    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        handleWebSocket(hub, w, r)
    })

    // Serve static files
    http.Handle("/", http.FileServer(http.Dir("./static/")))

    log.Println("Chat server starting on :8080 🚀")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

This hub is like having a gossip center in a small town - it knows everyone, spreads the news instantly, and keeps track of who's coming and going! The beauty of Go's channels here is that they handle all the synchronization for us, making concurrent programming feel like a walk in the park. 🌳

3. Creating the Frontend Magic 💻

Now for the frontend - where the rubber meets the road, or should I say, where the JavaScript meets the WebSocket! This is simpler than you might think:

<!DOCTYPE html>
<html>
<head>
    <title>Go Chat App</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #messages { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 10px; margin-bottom: 10px; }
        #messageInput { width: 70%; padding: 5px; }
        #sendButton { padding: 5px 10px; }
        .join { color: green; font-style: italic; }
        .leave { color: red; font-style: italic; }
        .message { margin: 5px 0; }
    </style>
</head>
<body>
    <h1>Go WebSocket Chat 💬</h1>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="Type your message..." />
    <button id="sendButton">Send</button>

    <script>
        const username = prompt("Enter your username:") || "Anonymous";
        const ws = new WebSocket(`ws://localhost:8080/ws?username=${encodeURIComponent(username)}`);
        const messages = document.getElementById('messages');
        const messageInput = document.getElementById('messageInput');
        const sendButton = document.getElementById('sendButton');

        ws.onopen = function(event) {
            console.log('Connected to chat server! 🎉');
            addMessage('System', 'Connected to chat!', 'system');
        };

        ws.onmessage = function(event) {
            const message = JSON.parse(event.data);
            addMessage(message.username, message.content, message.type);
        };

        ws.onclose = function(event) {
            console.log('Disconnected from chat server 😢');
            addMessage('System', 'Disconnected from chat', 'system');
        };

        ws.onerror = function(error) {
            console.error('WebSocket error:', error);
            addMessage('System', 'Connection error occurred', 'error');
        };

        function addMessage(username, content, type) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${type}`;

            if (type === 'join' || type === 'leave') {
                messageDiv.innerHTML = `<strong>${username}</strong> ${content}`;
            } else {
                messageDiv.innerHTML = `<strong>${username}:</strong> ${content}`;
            }

            messages.appendChild(messageDiv);
            messages.scrollTop = messages.scrollHeight;
        }

        function sendMessage() {
            const content = messageInput.value.trim();
            if (content && ws.readyState === WebSocket.OPEN) {
                const message = {
                    content: content,
                    type: 'message'
                };
                ws.send(JSON.stringify(message));
                messageInput.value = '';
            }
        }

        sendButton.addEventListener('click', sendMessage);
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Did you know? WebSocket frames use a masking mechanism for client-to-server messages to prevent cache poisoning attacks. It's like wearing a disguise to a masquerade ball, but for security! 🎭

The JavaScript WebSocket API is surprisingly straightforward - it's like having a direct hotline to your Go server. No more HTTP request-response cycles; just pure, unfiltered, real-time communication!

Conclusion

And there you have it! We've built a real-time chat application that's more responsive than a golden retriever hearing a treat bag open. 🐕
We covered the WebSocket foundation in Go, implemented the hub pattern for managing multiple concurrent connections, and created a simple but effective frontend that brings it all together. The beauty of Go's goroutines and channels makes handling thousands of concurrent connections feel like a breeze.
Key takeaways:

  • Go's lightweight goroutines make it perfect for concurrent WebSocket connections
  • The hub pattern centralizes connection management and message broadcasting
  • WebSockets provide true bidirectional, real-time communication
  • Channels in Go handle synchronization like a boss

What features would you add to make this chat application truly yours? Ready to scale this to handle thousands of concurrent users? The foundation is solid - now it's time to build your chat empire! 🏰


buy me a coffee

Top comments (0)