Client protocol description

This chapter aims to help developers to implement new client library or understand how already implemented clients work. This chapter is not complete. I will update it from time to time with additional information.

Centrifugo already has Javascript, Go, iOS, Android, Python clients to connect your application users.

One of the ways to understand how to implement new client is looking at source code of existing clients, for example centrifuge-js or centrifuge-python.

Currently websocket is the only available transport to implement client. Centrifugo also supports SockJS connections from browser but it's only for browser usage, there is no reason to use it from other environments. All communication done via exchanging JSON messages.

Let's look at client protocol step-by-step.

Connect, subscribe on channel and wait for published messages

Websocket endpoint is:

ws://your_centrifugo_server.com/connection/websocket

Or in case of using TLS:

wss://your_centrifugo_server.com/connection/websocket

What client should do first is to create Websocket connection to this endpoint.

After successful connection client must send connect command to server to authorize itself.

connect command is a JSON structure like this (all our examples here use Javascript language, but the same can be applied to any language):

var message = {
    "uid": "UNIQUE COMMAND ID",
    "method": "connect",
    "params": {
        "user": "USER ID STRING",
        "timestamp": "STRING WITH CURRENT TIMESTAMP SECONDS",
        "info": "OPTIONAL JSON ENCODED STRING",
        "token": "SHA-256 HMAC TOKEN GENERATED FROM PARAMETERS ABOVE"
    }
}

connection.send(JSON.stringify(message))

Look at method key with a name of our command – connect.

Centrifugo can parse an array of messages in one request, so you can add command above into array and send result to Centrifugo server over Websocket connection established before:

var messages = [message]
connection.send(JSON.stringify(messages))

Description of connect command parameters described in a chapter about javascript client.

In short here:

  • user - current application user ID (string)
  • timestamp - current Unix timestamp as seconds (string)
  • info - optional JSON string with client additional information (string)
  • token - SHA-256 HMAC token generated on backend (based on secret key from Centrifugo configuration) to sign parameters above.

Application backend must provide all these connection parameters (together with generated HMAC SHA-256 token) to client (pass to template when client opens web page for example).

After receiving connect command over Websocket connection Centrifugo server uses the same algorithm (HMAC SHA-256) to generate the token. Correct token proves that client provided valid user ID, timestamp and info in its connect message.

Note that Centrifugo can also allow non-authenticated users to connect to it (for example sites with public stream with notifications where all visitors can see new events in real-time without actually logging in). For this case backend must generate token using empty string as user ID. In this scenario anonymous access must be enabled for channels explicitly in configuration of Centrifugo.

What you should do next is wait for response from server to connect command you just sent.

In general structure that will come from Centrifugo server to your client looks like this:

[{response}, {response}, {response}]

Or just single response

{response}

I.e. array of responses or one response to commands you sent before. I.e. in our case Centrifugo will send to our client:

[{connect_command_response}]

Or just:

{connect_command_response}

So client must be ready to process both arrays of responses and single object response. This rule applies to all communication.

Every response is a structure like this:

{
    "uid": "ECHO BACK THE SAME UNIQUE COMMAND ID SENT IN REQUEST COMMAND",
    "method": "COMMAND NAME TO WHICH THIS RESPONSE REFERS TO",
    "error": "ERROR STRING, IF NOT EMPTY THEN SOMETHING WENT WRONG AND BODY SHOULD NOT BE PROCESSED",
    "body": "RESPONSE BODY, CONTAINS USEFUL RESPONSE DATA"
}

Javascript client uses method key to understand what to do with response. As Javascript is evented IO language it just calls corresponding function to react on response. Unique uid also can be used to implement proper responses handling in other languages. For example Go client remember command uid to call some callback when it receives response from Centrifugo.

General rule - if response contains a non-empty error then server returned an error.

You should not get errors in normal workflow. If you get an error then most probably you are doing something wrong and this must be fixed on development stages. It can also be internal server error from Centrifugo. Only developers should see text of protocol errors – they are not supposed to be shown to your application clients.

In case of successful connect response body is:

{
    "client": "UNIQUE CLIENT ID SERVER GAVE TO THIS CONNECTION",
    "expires": "false",
    "expired": false,
    "ttl": 0
}

At moment let's just speak about client key. This is unique client ID Centrifugo set to this connection.

As soon your client successfully connected and got its unique connection ID it is ready to subscribe on channels.

var message = {
    'uid': 'UNIQUE COMMAND ID',
    'method': 'subscribe',
    'params': {
        'channel': "CHANNEL TO SUBSCRIBE"
    }
}

Just send this subscribe command in the same way as connect command before.

After you received successful response on this subscribe command your client will receive messages published to this channel. Those messages will be delivered through Websocket connection as response with method message. I.e. response will look like this:

{
    "method": "message",
    "body": {
        "uid": "8d1f6279-2d13-45e2-542d-fac0e0f1f6e0",
        "info":{
            "user":"42",
            "client":"73cd5abb-03ed-40bc-5c87-ed35df732682",
            "default_info":null,
            "channel_info":null
        },
        "channel":"jsfiddle-chat",
        "data": {
            "input":"hello world"
        },
        "client":"73cd5abb-03ed-40bc-5c87-ed35df732682"
    }
}

body of message response contains channel to which message corresponds and data key - this is an actual JSON that was published into that channel.

This is enough to start with - client established connection, authorized itself sending connect command, subscribed on channel to receive new messages published into that channel. This is a core Centrifugo functionality. There are lots of other things to cover – channel presence information, channel history information, connection expiration, private channel subscriptions, join/leave events and more but in most cases all you need from Centrifugo - subscribe on channels and receive new messages from those channels as soon as your backend published them into Centrifugo server API.

Available methods

Lets now look at all available methods your client can send or receive:

connect
disconnect
subscribe
unsubscribe
publish
presence
history
join
leave
message
refresh
ping

Some of this methods used for client to server commands (publish, presence, history etc which then get a response from server with the same method and unique uid in it), some for server to clients (for example join, leave, message – which just come from server in any time when corresponding event occurred).

We have already seen connect, subscribe and publish above. Let's describe remaining.

Client to server commands

connect - send authorization parameters to Centrifugo so your connection could start subscribing on channels.

var message = {
    'uid': 'UNIQUE COMMAND ID',
    'method': 'connect',
    'params': {
        'user': "USER ID STRING",
        'timestamp': "STRING WITH CURRENT TIMESTAMP SECONDS"
        'info': "OPTIONAL JSON ENCODED STRING",
        'token': "SHA-256 HMAC TOKEN GENERATED FROM PARAMETERS ABOVE"
    }
}

subscribe - allows to subscribe on channel after client successfully connected

var message = {
    'uid': 'UNIQUE COMMAND ID',
    'method': 'subscribe',
    'params': {
        'channel': "CHANNEL TO SUBSCRIBE"
    }
}

unsubscribe - allows to unsubscribe from channel

message = {
    'uid': 'UNIQUE COMMAND ID',
    "method": "unsubscribe",
    "params": {
        "channel": "CHANNEL TO UNSUBSCRIBE"
    }
}

publish - allows clients directly publish messages into channel (application backend code will never know about this message). publish must be enabled for channel in sever configuration so this command can work (otherwise Centrifugo will return permission denied error in response).

message = {
    'uid': 'UNIQUE COMMAND ID',
    "method": "publish",
    "params": {
        "channel": "CHANNEL",
        "data": {}  // JSON DATA TO PUBLISH
    }
}

presence – allows to ask server for channel presence information (presence must be enabled for channel in server configuration or Centrifugo will return not available error in response)

message = {
    'uid': 'UNIQUE COMMAND ID',
    "method": "presence",
    "params": {
        "channel": "CHANNEL"
    }
}

history – allows to ask server for channel history information (history must be enabled for channel in server configuration using history_lifetime and history_size options or Centrifugo will return not available error in response)

message = {
    'uid': 'UNIQUE COMMAND ID',
    "method": "history",
    "params": {
        "channel": "CHANNEL"
    }
}

ping - allows to send ping command to server, server will answer this command with ping response.

message = {
    'uid': 'UNIQUE COMMAND ID',
    "method": "ping"
}

Responses of client to server commands

As soon as your client sent command to server it should then receive a corresponding response. Let's look at those response messages in detail.

TODO: write about responses

Server to client commands

message - new message published into channel current client subscribed to. Response for message coming over connection looks like this:

{
    "method":"message",
    "body": {
        "uid": "8d1f6279-2d13-45e2-542d-fac0e0f1f6e0",
        "info":{
            "user":"42",
            "client":"73cd5abb-03ed-40bc-5c87-ed35df732682",
            "default_info":null,
            "channel_info":null
        },
        "channel":"jsfiddle-chat",
        "data": {
            "input":"hello world"
        },
        "client":"73cd5abb-03ed-40bc-5c87-ed35df732682"
    }
}

join - someone joined a channel current client subscribed to. Note that join_leave option must be enabled for channel in server configuration to receive this type of messages. body of this message contains information about new subscribed client.

{
    "method":"join",
    "body": {
        "channel":"$public:chat",
        "data": {
            "user":"2694",
            "client":"3702659c-f28a-4166-5b44-115d9b544b29",
            "default_info": {
                "first_name":"Alexandr",
                "last_name":"Emelin"
            },
            "channel_info": {
                "channel_extra_info_example":"you can add additional JSON data when authorizing"
            }
        }
    }
}

leave - someone left channel current client subscribed to. Note that join_leave option must be enabled for channel in server configuration to receive this type of messages. body of this message contains information about unsubscribed client.

{
    "method":"leave",
    "body": {
        "channel":"$public:chat",
        "data": {
            "user":"2694",
            "client":"3702659c-f28a-4166-5b44-115d9b544b29",
            "default_info": {
                "first_name":"Alexandr",
                "last_name":"Emelin"
            },
            "channel_info": {
                "channel_extra_info_example":"you can add additional JSON data when authorizing"
            }
        }
    }
}

Private channel subscriptions.

As you could see successful connect response body has client field - a unique connection ID issued by Centrifugo to this particular client connection. It's important because it's used when obtaining private channel sign.

We've already seen above that in general case (non-private channel subscription) subscription request that must be sent by client to Centrifugo looks like this:

var message = {
    'uid': 'UNIQUE COMMAND ID',
    'method': 'subscribe',
    'params': {
        'channel': "CHANNEL TO SUBSCRIBE"
    }
}

When subscribing on private channel client must also provide additional fields in params object:

var message = {
    'uid': 'UNIQUE COMMAND ID',
    'method': 'subscribe',
    'params': {
        'channel': "channel to subscribe",
        'client': "current client ID",
        'info': "additional private channel JSON string info",
        'sign': "string channel sign generated on app backend based on client ID and optional info"
    }
}

See chapter about signs to get more knowledge about how to generate such private channel sign on your backend side.

In case of Javascript client we send client ID with private channel names to backend automatically in AJAX request so all that developer needs is to check user permissions (as we call backend via AJAX from browser user will be properly set by application backend session mechanism), generate valid private channel sign and return in response. In case of other clients (for example mobile) there is no convenient way (such as AJAX in web) to get data from backend - so it's up to developer to decide how he wants to obtain channel sign.

Client library should at least provide mechanism to give developer client ID of current connection and mechanism to set client, info and sign fields to subscription request params. As client ID will change after reconnect every time client wants to subscribe on private channel backend must generate new channel sign. So every time client library wants to send private subscription request it must first ask application code for new private channel sign.

To be continued...