In the last tutorial, we created a Group chat where multiple users chat with each other. In this post, we will go one step further and we will be Sending message to specific user with GoLang WebSocket. This feature will later help us to create a private chat application using GoLang.
But this article is truly based on Sending message to the specific user with GoLang WebSocket and I will explain how we can do that in GoLang WebSocket.
1. Sending message to the specific user with the GoLang WebSocket workflow
Here, We will create a map in the socket server in which we will store all the connected users to the socket server. When users connect to the Socket server, then the Socket server sends an array of objects to the client as a list of online users. This object contains the unique user id as well as username which is provided by the client when a client connects to the server.
for example,[ {id:'some randon id',name:'Shashank'},{...},{...}....{n}];
Note: In this application, I am using a map to store the users connected to the server, But In production please avoid this idea instead use your choice of database to store the user’s information.
The below Image shows how we are storing information of users connected to the server.
Heren
number of users connected to the server, we are storing their respective user ID (we generate the user id using uuid
), name, and throwing back the array of objects to all clients connected to the server.
To send a message to the particular client, we are must provide the user id of that client to the server. In GoLang Socket Server, We will search for that user using that user’s user-id as shown below.
func EmitToSpecificClient(hub *Hub, payload SocketEventStruct, userID string) {
for client := range hub.clients {
if client.userID == userID {
select {
case client.send <- payload:
default:
close(client.send)
delete(hub.clients, client)
}
}
}
}
For example, user 3 wants to send a message to user 1. Assuming user3 is contacted from the Chrome browser and user1 is connected to the Firefox browser as shown below image.
Here, managing the list of users plays a very important role in terms of showing online users in the chat room. For example, when one user goes offline other users should get an updated list of online users. The figure below explains the same have a look,
Also, readSending a message to a specific user with socket.io
2. Creating a new GoLang application
=>Let’s start off by creating a new GoLang project by usinggo mod init specific-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 asgo.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 specific-chat
go 1.13
require (
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4
github.com/gorilla/websocket v1.4.2
)
3. 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 usingcreate-react-app
CLI. Instead of that, we will use CDN files and Babel Transpiler JS files. Inside thepublic
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,
/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 packagehandlers
.
/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 the server.go file.
4. 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 the main()
function, First we are printing some information.
=>In the next line, we will create route
variable, which will hold Route instance.
=>Then AddApproutes()
function register application routes.
=>And at the end, using http.ListenAndServe()
we will start our GO server.
server.go:
// Sending message to specific user with GoLang WebSocket
// @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))
}
5. Adding GoLang routes in the application
Create aroutes.goin the root of the project, Here we will register application routes. Here we will use gorilla/mux
package to register routes.
=>Then AddApproutes()
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:
// Sending message to specific user with GoLang WebSocket
// @author Shashank Tiwari
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
handlers "specific-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,
}
// Reading username from request parameter
username := mux.Vars(request)["username"]
// Upgrading the HTTP connection socket connection
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 theusername
from the parameter usingGorilla 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 definitionshere, for upgrading the HTTP server connection to the WebSocket protocol.
7.Then we callCreateNewSocketUser()
function from the handler package, which creates a new socket connection.
6. Creating a Hub for Socket Server
Create ahub.gounder/handlers
folder, here we will write a code to create socket Hub. The code written inside this file is called from theroute-handler.gofile as saw in the above explanation.
/handlers/hub.go:
package handlers
// Hub maintains the set of active clients and broadcasts messages to the clients.
type Hub struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
}
// 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 functionNewHub()
returns the new instance of the Hub
2.Then we haveRun()
function, which is a Goroutine. In this Goroutine, we receive data from the←register
and←unregister
channel.
3.The←register
and←unregister
channels call theHandleUserRegisterEvent()
andHandleUserDisconnectEvent()
respectively of the samehandler
package.
7. Creating new users and Sending messages
Create asocket-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:
// Sending message to specific user with GoLang WebSocket
// @author Shashank Tiwari
package handlers
import (
"bytes"
"encoding/json"
"log"
"time"
"github.com/google/uuid"
"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) {
uniqueID := uuid.New()
client := &Client{
hub: hub,
webSocketConnection: connection,
send: make(chan SocketEventStruct),
username: username,
userID: uniqueID.String(),
}
go client.writePump()
go client.readPump()
client.hub.register <- client
}
// 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.userID,
})
}
// 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.userID,
})
}
}
// EmitToSpecificClient will emit the socket event to specific socket user
func EmitToSpecificClient(hub *Hub, payload SocketEventStruct, userID string) {
for client := range hub.clients {
if client.userID == userID {
select {
case client.send <- payload:
default:
close(client.send)
delete(hub.clients, client)
}
}
}
}
// 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: socketEventPayload.EventName,
EventPayload: JoinDisconnectPayload{
UserID: client.userID,
Users: getAllConnectedUsers(client.hub),
},
})
case "disconnect":
log.Printf("Disconnect Event triggered")
BroadcastSocketEventToAllClient(client.hub, SocketEventStruct{
EventName: socketEventPayload.EventName,
EventPayload: JoinDisconnectPayload{
UserID: client.userID,
Users: getAllConnectedUsers(client.hub),
},
})
case "message":
log.Printf("Message Event triggered")
selectedUserID := socketEventPayload.EventPayload.(map[string]interface{})["userID"].(string)
socketEventResponse.EventName = "message response"
socketEventResponse.EventPayload = map[string]interface{}{
"username": getUsernameByUserID(client.hub, selectedUserID),
"message": socketEventPayload.EventPayload.(map[string]interface{})["message"],
"userID": selectedUserID,
}
EmitToSpecificClient(client.hub, socketEventResponse, selectedUserID)
}
}
func getUsernameByUserID(hub *Hub, userID string) string {
var username string
for client := range hub.clients {
if client.userID == userID {
username = client.username
}
}
return username
}
func getAllConnectedUsers(hub *Hub) []UserStruct {
var users []UserStruct
for singleClient := range hub.clients {
users = append(users, UserStruct{
Username: singleClient.username,
UserID: singleClient.userID,
})
}
return users
}
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 isCreateNewSocketUser()
, this function will create a new user/client. After creating a new user we will send this user toregister←
channel.
2.Next, we haveHandleUserRegisterEvent()
andHandleUserDisconnectEvent()
function, which will callhandleSocketPayloadEvents()
. These methods will handlejoin
anddisconnect
socket event.
3. EmitToSpecificClient
function takes 3 args hub
, socket payload
, and userID
. Based on the userID
provided, it sends socket Payload to that user, whos userId
matches with the userId of users stored in socket server.
4.TheBroadcastSocketEventToAllClient()
function will send the socket events to all the users connected to Socket Server by using a←send
channel.
5.FunctionhandleSocketPayloadEvents()will send the Socket Event to the connected user based on the Event type. In this function, we have writtenswitch case
statement to send the valid socket response based on the socket events.
6.Then we havereadPump()
andwritePump()
functions, these functions are running as Goroutines. If you notice we have called these functions fromCreateNewSocketUser()
function.
7.InreadPump()
function, we first callunRegisterAndCloseConnection()
as defer statement, then we callsetSocketPayloadReadConfig()
function which sets the payload read configuration from peers.
8.TheunRegisterAndCloseConnection()
function closes the socket connection for on single user/client passed in the function parameter.
9.In the functionsetSocketPayloadReadConfig()
, we set the read limit of incoming packet usingSetReadLimit()
function. Ths functionSetReadDeadline()
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.
10.And then after reading and decoding the message we pass the socket event to thehandleSocketPayloadEvents()
function, which further delegates the socket event to the users based on the event type.
11.InwritePump()
function, we first call defer function close the socket connection. Then we read the socket event from the←send
channel. With the help of theNextWriter()
function, we get an instance of a Web socket response writer. On top of this Web socket response writer, we call the functionWrite()
to send the socket payload to the socket connection.
8. The Frontend: Creating A UI with React
First thing first create anindex.htmlfile 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.htmlfile and write below markup,
views/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<title>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 addedstyle.cssfile, 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 ascript.jsfile that contains the code of React to render the chat screen n all.
Now createscript.jsfile 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:
// Sending message to specific user with GoLang WebSocket
// @author Shashank Tiwari
const domElement = document.querySelector(".chat__app-container");
class App extends React.Component {
constructor() {
super();
this.state = {
chatUserList: [],
message: null,
selectedUserID: null,
userID: null
}
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) => {
this.setState({
message: 'Your Connection is closed.',
chatUserList: []
});
};
this.webSocketConnection.onmessage = (event) => {
try {
const socketPayload = JSON.parse(event.data);
switch (socketPayload.eventName) {
case 'join':
case 'disconnect':
if (!socketPayload.eventPayload) {
return
}
const userInitPayload = socketPayload.eventPayload;
this.setState({
chatUserList: userInitPayload.users,
userID: this.state.userID === null ? userInitPayload.userID : this.state.userID
});
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;
this.setState({
message: `${sentBy} says: ${actualMessage}`
});
break;
default:
break;
}
} catch (error) {
console.log(error)
console.warn('Something went wrong while decoding the Message Payload')
}
};
}
setNewUserToChat = (event) => {
if (event.target && event.target.value) {
if (event.target.value === "select-user") {
alert("Select a user to chat");
return;
}
this.setState({
selectedUserID: event.target.value
})
}
}
getChatList() {
if (this.state.chatUserList.length === 0) {
return(
<h3>No one has joined yet</h3>
)
}
return (
<div className="chat__list-container">
<p>Select a user to chat</p>
<select onChange={this.setNewUserToChat}>
<option value={'select-user'} className="username-list">Select User</option>
{
this.state.chatUserList.map(user => {
if (user.userID !== this.state.userID) {
return (
<option value={user.userID} className="username-list">
{user.username}
</option>
)
}
})
}
</select>
</div>
);
}
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: {
userID: this.state.selectedUserID,
message: event.target.value
},
}));
event.target.value = '';
}
} catch (error) {
console.log(error)
console.warn('Something went wrong while decoding the Message Payload')
}
}
getChatContainer() {
return (
<div class="chat__message-container">
<div class="message-container">
{this.state.message}
</div>
<input type="text" id="message-text" size="64" autofocus placeholder="Type Your message" onKeyPress={this.handleKeyPress}/>
</div>
);
}
render() {
return (
<React.Fragment>
{this.getChatList()}
{this.getChatContainer()}
</React.Fragment>
);
}
}
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. In this React Fragment, we have called getChatList()
and getChatContainer()
methods.
2. The getChatList()
method will render the select tag from which we can select a single user with whom we want to chat.
3. In the method getChatContainer()
, we will render the messages received from other users. Next, we have an input box to send messages to other users.
4.In getChatContainer()
method, we consumemessages
property of thestate
object. Themessages
property will be an array that will have an object with keysusername
andmessage
.
5.Above that, we havehandleKeyPress()
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.
6. As I said before the getChatList()
method renders select tag, which will contain a list of users. Here we use chatUserList
state property to render the chat list. Whenever we will select any user from the select tag, we will call setNewUserToChat()
method.
7. InsetNewUserToChat()
method, we update the selectedUserID
property of state object by using setState.
8.UsingwebSocketConnection
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 thecomponentDidMount
lifecycle method.
9.The methodssetWebSocketConnection()
andsubscribeToSocketMessage()
is called fromcomponentDidMount
lifecycle method. Names of both methods are self-explaining their purpose, which we will discuss below anyway.
10.The methodsetWebSocketConnection()
will set up a Socket connection and save its instance to thewebSocketConnection
property of the App Component class.
11.InsubscribeToSocketMessage()
method we are subscribing to the incoming socket events from web socket and based on their type we are pushing the events intomessages
property of the state object by usingsetState()
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 > ./specific-chat
Now you can test this application by opening the localhost:8000
.
10. Conclusion
For now, That’s it. Now using this article you canSend message to specific users with GoLang WebSocketand you can integrate this feature in the realtime private chat application. In the next post, we start building a Realtime private chat application in GoLang, where this feature will be useful.
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.
Cannot wait to see part 3 What time is it gonna be live?