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.
-
SocketCluster <=v14 uses Protocol v1.
SocketCluster >=v15 uses Protocol v2 by default and supportsprotocolVersion
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 ofcid
presence. Ifcid
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.
Protocol V1:
- All event names starting with symbol
#
Protocol V2:
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.
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.
{
event: '#handshake'
// [optional] A JSON-compatible Object
data: { },
// [optional] Call ID
cid: 1
}
{
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 matchingrid
.
Ifcid
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.
Ifcid
was specified in the Handshake event, Handshake event response will include matchingrid
.
Ifcid
was not specified,rid
will be omitted.
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.
Ping message (from server) is a String: '#1'
Pong message (from client) is a String: '#2'
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 is responsible for one-to-one
communication between a particular socket connection and the server.
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
{
// 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 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.
{
// 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.
{
// 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
:
{
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 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.
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
{
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.
{
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
:
{
rid: 12345,
error: {
message: 'The subscribe AGAction was blocked by inbound middleware',
name: 'SilentMiddlewareBlockedError',
type: 'inbound'
}
}
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)
{
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.
{
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
:
{
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.
In order to unsubscribe a socket connection from a Pub/Sub channel, a SocketCluster client should send to server an unsubscription event.
{
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.
{
rid: 12345
}
If no cid
was specified in the unsubscription event, no unsubscription event response will be sent back.
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.
{
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 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.
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.
In the default authentication strategy this could be done in two ways:
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.
{
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.
{
// [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
{
// [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
}
}
}
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.
{
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
.
{
// [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
{
// [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
}
}
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()
{
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
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 name
s 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 message
s 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