Skip to content

Latest commit

 

History

History
660 lines (477 loc) · 24.5 KB

socketcluster-protocol.md

File metadata and controls

660 lines (477 loc) · 24.5 KB

SocketCluster Protocol Overview

SocketCluster protocol is implemented on top of the WebSockets protocol and consists of multiple components:

Minimal requirements for a simple SocketCluster compatible client are to implement:
Handshake, ping/pong and (at least partially) the Event layer.

Pub/Sub and Authentication layers are completely optional.

Contrast between Protocol V1 and V2

  • SocketCluster <=v14 uses Protocol v1.
    SocketCluster >=v15 uses Protocol v2 by default and supports protocolVersion configuration option, which allows it to work with Protocol v1.

  • SocketCluster <=v14 doesn't send back Handshake event response, if cid is not specified in Handshake event.
    SocketCluster >=v15 always sends back Handshake event response, regardles of cid presence. If cid is not present in Handshake event, rid is omitted from Handshake event response.

  • In SocketCluster >=v15 #disconnect event is deprecated and no longer in use.

  • Protocol V1 uses '#1' and '#2' for ping/pong
    Protocol V2 uses empty strings '' for both.

  • In Protocol V1 all event names starting with symbol # are considered reserved for special control events.
    In Protocol V2 only a handful of event names starting with symbol # are considered reserved.

Reserved event names

Protocol V1:

  • All event names starting with symbol #

Protocol V2:

Call ID & Response ID

cid - Call ID
rid - Response ID

Some events require acknowledgement from another side of communication, in other words they expect event responses.
In order to track which event responses belong to which events, cid and rid exist in SocketCluster Protocol.

cid must be unique for each event sent, during the whole socket connection lifetime.
Call IDs originated from server and Call IDs originated from client are two different sets of ids and are being appointed and tracked separately.

cid, included in events sent from socketcluster-server to a SocketCluster client, for each new socket connection, will always start with number 1 and will be incremented with each event sent.

In your custom SocketCluster client you could use something like UUID strings for cid but, for efficiency, it's recommended to also use number 1 and increment it with each subsequent event sent.

Some special events expect no response, hence cid for them is not required and ignored if present.

Handshake

As soon as you establish a WebSocket connection, you are to send a special Handshake event for socketcluster-server to initiate the socket.
Clients are not allowed to interact with the server before Handshake.

Handshake event is a JSON-encoded string with the following structure:

{
  event: '#handshake'

  // [optional] A JSON-compatible Object
  data: { },

  // [optional] Call ID
  cid: 1
}

Handshake event response is a JSON-encoded string with the following structure:

{
  data: {
    // A unique ID, assigned to this socket connection by the server
    id: 'Y7gRvz-hVW_uXx5qAAH',

    // Value of `pingTimeout` configuration option of the server
    pingTimeout: 20000, // ms

    // Look at the Authentication layer overview for more information
    isAuthenticated: false
  }

  // [optional] Response ID
  rid: 1,
}

socketcluster-server <=v14:

  • If cid was specified in the Handshake event, socketcluster-server will send back Handshake event response with matching rid.
    If cid was not specified, no Handshake event response will be sent.

socketcluster-server >=v15:

  • Whether or not cid was specified in the Handshake event, socketcluster-server will always send back Handshake event response.
    If cid was specified in the Handshake event, Handshake event response will include matching rid.
    If cid was not specified, rid will be omitted.

Connection health check (ping/pong)

socketcluster-server periodically sends ping messages to connected clients to check whether or not a connection is still alive.
A SocketCluster client has to answer every ping message with pong message as soon as possible.

Protocol V1:

Ping message (from server) is a String: '#1'
Pong message (from client) is a String: '#2'

Protocol V2:

Ping message (from server) is an empty String: ''
Pong message (from client) is an empty String: ''

Ping/pong mechanism is required to account for cases when a connection might be closed without sending a proper SocketCluster #disconnect* event or WebSockets Close control frame.
For example, if a user's internet drops out suddenly, there would be no way to tell that the socket is no longer connected otherwise.

* - In Protocol V2 #disconnect event is deprecated and no longer in use.

Event layer

Event layer is responsible for one-to-one communication between a particular socket connection and the server.

Basic part

Basic part of the Event layer is responsible for transmitting and receiving user-defined events.
API example from JavaScript socketcluster-client v17:

// Transmit an event to the server
socket.transmit('eventName', data)

// Receive events from the server
for await (const data of socket.receiver('eventName')) {
  console.info('received data', data)
}

For more in depth knowledge on API visit https://socketcluster.io/docs/basic-usage

Transmitted event is a JSON-encoded string with the following structure:

{
  // Arbitrary name of the event*
  event: 'eventName',

  // [optional] Any JSON-compatible data
  data: eventData,
}

* - Some event names starting with '#' are reserved for special control events in SocketCluster Protocol.

Transmitted events never expect responses.
Even if action.TRANSMIT was blocked within agServer.MIDDLEWARE_INBOUND, no response will be sent.

Advanced part

Advanced part of the Event layer is responsible for invoking and processing Remote Procedure Calls.
API example from JavaScript socketcluster-client v17:

// Invoke a remote procedure on the server
const responseData = await socket.invoke('procedureName', data)

// Process remote procedure calls from the server
for await (const request of socket.procedure('procedureName')) { 
  console.info('received data', request.data)
  request.end(dataToReturnToServer)
}

Transmitted events and RPC are similar in structure. They share the same property event for their names, but they are different entities.
In order to invoke a RPC, a SocketCluster client should send a RPC request.
Unlike transmitted events, every RPC request must include a unique cid (Call ID), because every RPC request expects a RPC response with matching rid (Response ID) from another side of communication.

RPC request is a JSON-encoded string with the following structure:

{
  // Call ID
  cid: 12345,

  // Arbitrary name of the procedure*
  event: 'procedureName',

  // [optional] Any JSON-compatible data
  data: procedureData
}

* - Some procedure names starting with '#' are reserved for special control events in SocketCluster Protocol.

When the RPC request is processed, a RPC response should be sent back.

Successful RPC response is a JSON-encoded string with the following structure:

{
  // Response ID
  rid: 12345,

  // [optional] Any JSON-compatible data
  data: responseData
}

If the RPC request was blocked within agServer.MIDDLEWARE_INBOUND, then the argument, which was provided to the action.block(err) method, will be included into the RPC response as error property.
If no argument was provided to the action.block() method, the error will contain default SocketCluster SilentMiddlewareBlockedError:

Unsuccessful RPC response is a JSON-encoded string with the following structure:

{
  rid: 12345,

  error: {
    message: 'The invoke AGAction was blocked by inbound middleware',
    name: 'SilentMiddlewareBlockedError',
    type: 'inbound'
  }
}

Every RPC response must include rid exactly matching cid of the respective RPC.
As an example, let's invoke a RPC from the server side.
API example from socketcluster-server v17:

try {
  const responseData = await socket.invoke('procedureName', data)
} catch (err) {
  if (err.name === 'TimeoutError') {
    // ...
  }
}

If no response with matching rid will be received from a client, the socket.invoke method will throw TimeoutError, after time interval specified in ackTimeout configuration option of the server.

Most of the SocketCluster clients follow the same logic.
A SocketCluster client sets a timer (alike setTimeout) for each RPC sent, with consideration of cid. Those timers expose a TimeoutError when are finished. And if the client receives a RPC response with rid matching cid of one of the ongoing timers, the client destroys the timer before it fires up.

Pub/Sub layer

Pub/Sub layer is responsible for one-to-many communication between a particular socket connection or a particular SocketCluster worker and unlimited amount of connected sockets, which are subscribed to a Pub/Sub channel.

Subscribe

In order to subscribe a socket connection to a Pub/Sub channel, a SocketCluster client should send to server a subscription request.
API example from JavaScript socketcluster-client v17:

const channel = socket.subscribe('channelName')
for await (const message of channel) {
  console.info(message)
}

For more in depth knowledge on API visit https://socketcluster.io/docs/basic-usage

Subscription request is a JSON-encoded string with the following structure:

{
  event: '#subscribe',

  data: {
    // Arbitrary name of a Pub/Sub channel to subscribe to
    channel: 'channelName'
  },

  cid: 12345
}

When subscription request will be processed, socketcluster-server will send back subscription response with matching rid. So the client would know it's successfully subscribed to the channel.

Successful subscription response is a JSON-encoded string with the following structure:

{
  rid: 12345
}

If the subscription request was blocked within agServer.MIDDLEWARE_INBOUND, then the argument, which was provided to the action.block(err) method, will be included into the subscription response as error property.
If no argument was provided to the action.block() method, the error will contain default SocketCluster SilentMiddlewareBlockedError:

Unsuccessful subscription response is a JSON-encoded string with the following structure:

{
  rid: 12345,

  error: {
    message: 'The subscribe AGAction was blocked by inbound middleware',
    name: 'SilentMiddlewareBlockedError',
    type: 'inbound'
  }
}

Publish

In order to publish a message to a Pub/Sub channel, a SocketCluster client should send to server a publish request.
API example from JavaScript socketcluster-client v17:

// transmitPublish includes `cid`
socket.transmitPublish('channelName', messageData)

// invokePublish does not include `cid`
const responseData = await socket.invokePublish('channelName', messageData)

Publish request is a JSON-encoded string with the following structure:

{
  event: '#publish',

  data: {
    // Name of the channel to publish message to
    channel: 'channelName',
    // [optional] Any JSON-compatible data
    data: messageData
  },

  // [optional] Call ID
  cid: 12345
}

If cid was specified, socketcluster-server will send back publish response with matching rid. So the client would know the sent message was successfully published to the channel.

Successful publish response is a JSON-encoded string with the following structure:

{
  rid: 12345
}

If cid was specified and the publish request was blocked within agServer.MIDDLEWARE_INBOUND, then the argument, which was provided to the action.block(err) method, will be included into the publish response as error property.
If no argument was provided to the action.block() method, the error will contain default SocketCluster SilentMiddlewareBlockedError:

Unsuccessful publish response is a JSON-encoded string with the following structure:

{
  rid: 12345,

  error: {
    message: 'The publishIn AGAction was blocked by inbound middleware',
    name: 'SilentMiddlewareBlockedError',
    type: 'inbound'
  }
}

If cid was not specified in the publish request, no publish response will be sent even if the request was unsuccessful.

Unsubscribe

In order to unsubscribe a socket connection from a Pub/Sub channel, a SocketCluster client should send to server an unsubscription event.

Unsubscription event is a JSON-encoded string with the following structure:

{
  event: '#unsubscribe',

  // Name of a Pub/Sub channel to unsubscribe from
  data: 'channelName',

  // [optional] Call ID
  cid: 12345
}

When unsubscription event will be processed, socketcluster-server will send back unsubscription event response with matching rid. So the client would know it's successfully unsubscribed from the channel.

Unsubscription event response is a JSON-encoded string with the following structure:

{
  rid: 12345
}

If no cid was specified in the unsubscription event, no unsubscription event response will be sent back.

Kick out

It's possible, from server side, to forcibly unsubscribe a socket connection from one or more particular Pub/Sub channels or from all Pub/Sub channels at once.
API example from socketcluster-server v17:

socket.kickOut(['channelName', 'channelName2'], 'custom message')

In that case a SocketCluster client will receive a special #kickOut event. Or multiple events, if it was kicked from multiple channels. One per each channel it was kicked from.

kickOut event is a JSON-encoded string with the following structure:

{
  event: '#kickOut',

  data: {
    // Name of the Pub/Sub channel the socket connection was kicked from
    channel: 'channelName',

    // [optional] If a message has been provided to the socket.kickOut method
    message: 'custom message'
  }
}

Authentication layer

Authentication layer is responsible for means of acquiring and storing an authentication token on client side, as well as for transfering previously acquired authentication token to server for processing.

Authentication process in SocketCluster is deeply customizable. You could implement and use in your SocketCluster client virtually any authentication strategy for your application.
Here let's review the default SocketCluster authentication strategy, which uses JWT as the authentication token and transfers it to server via WebSockets connection within Handshake event.

Token acquisition

For more in depth knowledge on authentication process visit https://socketcluster.io/docs/authentication
API example from socketcluster-server v17:

socket.setAuthToken({username: 'Alice', channels: []})

When setAuthToken method is called on the server side, a SocketCluster client will receive a JSON-encoded string with the following structure:

{
  event: '#setAuthToken',

  data: {
    token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkFsaWNlIiwiY2hhbm5lbHMiOltdLCJUaGFua1lvdUZvckNob29zaW5nU29ja2V0Q2x1c3RlciI6dHJ1ZSwiaWF0IjoxNjc0NzMxODc3LCJleHAiOjE2NzQ4MTgyNzd9.MzQ0QQzofbtlnzvPbeTgtpcvg8Sh6cY8EwXqNXHj5ns'
  }
}

Receiving #setAuthToken event on the client side means the socket connection is now authenticated. A SocketCluster client is supposed to store the acquired token somewhere locally for later use. If the client was already authenticated, it's supposed to replace previously stored token with the new one.

Transfer token to server for processing

In the default authentication strategy this could be done in two ways:

Authentication within Handshake

The most practical way is to include previously acquired authentication token within the Handshake event.
This way socketcluster-server will automatically pick up the token and your client will become authenticated right away.

Handshake event is a JSON-encoded string with the following structure:

{
  event: '#handshake',

  data: {
    authToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkFsaWNlIiwiY2hhbm5lbHMiOltdLCJUaGFua1lvdUZvckNob29zaW5nU29ja2V0Q2x1c3RlciI6dHJ1ZSwiaWF0IjoxNjc0NzMxODc3LCJleHAiOjE2NzQ4MTgyNzd9.MzQ0QQzofbtlnzvPbeTgtpcvg8Sh6cY8EwXqNXHj5ns'
  },

  // [optional] Call ID
  cid: 1
}

Depending on success of authentication process, Handshake event response may include different information.
If provided authToken is valid and not expired, a SocketCluster client will receive Handshake event response with no authError property included.

Handshake event response is a JSON-encoded string with the following structure:

{
  // [optional] Response ID
  rid: 1,

  data: {
    // A unique ID, assigned to this socket connection by the server
    id: 'ZnY2picItxGyKbzeAAAE',

    // Value of `pingTimeout` configuration option of the server
    pingTimeout: 20000, // ms

    // If true, then `#setAuthToken` event will be received immediately after
    isAuthenticated: true
  }
}

If any errors will occur during authentication process, for example the provided authToken is invalid or expired, or Handshake was blocked in the server middleware, then Handshake event response will include authError property describing the occured error. More about authentication errors

Handshake event response is a JSON-encoded string with the following structure:

{
  // [optional] Response ID
  rid: 1,

  data: {
    // A unique ID, assigned to this socket connection by the server
    id: 'Y7Uw-jHCJP_gld4QAAAA',

    // Value of `pingTimeout` configuration option of the server
    pingTimeout: 20000, // ms

    // If false, then `#removeAuthToken` event will be received immediately after
    isAuthenticated: false,

    authError: {
      name: 'ErrorName',
      message: 'error message',
      isBadToken: true
    }
  }
}

Authentication event

Instead of including authentication token within Handshake event, a SocketCluster client could send special Authentication event any time after Handshake.
This could be useful if authentication token somehow becomes available in your client after Handshake.
As an example, if you use JavaScript socketcluster-client in a web browser and get multiple tabs opened, you could acquire authentication token in one tab and, when authToken becomes available in the localStorage, other tabs would send to server the authToken within Authentication event, for their socket connections to also become authenticated, without need for page reload.

Authentication event is a JSON-encoded string with the following structure:

{
  event: '#authenticate',

  // A String with an authentication token
  data: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkFsaWNlIiwiY2hhbm5lbHMiOltdLCJUaGFua1lvdUZvckNob29zaW5nU29ja2V0Q2x1c3RlciI6dHJ1ZSwiaWF0IjoxNjc0NzMxODc3LCJleHAiOjE2NzQ4MTgyNzd9.MzQ0QQzofbtlnzvPbeTgtpcvg8Sh6cY8EwXqNXHj5ns',

  // [optional] Call ID
  cid: 12345
}

Depending on success of authentication process, Authentication event response may include different information.
If authToken is valid and not expired, a SocketCluster client will receive successful Authentication event response, which means socket connection is now authenticated.
If cid was specified in Authentication event, Authentication event response will also include matching rid.

Successful Authentication event response is a JSON-encoded string with the following structure:

{
  // [optional] Response ID
  rid: 12345,

  data: {
    isAuthenticated: true,
    authError: null
  }
}

If any errors will occur during authentication process, for example authentication token is invalid or expired, Authentication event response will include error property describing the occured error. More about authentication errors

Unsuccessful Authentication event response is a JSON-encoded string with the following structure:

{
  // [optional] Response ID
  rid: 12345,

  // If error is present, then `#removeAuthToken` event will be received immediately after
  error: {
    name: 'ErrorName',
    message: 'error message',
    isBadToken: true
  }
}

Deathentication

When you deauthenticate a socket connection from server side, a SocketCluster client will receive special #removeAuthToken event. It means the socket connection is no longer authenticated.
Also Deauthentication event could be received immediately after sending Handshake or Authentication event, if authToken provided within Handshake or Authentication event is invalid.
API example from socketcluster-server v17:

socket.deauthenticate()

Deauthentication event is a JSON-encoded string with the following structure:

{
  event: '#removeAuthToken'
}

No response is expected for Deauthentication event.

If a SocketCluster client intends to deauthenticate, it should send to server identical Deauthentication event.

In both cases the client should remove previously stored authentication token, as well as to perform any other operations you deem necessary, like routing client to login screen to start over the token acquisition process


Authentication errors

By default authentication errors consist of name, message and isBadToken. But they also could include additional information depending on the error type. Or to have various content in case of user-defined errors.

When you block an authentication action (action.type === action.AUTHENTICATE) within the server middleware (agServer.MIDDLEWARE_INBOUND), the error object you pass to the action.block(err) method becomes the authentication error, which your client will receive.
For more in depth knowledge on API visit https://socketcluster.io/docs/middleware-and-authorization

name:
Could be either one of the SocketCluster error names or a user-defined error name.

All possible SocketCluster errors are listed in SC-errors module.
List of the names which are relevant to the authentication process, in order of frequency occurring are:

  • AuthTokenExpiredError
  • AuthTokenInvalidError
  • AuthTokenError
  • AuthTokenNotBeforeError

message:
Could be originated from SocketCluster or from underlying libraries like jsonwebtoken. It also could be a user-defined error message.
List of all the possible error messages is hard to come by, but some of them you could find in JWT Readme

isBadToken:
If an authentication error was caused by an expired or invalid or malformed JWT or by incorrect JWT signature, isBadToken will be true. If the authentication error was caused by any other reason, it will be false.

As an example, the two most frequently occurring errors are:

authError: {
  name: 'AuthTokenExpiredError',
  message: 'jwt expired',
  expiry: '2023-01-01T01:23:45.000Z',
  isBadToken: true
}

authError: {
  name: 'AuthTokenInvalidError',
  message: 'invalid token',
  isBadToken: true
}

Any authentication error will always cause a SocketCluster client to receive #removeAuthToken event immediately after the event response, which contained the error.


Thank you for your time implementing custom SocketCluster client! ❤️
If you have completed one, please, open a pull request to add your client to the list of SocketCluster clients
If you encounter any errors or have any questions, feel free to ask for help in SocketCluster Gitter chat room