Exploring Websockets with Soketi (Part 1)

Soketi is an open source Websockets server, that allows you to build realtime applications with ease. Realtime applications are applications that require data to be updated in realtime. This means that when data changes on the server, the client should be able to see the changes immediately. Examples of realtime applications include chat applications, multiplayer games, and collaborative editing tools.

Soketi was built as an alternative to Pusher, a hosted service for building realtime applications. Therefore, it is compatible with the Pusher protocol, and can be used with libraries from Pusher.

The main concept of Soketi we will be talking about is channels. Channels can be broken down into two parts: subscribing, and publishing. Channels in Soketi can be thought of as pub/sub system built on top of Websockets.

Why Soketi?

There are a few reasons why you might want to use a realtime service like Soketi or Pusher instead of building your own websocket server.

By building a separate websocket service, you can offload the complexity of managing websocket connections from your main application. Since websockets are stateful connections, they require more consideration to scale horizontally than stateless HTTP connections.

Another reason to use a service like Soketi is that it provides a simple API for publishing and subscribing to channels, this API can be used across multiple clients to subscribe to, and also multiple servers to publish to.

Understanding Soketi

Subscribing

As mentioned earlier, you can use Pusher libraries to interact with the Soketi server. The Pusher libraries can be split into client libraries which are used for subscribing and server libraries which are used for publishing.

In order to subscribe, we need to use the Pusher client library. Let's have a look at how we can subscribe to a channel using the Pusher Javascript client library pusher-js. The example below is using version 7.6.0 of pusher-js.

import PusherJS from "pusher-js";

const client = new PusherJS("some-key", {
    wsHost: "127.0.0.1",
    wsPort: 6001,
    forceTLS: false,
});

client.subscribe("chat-room").bind("message", (message: any) => {
    console.log(message)
});

Let's break down what's happening here in the example above.

Creating a websocket connection

When we create a new PusherJS client, we create a websocket connection to the Soketi server. Since Soketi is meant to be a service that works with multiple apps, we need to specify the app key when creating the connection.

Underneath the hood, the request looks like this:

GET ws://127.0.0.1:6001/app/some-key?protocol=7&client=js&version=7.6.0&flash=false
Connection: Upgrade
Upgrade: websocket
Sec-Websocket-Key: yTFDb21QgfG6VwUc8hJV/w==
Sec-Websocket-Version: 13
Sec-Websocket-Extensions: permessage-deflate; client_max_window_bits

Websocket connections are usually initiated with a GET request to the server, with the Upgrade header set to websocket. The server then responds with a 101 Switching Protocols status code, which means the connection has been upgraded to a websocket connection.

Subscribing to a channel

After creating the websocket connection, we can subscribe to a channel using the subscribe method on the PusherJS client. In the example above, we are subscribing to the chat-room channel.

Underneath the hood, the following message is sent over the established websocket connection to the Soketi server, requesting to subscribe to the channel.

{
  "event": "pusher:subscribe",
  "data": {
    "auth": "",
    "channel": "chat-room"
  }
}

Notice that the auth field is empty. This is because the channel is public, and no authentication is required to subscribe to this channel.

If the subscription is successful, the Soketi server will respond with a message like this:

{
  "event": "pusher_internal:subscription_succeeded",
  "channel": "chat-room"
}

Publishing

Once we have a client subscribed to a channel, we can publish messages to that channel. In order to publish messages, we need to use the Pusher server library. Let's have a look at how we can publish a message to a channel using the Pusher

import Pusher from "pusher";

const pusher = new Pusher({
    appId: "some-id",
    key: "some-key",
    secret: "some-secret",
    host: "127.0.0.1",
    port: "6001",
});

pusher.trigger("chat-room", "message", "hello world");

Let's break down what's happening here in the example above.

When we call the trigger method on the Pusher server library, it sends a POST request to the Soketi server. An example of the request is shown below:

POST http://127.0.0.1:6001/apps/some-id/events?auth_key=some-key&auth_timestamp=1721287663&auth_version=1.0&
    body_md5=9ed49240e1fc03bfd8c168731dcd1b6a&
    auth_signature=9b059bd5d7d30ce012fd9c2c8cdffd6fda10ebc9e713d0ffadcf3cbfc03809c0

{
  "name": "message",
  "data": "hello world",
  "channels": [
    "chat-room"
  ]
}

The request path contains the app ID. This is used to identify the app that the message is being published to as mentioned earlier.

The request parameters contain a number of fields that are used to authenticate the request.

  • auth_key: The secret key of the app.

  • auth_timestamp: The timestamp of the request.

  • auth_version: The version of the authentication protocol.

  • body_md5: The MD5 hash of the message body.

  • auth_signature: The signature of the request.

The message body contains the name of the event, the data to be sent, and the channels to send the message to.

When the Soketi server receives the message, it will broadcast the message to all clients subscribed to the chat-room. The following message is sent to subscribed clients:

{
  "event": "message",
  "channel": "chat-room",
  "data": "hello world"
}

Conclusion

In this article, we have explored how realtime services like Soketi or Pusher work. We have seen how to subscribe to a channel and publish messages to a channel using the Pusher client and server libraries. We have also seen the messages that are sent between the client and server when subscribing and publishing messages.

This is the first part of a series on exploring how realtime services work. In future parts, we will look into how

  • Why we need ping/pong messages in Websockets

  • How to handle disconnections and reconnects

  • How to horizontally scale a realtime service like Soketi

  • How to authenticate and authorize users to subscribe to channels

If you have anything you'd like to learn more about, feel free to let me know in the comments.

Did you find this article valuable?

Support Edrick Leong's blog by becoming a sponsor. Any amount is appreciated!