• Download
  • Contact
  • Terms of Service
  • Privacy Policy
  • About US
Codershood
  • Demos
  • Plugins
  • Angular
  • NodeJs
  • GO lang
  • Others
No Result
View All Result
Codershood
  • Demos
  • Plugins
  • Angular
  • NodeJs
  • GO lang
  • Others
No Result
View All Result
Codershood
No Result
View All Result

Build a Realtime group chat app in Golang using WebSockets

by Shashank Tiwari
June 17, 2020
in GO lang
0
10 Minutes Read
Build a Realtime group chat app in Golang using WebSockets

From Crypto Exchange platforms to Chatting Applications having a realtime interaction between the users and the product is gold, it makes your product look more professional and accessible. As a developer you know, we can do that using Web sockets and Creating Web Chat server in Go Lang kinda tricky, so in this article, we will Build a Realtime group chat app in Golang using WebSockets.

In this post, we will Build a Realtime group chat app in Golang using WebSockets with the easiest way possible. The front end will be in React and just to keep things simple won’t be using any database to store messages. Since we are not storing users and messages in the database so the idea behind the application is very simple.

Here, We will create a map in the socket server in which we will store all the connected users to the socket server. Since here we will broadcast the messages to all the users stored in the socket server, so we don’t have to implement any complicated stuff.

 

 Download

 




 

Let’s take a look at the final outcome of this application Before we get our hands dirty,

 

Build a Realtime group chat app in Golang using WebSockets Outcome

 

1. Creating a new GoLang application

=>Let’s start off by creating a new GoLang project by using go mod init group-chat command. This command will initialize a new module in the current directory.

=>Also, if you notice the above command will create a new mod file named as go.mod in the current directory.

=> In this file you will have all your package listed, for now this is how my mod file looks like,

go.mod:

module group-chat

go 1.13

require (
    github.com/google/uuid v1.1.1 // indirect
    github.com/gorilla/mux v1.7.4
    github.com/gorilla/websocket v1.4.2
    github.com/joho/godotenv v1.3.0 // indirect
)

2. Understanding the project structure

Here we will give a very little bit of styling to our web application just to make it look presentable. On the front-end, we are using React but here we won’t be using create-react-app CLI. Instead of that, we will use CDN files and Babel Transpiler JS files. Inside the public folder, we will write down the Javascript scripts and inside view folder, we will write down the MARKUP.

Build a Realtime group chat app in Golang using WebSockets Folder Structure

 

 

In the above image, you can see we have listed files created in the application. Below I have listed the purpose of each file, why we need them, let’s start from the top,

constants.go: This file contains all constants used in the application.

/handlers: This folder will have all the file which are responsible for Creating Socket Connections and sending socket messages.

hub.go: In this file, we will create the Socket Hub where we will have information about all the users.

routes-handlers.go:Here we will write handlers for application routes.

socket-handlers.go:In this file, we will handle the incoming socket connections and here we have written logic to send the messages.

structs.go:In this file, we will write all the structs that will be used in package handlers.

/public: In this folder, we will write React Logic to render messages and CSS styling.

routes.go:As the name suggests, this file will contain the endpoints that we will define in this application.

server.go:To create the Golang server we will use server.go file.

3. Creating a GoLang Server

Create aserver.goin the root of the project, which will be our entry point for the project. Here we will make a connection with the MongoDB database and we will define our application routes.

=>Inside themain()function, First we are printing some information.

=>In the next line, we will createroutevariable, which will hold Route instance.

=>ThenAddApproutes()function register application routes.

=>And at the end, usinghttp.ListenAndServe()we will start our GO server.

server.go:

// Build a Realtime group chat app in Golang using WebSockets
// @author Shashank Tiwari

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {

    log.Println("Server will start at http://localhost:8000/")

    route := mux.NewRouter()

    AddApproutes(route)

    log.Fatal(http.ListenAndServe(":8000", route))
}

4. Adding GoLang routes in the application

Create aroutes.goin the root of the project, Here we will register application routes. Here we will usegorilla/muxpackage to register routes.

=>ThenAddApproutes()function will register all the routes in the application. Here we have only one route to add which will be used by the FrontEnd javascript.

routes.go:

// Build a Realtime group chat app in Golang using WebSockets
// @author Shashank Tiwari


package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/gorilla/websocket"

    handlers "group-chat/handlers"
)

func setStaticFolder(route *mux.Router) {
    fs := http.FileServer(http.Dir("./public/"))
    route.PathPrefix("/public/").Handler(http.StripPrefix("/public/", fs))
}

// AddApproutes will add the routes for the application
func AddApproutes(route *mux.Router) {

    log.Println("Loadeding Routes...")

    setStaticFolder(route)

    hub := handlers.NewHub()
    go hub.Run()

    route.HandleFunc("/", handlers.RenderHome)

    route.HandleFunc("/ws/{username}", func(responseWriter http.ResponseWriter, request *http.Request) {
        var upgrader = websocket.Upgrader{
            ReadBufferSize:  1024,
            WriteBufferSize: 1024,
        }

        username := mux.Vars(request)["username"]

        connection, err := upgrader.Upgrade(responseWriter, request, nil)
        if err != nil {
            log.Println(err)
            return
        }

        handlers.CreateNewSocketUser(hub, connection, username)

    })

    log.Println("Routes are Loaded.")
}

Explanation:

1.setStaticFolderfunction will set the static folder path.

2. After that, we start our socket server Hub, which contains all the users connected to the socket server.

3./API will render the Home of the Group Chat.

4./ws/{username}/API will initiate a Socket connection. This Route will also create a new User in the Socket server.

5. We read the username from the parameter using Gorilla MUX routing, here you can pass any name for the user when creating a new socket connection.

6. The API/ws/{username}/calls the anonymous function, which first upgrades the HTTP server connection to the WebSocket protocol. You can find the original definitions here, for upgrading the HTTP server connection to the WebSocket protocol.

7. Then we call CreateNewSocketUser() function from the handler package, which creates a new socket connection.

5. Creating a Hub for Socket Server

Create a hub.go under /handlers folder, here we will write a code to create socket Hub. The code written inside this file is called from the route-handler.go file as saw in the above explanation.

/handlers/hub.go:

// Build a Realtime group chat app in Golang using WebSockets
// @author Shashank Tiwari

package handlers

// NewHub will will give an instance of an Hub
func NewHub() *Hub {
    return &Hub{
        register:   make(chan *Client),
        unregister: make(chan *Client),
        clients:    make(map[*Client]bool),
    }
}

// Run will execute Go Routines to check incoming Socket events
func (hub *Hub) Run() {
    for {
        select {
        case client := <-hub.register:
            HandleUserRegisterEvent(hub, client)

        case client := <-hub.unregister:
            HandleUserDisconnectEvent(hub, client)
        }
    }
}

Explanation:

1. The function NewHub() returns the new instance of the Hub

2. Then we have Run() function, which is a Goroutine. In this Goroutine, we receive data from the ←register and ←unregister channel.

3. The ←register and ←unregister channels call the HandleUserRegisterEvent() and HandleUserDisconnectEvent() respectively of the same handler package.

6. Creating new users and Sending messages

Create a socket-handler.gounder /handlers folder, here we write code for creating a new user in the socket server and we will write a code to send messages across all users connected to the socket server.

/handlers/socket-handler.go:

// Build a Realtime group chat app in Golang using WebSockets
// @author Shashank Tiwari

package handlers

import (
    "bytes"
    "encoding/json"
    "log"
    "time"

    "github.com/gorilla/websocket"
)

const (
    writeWait      = 10 * time.Second
    pongWait       = 60 * time.Second
    pingPeriod     = (pongWait * 9) / 10
    maxMessageSize = 512
)

// CreateNewSocketUser creates a new socket user
func CreateNewSocketUser(hub *Hub, connection *websocket.Conn, username string) {
    client := &Client{
        hub:                 hub,
        webSocketConnection: connection,
        send:                make(chan SocketEventStruct),
        username:            username,
    }

    client.hub.register <- client

    go client.writePump()
    go client.readPump()
}

// HandleUserRegisterEvent will handle the Join event for New socket users
func HandleUserRegisterEvent(hub *Hub, client *Client) {
    hub.clients[client] = true
    handleSocketPayloadEvents(client, SocketEventStruct{
        EventName:    "join",
        EventPayload: client.username,
    })
}

// HandleUserDisconnectEvent will handle the Disconnect event for socket users
func HandleUserDisconnectEvent(hub *Hub, client *Client) {
    _, ok := hub.clients[client]
    if ok {
        delete(hub.clients, client)
        close(client.send)

        handleSocketPayloadEvents(client, SocketEventStruct{
            EventName:    "disconnect",
            EventPayload: client.username,
        })
    }
}

// BroadcastSocketEventToAllClient will emit the socket events to all socket users
func BroadcastSocketEventToAllClient(hub *Hub, payload SocketEventStruct) {
    for client := range hub.clients {
        select {
        case client.send <- payload:
        default:
            close(client.send)
            delete(hub.clients, client)
        }
    }
}

func handleSocketPayloadEvents(client *Client, socketEventPayload SocketEventStruct) {
    var socketEventResponse SocketEventStruct
    switch socketEventPayload.EventName {
    case "join":
        log.Printf("Join Event triggered")
        BroadcastSocketEventToAllClient(client.hub, SocketEventStruct{
            EventName:    "join",
            EventPayload: socketEventPayload.EventPayload,
        })

    case "disconnect":
        log.Printf("Disconnect Event triggered")
        BroadcastSocketEventToAllClient(client.hub, SocketEventStruct{
            EventName:    "disconnect",
            EventPayload: socketEventPayload.EventPayload,
        })

    case "message":
        log.Printf("Message Event triggered")
        socketEventResponse.EventName = "message response"
        socketEventResponse.EventPayload = map[string]interface{}{
            "username": client.username,
            "message":  socketEventPayload.EventPayload,
        }
        BroadcastSocketEventToAllClient(client.hub, socketEventResponse)
    }
}

func (c *Client) readPump() {
    var socketEventPayload SocketEventStruct

    defer unRegisterAndCloseConnection(c)

    setSocketPayloadReadConfig(c)

    for {
        _, payload, err := c.webSocketConnection.ReadMessage()

        decoder := json.NewDecoder(bytes.NewReader(payload))
        decoderErr := decoder.Decode(&socketEventPayload)

        if decoderErr != nil {
            log.Printf("error: %v", decoderErr)
            break
        }

        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("error ===: %v", err)
            }
            break
        }

        handleSocketPayloadEvents(c, socketEventPayload)
    }
}

func (c *Client) writePump() {
    ticker := time.NewTicker(pingPeriod)
    defer func() {
        ticker.Stop()
        c.webSocketConnection.Close()
    }()
    for {
        select {
        case payload, ok := <-c.send:

            reqBodyBytes := new(bytes.Buffer)
            json.NewEncoder(reqBodyBytes).Encode(payload)
            finalPayload := reqBodyBytes.Bytes()

            c.webSocketConnection.SetWriteDeadline(time.Now().Add(writeWait))
            if !ok {
                c.webSocketConnection.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }

            w, err := c.webSocketConnection.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }

            w.Write(finalPayload)

            n := len(c.send)
            for i := 0; i < n; i++ {
                json.NewEncoder(reqBodyBytes).Encode(<-c.send)
                w.Write(reqBodyBytes.Bytes())
            }

            if err := w.Close(); err != nil {
                return
            }
        case <-ticker.C:
            c.webSocketConnection.SetWriteDeadline(time.Now().Add(writeWait))
            if err := c.webSocketConnection.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}

func unRegisterAndCloseConnection(c *Client) {
    c.hub.unregister <- c
    c.webSocketConnection.Close()
}

func setSocketPayloadReadConfig(c *Client) {
    c.webSocketConnection.SetReadLimit(maxMessageSize)
    c.webSocketConnection.SetReadDeadline(time.Now().Add(pongWait))
    c.webSocketConnection.SetPongHandler(func(string) error { c.webSocketConnection.SetReadDeadline(time.Now().Add(pongWait)); return nil })
}

Explanation:

Let’s start from the Top,

1. The first function is CreateNewSocketUser(), this function will create a new user/client. After creating a new user we will send this user to register←channel.

2. Next, we have HandleUserRegisterEvent() and HandleUserDisconnectEvent() function, which will call handleSocketPayloadEvents(). These methods will handle join and disconnect socket event.

3. TheBroadcastSocketEventToAllClient() function will send the socket events to all the users connected to Socket Server by using a ←send channel.

4. Function handleSocketPayloadEvents() will send the Socket Event to the connected user based on the Event type. In this function, we have written switch case statement to send the valid socket response based on the socket events.

5. Then we have readPump() and writePump() functions, these functions are running as Goroutines. If you notice we have called these functions from CreateNewSocketUser() function.

6. In readPump() function, we first call unRegisterAndCloseConnection() as defer statement, then we callsetSocketPayloadReadConfig() function which sets the payload read configuration from peers.

7. The unRegisterAndCloseConnection() function closes the socket connection for on single user/client passed in the function parameter.

8. In the function setSocketPayloadReadConfig(), we set the read limit of incoming packet using SetReadLimit() function. Ths function SetReadDeadline() will set the read deadline on the underlying network connection. After a read has timed out, the WebSocket connection state will be corrupted and all future reads will return an error.

9. And then after reading and decoding the message we pass the socket event to the handleSocketPayloadEvents() function, which further delegates the socket event to the users based on the event type.

10. In writePump() function, we first call defer function close the socket connection. Then we read the socket event from the ←send channel. With the help of the NextWriter() function, we get an instance of a Web socket response writer. On top of this Web socket response writer, we call the function Write() to send the socket payload to the socket connection.

8. The Frontend: Creating A UI with React

First thing first create an index.html file under views folder. In this file, we will import the Library of React and Babel, so that our application will have the capability to handle react code.

Also, here we will include style.css file in order to add styling of the Page. We won’t talk much about it since it is not that interesting topic here. Open theindex.html file and write below markup,

views/index.html:

<!-- Build a Realtime group chat app in Golang using WebSockets
@author Shashank Tiwari -->

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

    <head>
        <title>Group Chat Example</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <link href="/public/css/style.css" rel="stylesheet" type="text/css" />
        <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
        <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
        <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    </head>
    <body>
        <div class="chat__app-container">
        </div>
        <script src="/public/js/script.js" type="text/babel"></script>
    </body>

</html>

Explanation:

1. First, we have added style.css file, which will style our Page.

2. Second, we have added React and Babel libraries, so our webpage will have the capability to compile the react code.

3. We have created a root element where we will render all our Virtual Dom with the help of React.

4. Lastly, we have added a script.js file that contains the code of React to render the chat screen n all.

Now createscript.js file under /public/js folder, As I told you just now that this file will have code written in React in order to render the Chat Screen.

/public/js/script.js:

// Build a Realtime group chat app in Golang using WebSockets
// @author Shashank Tiwari

const domElement = document.querySelector(".chat__app-container");

class App extends React.Component {
    constructor() {
        super();
        this.state = {
            messages: [],
        }
        this.webSocketConnection = null;
    }

    componentDidMount() {
        this.setWebSocketConnection();
        this.subscribeToSocketMessage();
    }

    setWebSocketConnection() {
        const username = prompt("What's Your name");
        if (window["WebSocket"]) {
            const socketConnection = new WebSocket("ws://" + document.location.host + "/ws/" + username);
            this.webSocketConnection = socketConnection;
        }
    }

    subscribeToSocketMessage = () => {
        if (this.webSocketConnection === null) {
            return;
        }

        this.webSocketConnection.onclose = (evt) => {
            const messages = this.state.messages;
            messages.push({
                message: 'Your Connection is closed.',
                type: 'announcement'
            })
            this.setState({
                messages
            });
        };

        this.webSocketConnection.onmessage = (event) => {
            try {
                const socketPayload = JSON.parse(event.data);
                switch (socketPayload.eventName) {
                    case 'join':
                        if (!socketPayload.eventPayload) {
                            return
                        }

                        this.setState({
                            messages: [
                                ...this.state.messages,
                                ...[{
                                    message: `${socketPayload.eventPayload} joined the chat`,
                                    type: 'announcement'
                                }]
                            ]
                        });

                        break;
                    case 'disconnect':
                        if (!socketPayload.eventPayload) {
                            return
                        }
                        this.setState({
                            messages: [
                                ...this.state.messages,
                                ...[{
                                    message: `${socketPayload.eventPayload} left the chat`,
                                    type: 'announcement'
                                }]
                            ]
                        });
                        break;

                    case 'message response':

                        if (!socketPayload.eventPayload) {
                            return
                        }

                        const messageContent = socketPayload.eventPayload;
                        const sentBy = messageContent.username ? messageContent.username : 'An unnamed fellow'
                        const actualMessage = messageContent.message;

                        const messages = this.state.messages;
                        messages.push({
                            message: actualMessage,
                            username: `${sentBy} says:`,
                            type: 'message'
                        })

                        this.setState({
                            messages
                        });

                        break;

                    default:
                        break;
                }
            } catch (error) {
                console.log(error)
                console.warn('Something went wrong while decoding the Message Payload')
            }
        };
    }

    handleKeyPress = (event) => {
        try {
            if (event.key === 'Enter') {
                if (!this.webSocketConnection) {
                    return false;
                }
                if (!event.target.value) {
                    return false;
                }

                this.webSocketConnection.send(JSON.stringify({
                    EventName: 'message',
                    EventPayload: event.target.value
                }));

                event.target.value = '';
            }
        } catch (error) {
            console.log(error)
            console.warn('Something went wrong while decoding the Message Payload')
        }
    }

    getChatMessages() {
        return (
            <div class="message-container">
                {
                    this.state.messages.map(m => {
                        return (
                            <div class="message-payload">
                                {m.username && <span class="username">{m.username}</span>}
                                <span class={`message ${m.type === 'announcement' ? 'announcement' : ''}`}>{m.message}</span>
                            </div>
                        )
                    })
                }
            </div>
        );
    }

    render() {
        return (
            <>
                {this.getChatMessages()}
                <input type="text" id="message-text" size="64" autofocus placeholder="Type Your message" onKeyPress={this.handleKeyPress} />
            </>
        );
    }
}

ReactDOM.render(<App />, domElement)

Explanation:

1.Let’s start from the Bottom, i.e. render() method, Here we have to return obviously JSX which enclosed by React Fragment.

2. In this React Fragment, we have calledgetChatMessages() method. In this method, we will render the messages received from other users. Next, we have an input box to send messages to others users.

3. In getChatMessages() method, we consume messages property of the state object. The messages property will be an array that will have an object with keys username and message.

4. Above that, we have handleKeyPress() method, which will be triggered every time when you will type anything in the input box. This method is responsible to send the messages to the socket server.

5. Using webSocketConnection property of the App Component, we will send the message to the Web Socket server. The propertywebSocketConnection holds an instance of the Web Socket connection, which initialized in the componentDidMount lifecycle method.

6. The methodssetWebSocketConnection() and subscribeToSocketMessage() is called from componentDidMount lifecycle method. Names of both methods are self-explaining their purpose, which we will discuss below anyway.

7. The methodsetWebSocketConnection()will set up a Socket connection and save its instance to thewebSocketConnection property of the App Component class.

8. InsubscribeToSocketMessage() method we are subscribing to the incoming socket events from web socket and based on their type we are pushing the events into messages property of the state object by using setState() method.

9. Running the application: Moment of Truth

To run the application, you need to build the application and then run the executable file as shown below,

> go build
> ./group-chat

Now you can test this application by opening thelocalhost:8000.

10. Conclusion

For now, That’s it. In this application, we created a Group Chat application and we understood how we can create a Web Socket server in Golang in the easiest way possible. On the Frontend, we used React to implement the UI just for simplicity.

If you have any suggestions, questions, or feature requests, let me know in the below comment box, I would be happy to help. If you like this article, do spread a word about it and share it with others.

 

Tags: ChatGo serverGolangGolang serverGroup ChatReact Chat
Previous Post

Google reCaptcha V3 in GoLang Tutorial

Next Post

Sending message to specific user with GoLang WebSocket

Related Posts

Real time private chatting app using React, Golang and mongodb banner-part 2
GO lang

Real time private chatting app using GoLang, React and mongodb – Part 2

July 4, 2020
Real time private chatting app using React, Golang and mongodb banner
GO lang

Real time private chatting app using GoLang, React and Mongodb – Part 1

July 4, 2020
Sending message to specific user with GoLang WebSocket
GO lang

Sending message to specific user with GoLang WebSocket

August 6, 2023
Next Post
Sending message to specific user with GoLang WebSocket

Sending message to specific user with GoLang WebSocket

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *




https://codershood.info

www.codershood.info programming blog dedicated to providing high-quality coding tutorial and articles on web development, Angular, React, Laravel, AngularJs, CSS, Node.js, ExpressJs and many more. We also provide ebook based on complicated web application along with the source code.

  • Download
  • Contact
  • Terms of Service
  • Privacy Policy
  • About US

www.codershood.info is licensed under a Creative Commons Attribution 4.0 International License.

No Result
View All Result
  • Demos
  • Plugins
  • Angular
  • NodeJs
  • GO lang
  • Others

www.codershood.info is licensed under a Creative Commons Attribution 4.0 International License.