Skip to main content

Command Palette

Search for a command to run...

๐Ÿ’ฌ Building a Real-Time WebSocket Broadcast Server in Go

Published
โ€ข10 min read
๐Ÿ’ฌ Building a Real-Time WebSocket Broadcast Server in Go

In this post, Iโ€™ll walk you through how I built a real-time WebSocket broadcast server and client in Go โ€” a simple chat system that lets multiple clients connect and exchange messages instantly from the terminal.

This project combines:

  • ๐Ÿง  Goroutines for concurrency

  • ๐Ÿ” Channels for message communication

  • ๐Ÿ”’ Mutexes for safe shared access

  • ๐ŸŒ Gorilla WebSocket for real-time communication

  • ๐Ÿ’ป Cobra CLI for a clean command-line interface

๐Ÿ“˜ Project idea from:
๐Ÿ”— https://roadmap.sh/projects/broadcast-server

๐Ÿ“‚ Repo:
๐Ÿ”— https://github.com/pranav767/GO_PROJECTS/tree/main/broadcast-server

โš™๏ธ What the Project Does

This project acts as both a server and a client โ€” depending on which command you run.

  • The server accepts WebSocket connections and broadcasts messages to all connected clients.

  • The clients connect from the terminal and send/receive messages in real-time.

You can:

  • Chat with multiple users via terminal

  • See join/leave system notifications

  • Change your display name using /name NewName

  • Quit with /quit

๐Ÿงฑ Project Structure

broadcast-server/
โ”œโ”€โ”€ cmd/
โ”‚   โ”œโ”€โ”€ connect.go     # CLI for client
โ”‚   โ”œโ”€โ”€ root.go        # Root Cobra command
โ”‚   โ””โ”€โ”€ start.go       # CLI for server
โ”œโ”€โ”€ client/
โ”‚   โ””โ”€โ”€ client.go      # WebSocket client logic
โ”œโ”€โ”€ server/
โ”‚   โ””โ”€โ”€ server.go      # WebSocket server logic
โ”œโ”€โ”€ main.go            # Entry point
โ””โ”€โ”€ go.mod             # Dependencies

๐Ÿง  Code Flow (High-Level)

  1. The server starts and listens for /ws connections.

  2. Each connected client runs in its own goroutine.

  3. When a client sends a message:

    • Itโ€™s placed on a channel (broadcast).
  4. A separate goroutine (handleMessages) listens to that channel.

  5. The message is broadcasted to all clients concurrently.

  6. Mutex locks protect shared data (clients map) during writes/removals.

What are Web Sockets?

A WebSocket is a communication Protocol providing full duplex, real-time communication between a server & client over a single persistent TCP Connection. After an initial HTTP handshake, the connection is upgraded to a WebSocket , allowing both the client & server to send messages independently & simultaneously.

What are Golang Routines

A GoRoutine is a lightweight thread of execution. The way we normally use functions is

package main
import (
    "fmt"
    "time"
)
func f(from string) {
    for i := range 3 {
        fmt.Println(from, ":", i)
    }
}
func main() {
    f("direct")

    func (msg string) {
        fmt.Println(msg)
    }("going")
}
// this will print
// direct : 1
// direct : 2
// direct : 3
// Going

This is the traditional way we execute/write functions, if you look at the function flow, when it starts with main function, it executes f(โ€œdirectโ€œ) & will wait untill this is finished, after finishing the execution of this function it goes with the execution of next anoymous function, but what if we donโ€™t want to wait for the first function to complete and keep
It allows you to execute functions concurrently

๐Ÿงฉ main.go

Entry point for the application

// Package main is the entry point for the broadcast server application.
package main

import "broadcast-server/cmd"

func main() {
    cmd.Execute()
}
  • Explanation:

    • Imports the cmd package (which contains all command definitions).

    • Calls cmd.Execute() โ€” Cobraโ€™s entry point for parsing commands.

    • Based on the subcommand (start or connect), it routes execution accordingly.

    • Keeps main.go minimal โ€” this is a Go best practice.

๐Ÿ’ก Your binary can act as both server and client, depending on which command you run.

โš™๏ธ cmd/root.go

Initializes the Cobra command-line interface.

// Package cmd provides the command-line interface for the broadcast server application.
package cmd

import (
    "os"

    "github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
    Use:   "broadcast-server",
    Short: "A WebSocket broadcast server and client",
    Long: `A real-time chat application using WebSocket technology.
This application can be run either as a server to accept connections
or as a client to connect to an existing server.`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
    err := rootCmd.Execute()
    if err != nil {
        os.Exit(1)
    }
}
  • Cobra provides structured subcommands like:

    • broadcast-server start

    • broadcast-server connect

  • Handles flag parsing, validation, and help text.

    • Use โ€” defines how to run the command (broadcast-server).

    • Short โ€” one-line summary used in help.

    • Long โ€” multi-line description printed in CLI help output.

    • rootCmd acts as the parent to start and connect commands.

  • Execute() runs the CLI and parses arguments.

  • If user runs an invalid command or thereโ€™s a parsing error, it exits cleanly.

๐Ÿ’ก Cobraโ€™s design helps you keep your CLI code structured and scalable.

๐Ÿงญ cmd/start.go

Defines the server command

package cmd

import (
    "broadcast-server/server"

    "github.com/spf13/cobra"
)

// startCmd represents the start command
var startCmd = &cobra.Command{
    Use:   "start",
    Short: "Start the WebSocket broadcast server",
    Long: `Start the WebSocket broadcast server and begin accepting client connections.
The server will listen for incoming WebSocket connections and broadcast
messages between all connected clients.`,
    Run: func(cmd *cobra.Command, args []string) {
        host, _ := cmd.Flags().GetString("host")
        port, _ := cmd.Flags().GetInt("port")
        server.StartServer(host, port)
    },
}

func init() {
    rootCmd.AddCommand(startCmd)
    startCmd.Flags().StringP("host", "H", "localhost", "Host address to bind to")
    startCmd.Flags().IntP("port", "p", 8080, "Port to listen on")
}
  • Use โ€” command name (start).

  • Short & Long โ€” describe what this command does.

  • Run โ€” main function executed when this command is called.

Inside the Run function:

  • Reads CLI flags:

    • --host (default: localhost)

    • --port (default: 8080)

  • Passes them to server.StartServer(host, port).

  • This call starts the WebSocket server and begins listening for connections.

  • Adds --host and --port flags with short forms (-H, -p).

  • Cobra automatically includes these in help output.

๐Ÿ’ก Running go run main.go start -H 0.0.0.0 -p 9000 binds the server to port 9000.

๐Ÿ”Œ cmd/connect.go

Defines the client command

package cmd

import (
    "broadcast-server/client"
    "fmt"

    "github.com/spf13/cobra"
)

var connectCmd = &cobra.Command{
    Use:   "connect",
    Short: "Connect to the broadcast server",
    Long:  `Connect to the WebSocket broadcast server as a client`,
    Run: func(cmd *cobra.Command, args []string) {
        host, _ := cmd.Flags().GetString("host")
        port, _ := cmd.Flags().GetInt("port")
        name, _ := cmd.Flags().GetString("name")

        fmt.Printf("Connecting to %s:%d as %s\n", host, port, name)
        client.StartClient(host, port, name)
    },
}

func init() {
    rootCmd.AddCommand(connectCmd)

    connectCmd.Flags().StringP("host", "H", "localhost", "Server host address")
    connectCmd.Flags().IntP("port", "p", 8080, "Server port")
    connectCmd.Flags().StringP("name", "n", "anonymous", "Client name")
}
  • Use โ€” defines connect command.

  • Short & Long โ€” describe what this command does.

  • Run โ€” executes when the user runs broadcast-server connect.

Inside Run:

  • Reads CLI flags:

    • --host or -H: WebSocket server host (default: localhost)

    • --port or -p: Port to connect (default: 8080)

    • --name or -n: Username for the client (default: anonymous)

  • Prints the connection details for user feedback.

  • Calls client.StartClient(host, port, name) to initialize the WebSocket client logic.

๐Ÿ’ก Each terminal acts as one chat client.

  • Registers flags for host, port, and name.

  • Automatically integrates with Cobraโ€™s help system.

๐Ÿ’ก Running go run main.go connect -n Alice opens a chat session as Alice.

๐Ÿงฎ Server Code (server/server.go)

The WebSocket broadcast server logic.

๐Ÿงฑ Key Variables

var clients = make(map[*Client]bool) // Track all clients
var mu sync.Mutex                   // Protect shared access
var broadcast = make(chan []byte)   // Message channel
  • clients โ€” map of all connected clients.

  • mu โ€” ensures thread-safe read/write access.

  • broadcast โ€” channel to send messages to all clients.

๐Ÿ“ฅ handleConnection()

Handles new WebSocket client connections.

func handleConnection(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    conn, _ := upgrader.Upgrade(w, r, nil)
    client := &Client{conn: conn, name: name}

    mu.Lock()
    clients[client] = true
    broadcast <- []byte(fmt.Sprintf("System: %s joined", client.name))
    mu.Unlock()

    for {
        _, msg, err := client.conn.ReadMessage()
        if err != nil {
            mu.Lock()
            delete(clients, client)
            broadcast <- []byte(fmt.Sprintf("System: %s left", client.name))
            mu.Unlock()
            break
        }
        if len(msg) > 6 && string(msg[:6]) == "/name " {
            old := client.name
            client.name = string(msg[6:])
            broadcast <- []byte(fmt.Sprintf("System: %s is now known as %s", old, client.name))
            continue
        }
        broadcast <- []byte(fmt.Sprintf("%s: %s", client.name, string(msg)))
    }
}

What happens here:

  • Converts HTTP โ†’ WebSocket connection using Gorilla WebSocket.

  • Extracts the name query parameter (?name=Alice).

  • Creates a new Client struct with the connection and name.

  • Adds client to active list (clients map).

  • Locks the mutex before modifying shared clients map.

  • Adds client to map and broadcasts join message.

  • Unlocks mutex after operation.

Then enters an infinite loop to listen for messages:

  • Waits for messages from client.

  • If connection closes or error occurs โ†’ removes client safely and announces departure.

Handles /name command:

  • Broadcasts โ€œjoinedโ€ message.

  • Waits for messages:

    • If /name, updates user name.

    • Otherwise sends message to broadcast channel.

  • On disconnect:

    • Removes client safely using mutex.

    • Broadcasts โ€œleft the chatโ€.

  • Upgrades the HTTP connection to a WebSocket connection using Gorilla WebSocket.

๐Ÿ’ก Each client connection runs in its own goroutine, so multiple users can chat concurrently.


๐Ÿ“ค handleMessages()

Broadcast messages to all connected clients.

func handleMessages() {
    for {
        msg := <-broadcast
        mu.Lock()
        for client := range clients {
            client.conn.WriteMessage(websocket.TextMessage, msg)
        }
        mu.Unlock()
    }
}
  • Continuously reads messages from the broadcast channel.

  • Locks clients to safely iterate over all connected users.

  • Sends (WriteMessage) the message to each clientโ€™s WebSocket.

  • Removes any disconnected client.

๐Ÿ’ก This function is the broadcaster loop โ€” every message from any user flows through here.


๐Ÿš€ StartServer()

func StartServer(host string, port int) {
    http.HandleFunc("/ws", handleConnection)
    go handleMessages()

    addr := fmt.Sprintf("%s:%d", host, port)
    fmt.Printf("Server started at ws://%s/ws\n", addr)
    log.Fatal(http.ListenAndServe(addr, nil))
}

Explanation:

  • Registers /ws endpoint.

  • Launches broadcaster (handleMessages()) as goroutine.

  • Starts HTTP server to listen for WebSocket connections.

๐Ÿ’ก Server ready to broadcast!

๐Ÿ’ป Client Code (client/client.go)

The WebSocket client logic that connects to the server and interacts in real time.

๐Ÿ”Œ StartClient()

addr := fmt.Sprintf("ws://%s:%d/ws?name=%s", host, port, name)
conn, _, err := websocket.DefaultDialer.Dial(addr, nil)
defer conn.Close()

๐Ÿง  Message Receiver (Goroutine)

go func() {
    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            log.Println("read error: ", err)
            return
        }
        fmt.Printf("\n[Broadcast] %s\n>", string(msg))
    }
}()
  • Runs in a separate goroutine for asynchronous reading.

  • Constantly listens for messages from the server.

  • Prints every broadcast message prefixed with [Broadcast].


โŒจ๏ธ User Input Loop

scanner := bufio.NewScanner(os.Stdin)
fmt.Print(">")
for scanner.Scan() {
    text := scanner.Text()
    if text == "/quit" {
        fmt.Println("Exiting...")
        return
    }
    conn.WriteMessage(websocket.TextMessage, []byte(text))
    fmt.Print(">")
}
  • Reads user input line by line using bufio.Scanner.

  • If /quit, exits cleanly.

  • Otherwise, sends message as WebSocket text frame to server.

๐Ÿ’ก This loop runs concurrently with the receiver goroutine, allowing full-duplex communication.


๐Ÿ“ฆ Libraries Used

LibraryPurpose
github.com/gorilla/websocketReal-time WebSocket connections
github.com/spf13/cobraCommand-line interface
sync, net/http, fmt, bufio, osStandard Go libraries

๐ŸŒ€ Goroutines

  • Lightweight threads managed by Goโ€™s runtime.

  • Run concurrently using go keyword:

      go handleMessages()
    
  • Here used for:

    • Each client connection (handleConnection).

    • Message broadcaster (handleMessages).

    • Client reader goroutine.

๐Ÿ’ก Goroutines make the chat system concurrent and non-blocking.

๐Ÿ“ฌ Golang Channels

  • Channels provide safe communication between goroutines.

  • Created using:

      broadcast := make(chan []byte)
    
  • Used for:

    • Sending messages from clients to broadcaster:

        broadcast <- []byte("Alice: Hello")
      
    • Receiving messages to forward to all clients:

        msg := <-broadcast
      

๐Ÿ’ก Channels are like Goโ€™s internal message queues.


๐Ÿ”’ Golang Mutex

  • Ensures safe access to shared variables between goroutines.

  • Used when updating the clients map:

      mu.Lock()
      clients[client] = true
      mu.Unlock()
    
  • Prevents race conditions (when two goroutines modify data simultaneously).

๐Ÿ’ก Mutex = mutual exclusion. Only one goroutine modifies data at a time.


๐ŸŒ Gorilla WebSocket

  • Popular Go library implementing the WebSocket protocol.

  • Enables real-time, bi-directional communication over TCP.

  • In server:

      upgrader := websocket.Upgrader{
          CheckOrigin: func(r *http.Request) bool { return true },
      }
      conn, _ := upgrader.Upgrade(w, r, nil)
    
  • In client:

      conn, _, _ := websocket.DefaultDialer.Dial(addr, nil)
    

๐Ÿ’ก Turns HTTP into persistent, full-duplex WebSocket connections.

๐Ÿš€ How to Run

Start the Server:

go run main.go start

Connect Clients:

go run main.go connect --name "Alice"
go run main.go connect --name "Bob"

Now you have a real-time terminal chat ๐ŸŽ‰

โš™๏ธ Example Output

Server:

Server started at ws://localhost:8080/ws
Broadcasting: System: Alice joined
Broadcasting: Alice: Hello everyone

Client 1 (Alice):

>Hello everyone
[Broadcast] Bob: Hi Alice!

Client 2 (Bob):

[Broadcast] System: Alice joined the chat
Hi Alice!

๐Ÿงฐ Key Learnings

How WebSockets enable real-time communication
โœ… How goroutines enable parallelism
โœ… How channels pass messages between goroutines
โœ… How mutexes keep shared state consistent
โœ… How to build a CLI with Cobra

This project shows how Goโ€™s concurrency features work seamlessly with WebSockets โ€” perfect for chat apps, live dashboards, or multiplayer systems.

This is it for websockets.