DEV Community

Aswath S for Gopher wizards

Posted on with Adarsh A • Updated on

Simple Go Chat Application in under 100 lines of code - Part 1

This blog is the first part of a multi part series.

Yes you read it right! This blog provides a comprehensive guide on creating a straightforward broadcast chat application in under 100 lines of Golang code.

The blog is structured into two parts.

The initial segment delves into the implementation of a chat application using the Gorilla websockets package in Golang, accompanied by a basic static HTML page for the chat interface. In the subsequent section, we will explore enhancing the scalability of the application by incorporating Redis pub-sub.

Why Websockets over HTTP ?

  • Websockets offer bidirectional, low-latency communication, ideal for real-time applications, unlike HTTP's request-response nature.
  • This choice eliminates the need for polling for new messages from the user interface.

Let’s dive into how we can implement a websocket based chat application in Golang.

Let’s start by creating a directory called go_chat and initializing a new go module.

mkdir go_chat
cd go_chat
go mod init go_chat
Enter fullscreen mode Exit fullscreen mode

Let’s install the dependencies using go get

go get github.com/gin-gonic/gin
go get github.com/gorilla/websocket
Enter fullscreen mode Exit fullscreen mode

Let's write a simple main.go file with a main() function that initializes a Gin server.

package main

import (
    "log"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    err := router.Run()
    if err != nil {
        log.Fatalf("Unable to start server. Error %v", err)
    }
    log.Println("Server started successfully.")
}

Enter fullscreen mode Exit fullscreen mode

Next, let’s write a gin handler to handle websocket connections

func serveWs(c *gin.Context) {

    upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("Error in upgrading web socket. Error: %v", err)
        return
    }

    go handleClient(conn)
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the serveWs function.

  • Websockets start with an HTTP handshake, then upgrade to a persistent TCP connection for full-duplex communication. In order to do this, we need to upgrade the HTTP connection using websocket.Upgrader from the gorilla websockets package.
  • The websocket.Upgrader accepts a CheckOrigin function to handle CORS. Note: For demonstration purposes, we have implemented the CheckOrigin function to always return true. (Refrain from implementing this in a production setting unless you're willing to expose yourself to potential CSRF attacks).
  • The Upgrade function takes in an http.ResponseWriter and an http.Request, and returns a websocket.Conn and an error if there is one.

Now that we have the websocket connection, let’s write a goroutine handles the connection.

var clients = make(map[*websocket.Conn]struct{})

type Message struct {
    From    string `json:"from"`
    Message string `json:"message"`
}

func handleClient(c *websocket.Conn) {
    defer func() {
        delete(clients, c)
        log.Println("Closing Websocket")
        c.Close()
    }()
    clients[c] = struct{}{}

    for {
        var msg Message
        err := c.ReadJSON(&msg)
        if err != nil {
            log.Printf("Error in reading json message. Error : %v", err)
            return
        }

        // process the message
        broadcast(msg)
    }
}
Enter fullscreen mode Exit fullscreen mode

Let’s see what we are doing here.

  • We’ve created a map clients to store the websocket connections
  • Inside handleClient, we make sure to store the client connection in the clients map
  • Then we spin up an indefinite for loop to listen for messages from the connection.
  • Whenever we receive a message from the connection, it’s read using the ReadJSON method that reads and marshals the data into a Message struct.

Now that we’ve read the message, we’ll see more on how we can broadcast it to all other websocket connections

func broadcast(msg Message) {
    for conn := range clients {
        conn.WriteJSON(msg)
    }
}
Enter fullscreen mode Exit fullscreen mode

The above function simply takes in a message and writes it to all websocket connections using the WriteJSON method using a for loop.

Now, let’s write a simple index.html file and save it inside a static folder that will act as the chat interface.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go chat 🚀</title>
</head>

<body>
    <div style="display: flex;flex-direction: column;">
        <div id="inputs">
            <form onsubmit="handleSubmit(event)" method="post">
                <input id="username" type="text" name="username" placeholder="username" required>
                <input id="message" type="text" name="message" placeholder="what's on your mind ?" required>
                <input type="submit" value="Send">
            </form>
        </div>
        <div id="messages" style="display: flex;flex-direction: column;"></div>
    </div>
</body>

<script>

    const websocket = new WebSocket("ws://localhost:8080/ws");

    function handleSubmit(e) {
        e.preventDefault();
        websocket.send(JSON.stringify({
            "from": e.target.username.value,
            "message": e.target.message.value
        }))
    }

    websocket.onmessage = function (event) {
        const message = JSON.parse(event.data)
        messages.innerHTML += `<p><b>${message.from}</b> says ${message.message}</p>`
    }
</script>

</html>
Enter fullscreen mode Exit fullscreen mode

The above HTML file, when opened, creates a websocket connection with the backend and sends a message whenever the form is submitted. It also listens for incoming messages using the onmessage handler.

Let’s update the main function to serve the index.html and also wire up the websocket handler.

...

func main() {
    ...
    router.StaticFile("/", "./static/index.html")
    router.GET("/ws", serveWs)
...
}
...

Enter fullscreen mode Exit fullscreen mode

The final main.go file looks something like this.


package main

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

func main() {

    router := gin.Default()
    router.StaticFile("/", "./static/index.html")
    router.GET("/ws", serveWs)
    err := router.Run()
    if err != nil {
        log.Fatalf("Unable to start server. Error %v", err)
    }
    log.Println("Server started successfully.")
}

func serveWs(c *gin.Context) {

    upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Printf("Error in upgrading web socket. Error: %v", err)
        return
    }

    go handleClient(conn)
}

var clients = make(map[*websocket.Conn]struct{})

type Message struct {
    From    string `json:"from"`
    Message string `json:"message"`
}

func broadcast(msg Message) {
    for conn := range clients {
        conn.WriteJSON(msg)
    }
}

func handleClient(c *websocket.Conn) {
    defer func() {
        delete(clients, c)
        log.Println("Closing Websocket")
        c.Close()
    }()
    clients[c] = struct{}{}

    for {
        var msg Message
        err := c.ReadJSON(&msg)
        if err != nil {
            log.Printf("Error in reading json message. Error : %v", err)
            return
        }

        broadcast(msg)
    }
}


Enter fullscreen mode Exit fullscreen mode

Now let’s run the application and open localhost:8080 in a browser.

go run .
Enter fullscreen mode Exit fullscreen mode

Image description

Ta-da ✨! We’ve successfully built a simple chat application with Websockets in less than 100 lines of code.

However, this solution has one big drawback i.e, it cannot be deployed in a scalable manner.

In the next part, we’ll go through in detail about this problem and how we can solve it with Redis pub sub. Stay tuned !

Top comments (0)