Socket.IO explained and implemented with Go

A bidirectional, low latency and event-based communication between client and server

Rahul Kapoor
4 min readJul 4, 2023
Image showing communication between client and server via Socket.IO

Socket.IO is built on top of the WebSocket protocol and provides additional guarantees like a fallback to HTTP long-polling.

Image showing communication between client and server via webSocket

Till now we introduced a new term: WebSocket

Let’s cover what WebSocket protocol first and then the rest of the article covers Socket.IO with examples.

WebSocket allows us to create “real-time” applications which are faster and require less overhead than traditional API protocols. Sometimes referred to as a high-end computer communication protocol, WebSocket is needed to establish the client-server communication channel.

As per the conventional definition, WebSocket is a duplex protocol used mainly in the client-server communication channel. It’s bidirectional in nature which means communication happens to and fro between client-server.

The connection, developed using the WebSocket, lasts as long as any of the participating parties lays it off. Once one party breaks the connection, the second party won’t be able to communicate as the connection breaks automatically at its front.

The image is taken from https://www.wallarm.com/what/a-simple-explanation-of-what-a-websocket-is

What Socket.IO is?

Socket.IO is a library that enables low-latency, bidirectional and event-based communication between a client and a server.

How does it work?

The bidirectional channel between the Socket.IO server and the Socket.IO client is established with a WebSocket connection whenever possible and will use HTTP long-polling as a fallback.

The Socket.IO codebase is split into two distinct layers:

  • the low-level plumbing: what we call Engine.IO, the engine inside Socket.IO
  • the high-level API: Socket.IO itself

Engine.IO is responsible for establishing the low-level connection between the server and the client.

It handles:

  • the various transports and the upgrade mechanism
  • the disconnection detection

By default, the client establishes the connection with the HTTP long-polling transport.

But, why?

While WebSocket is clearly the best way to establish a bidirectional communication, experience has shown that it is not always possible to establish a WebSocket connection, due to corporate proxies, personal firewall, antivirus software…

From the user perspective, an unsuccessful WebSocket connection can translate in up to 10 seconds of waiting for the realtime application to begin exchanging data. This perceptively hurts user experience.

To summarize, Engine.IO focuses on reliability and user experience first, marginal potential UX improvements and increased server performance second.

To upgrade, the client will:

  • ensure its outgoing buffer is empty
  • put the current transport in read-only mode
  • try to establish a connection with the other transport
  • if successful, close the first transport
Network monitor — image taken from https://socket.io/docs/v4/how-it-works/

Implementation with Go

Setting up a simple server in golang:

go mod init myproject
go get  github.com/googollee/go-socket.io@v1.0.1

Server-side code:

package main

import (
"fmt"
"log"
"net/http"

socketio "github.com/googollee/go-socket.io"
)

func main() {
server, err := socketio.NewServer(nil)
if err != nil {
log.Fatal("error establishing new socketio server")
}

server.On("connection", func(so socketio.Socket) {
log.Println("On connection established")

so.Join("mychat") //join the room with the above socketio connection

so.On("chat message", func(msg string) {
log.Println("messge received: " + msg)
so.BroadcastTo("mychat", "chat message", msg) //broadcasting the message to the room so all the clients connected can get it
})
})

fmt.Println("Server running on localhost: 5001")

//create a folder with name static and within that index.html
fs := http.FileServer(http.Dir("static"))
http.Handle("/", fs)

http.Handle("/socket.io/", server)

log.Fatal(http.ListenAndServe(":5001", nil))

}

I created a folder with name static and created index.html in that with the below frontend code:

We will create a basic frontend application which will emit the message “hello world!” to our event “chat message”

Here we are also reading on that event in the same client anything which is broadcasted but this can be done on a separate frontend client as well.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Go WebSocket Tutorial</title>
</head>
<body>
<h2>Hello World</h2>

<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
<script>
const socket = io("http://localhost:5001/socket.io/");

socket.emit("chat message", "hello world!") //emitting the message from 1 client

//all the client reading on that event will get that message once broadcasted
socket.on("chat message", (msg) => {
console.log("Client side: " + msg)
})
</script>
</body>
</html>

Now run the server using:

go run app.go

Redirect to localhost:5001 and you can see the desired logs getting printed both on the server and client side.

In my case, I got:

Server running on localhost: 5001
2023/07/05 01:53:35 On connection established
2023/07/05 01:53:35 On connection established
2023/07/05 01:53:35 On connection established
2023/07/05 01:53:35 On connection established
2023/07/05 01:53:35 messge received: hello world!

--

--