Adding WebSockets to an application opens up a whole new set of opportunities. The only problem with WebSockets in most languages, for example Golang, is they’re not quite as simple to integrate into your application so people would like. This is where Socket.IO comes in, it enables real-time, bidirectional and event-based communication.

In this post, I will outline how to make a basic chat application where Socket.IO communicates for persisted storage with a Golang API backend. You can find a full Docker setup and the code on GitHub here!

Setting Up the API

The first thing we need to do is create the Golang API which Socket.IO will be sending HTTP requests to. We will need two endpoints on this API.

POST / - Add a message
GET / - Retrieve all messages

I made a simple API exposing the two endpoints from above. Because this is a pretty basic tutorial I am using a simple in memory but you can easily use a database and persist the data. If this sort of API is new to you I’d recommend my other blog post about building an API in Golang.

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"os"
)

type MessagesResponse struct {
	Messages []*Message `json:"messages"`
}

type Message struct {
	Username string `json:"username"`
	Text     string `json:"text"`
}

type Server struct {
	Messages []*Message
}

func (s *Server) AddMessageHandler(w http.ResponseWriter, r *http.Request) {
	decoder := json.NewDecoder(r.Body)
	var m Message
	err := decoder.Decode(&m)
	if err != nil {
		panic(err)
	}
	s.Messages = append(s.Messages, &m)
	w.Write([]byte("OK"))
}

func (s *Server) GetMessagesHandler(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(s.Messages)
}

func main() {
	messages := make([]*Message, 0)
	s := &Server{
		Messages: messages,
	}

	r := mux.NewRouter()
	// Routes consist of a path and a handler function.
	r.HandleFunc("/", s.AddMessageHandler).Methods("POST")
	r.HandleFunc("/", s.GetMessagesHandler).Methods("GET")

	// Bind to a port and pass our router in
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), r))
}

Creating the Socket.IO Server

Now that the API is done, we have to build out the Socket.IO server. It will be pretty basic because we only need two socket channels open. First is username to set the username of each user who enters the chat, and second is message to send and recieve messages when a user hits submit. Socket.IO makes it super simple with the syntax socket.on('CHANNEL', (data) => {});.

We can persist data in for the socket connection by attaching data to the socket variable, so we will attach the username given to us by the client. This is so we don’t have to worry about sending the username up in every message.

const axios = require('axios');
const app = require('express')();
const server = require('http').Server(app);
const io = require('socket.io')(server);

server.listen(process.env.PORT, () => {
  console.log('Server listening at port %d', process.env.PORT);
});

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

app.get('/messages', async (req, res) => {
  const { data } = await axios.get(process.env.API_HOST);
  res.send(data);
});

io.on('connection', (socket) => {
  socket.on('username', (data) => {
    socket.username = data.name;
  });

  socket.on('message', (data) => {
    data.username = socket.username;
    socket.broadcast.emit('message', data);
    axios.post(process.env.API_HOST, data);
  });
});

After both the API and the Socket.IO server are built out we have one last task, hooking the two together. This is a very straightforward task when using a library like axios to send requests. All we will be doing is sending a GET and a POST request between the two services like await axios.get(URL);.

Build a Basic Frontend

The final step which ties this example together is a frontend to see the code in action! Below is a quick and dirty HTML file I made which will show the chat application in action. I generate and store a username to differentiate and persist the users. Finally, I use an onclick event on the submit button to send the data up to the Socket.IO server.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.js"></script>
</head>
<body>
<h1>Chat Example</h1>

<div id="messages">

</div>
<input type="text" id="message">
<button id="chat-submit">Submit</button>

<script>
  var me = localStorage.getItem('me');
  if (me === null) {
      me = `user-${Math.round(Math.random() * 1000)}`;
      localStorage.setItem('me', me);
  }
  axios.get('http://localhost:8082/messages').then(({ data }) => {
      if (data.length) {
          data.map((i) => ({
              username: me === i.username ? undefined : i.username,
              text: i.text,
          })).forEach(i => addMessage(i));
      }
  });
  var addMessage = (message) => {
      const messageWrapper = document.createElement('div');
      const messageTxt = document.createTextNode(`${message.username || 'You'}: ${message.text}`);
      messageWrapper.appendChild(messageTxt);
      document.getElementById('messages').appendChild(messageWrapper);
  };
  var socket = io.connect('http://localhost:8082');
  socket.emit('username', { name: me });
  document.getElementById('chat-submit').onclick = () => {
      var text = document.getElementById('message').value;
      var message = { text };
      socket.emit('message', message);
      addMessage(message);
      document.getElementById('message').value = '';
  };
  socket.on('message', (message) => {
      addMessage(message);
  });
</script>
    
</body>
</html>

Conclusion

After breaking this down in to those steps I hope I gave you some ideas of how you can use this, or similar ideas in your own projects. If you are interested in running this project or working off of it there are Dockerfiles and code on Github here.


Leave a Reply

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