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.
Let’s take a look at the final outcome of this application Before we get our hands dirty,
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.
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 createroute
variable, 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/mux
package 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.setStaticFolder
function 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.