-
-
Notifications
You must be signed in to change notification settings - Fork 29
Heartbeats and SockJS
SockJS till version 0.3.1 didn't support real heartbeats. The only supported thing was a single "heartbeat" frame, that could be sent from the server at any time. The spec required the client to ignore this frame. For example, this was a valid conversation:
Server -- Client
<---- request for /info url
----> reply for /info
<---- initial session request
"o" ----> open frame
... after some time
"h" ----> heartbeat frame - must be ignored by the client
Many load balancers by default kill long-lived connections after some time of inactivity. In this scheme the heartbeat frame is just used to keep traffic flowing and thus keep the outgoing (server->client) connection alive for longer.
By default the heartbeat frame should be ignited every 25 seconds, in order to circumvent the common 30-second inactivity timeout.
In order to allow user to write a custom keepalive logic, SockJS-client does emit an event on heartbeat. For example:
var sock = new SockJS('/my_prefix');
sock.onheartbeat = function() {
console.log('heartbeat');
};
Additionally, some transports need to send a prelude (header) before they start working, and the 'heartbeat' frame is used for that fill-out. In other words - it is possible to get a large "heartbeat" frame before the "open" frame.
Frame | websockets | streaming | polling |
---|---|---|---|
heartbeat before "o" | N/A | ignored | N/A |
heartbeat after "o" | ignored | ignored | ignored |
empty data frame "a[]" | ignored | ignored | ignored |
In #64 Nick Martin noticed that SockJS doesn't behave nicely when the server disappears from the network (unplug cable type of failure).
To support this use case we decided to implement proper heartbeats / keepalives on the SockJS layer. Heartbeats are an "opt-in" feature and is not tested by the usual sockjs-protocol tests.
When server "opts-in" to heartbeats the client is obliged to reply to
heartbeat frames. The server informs the client about this by setting
server_heartbeat_interval
setting in the info
url, with a
specified timeout. Since that moment the server also is obliged to
send some traffic (data or heartbeats) to the client at most every
server_heartbeat_interval
milliseconds.
To recap:
- The server sets
server_heartbeat_interval
option oninfo
url. - The client must respond with an empty data frame
[]
to all heartbeat frames received after the "o" open frame (ie: not counting prelude). - The server must send some traffic (heartbeat or keepalive) at least
every
server_heartbeat_interval
milliseconds. - The server may send heartbeat frames more often if it wishes to receive data from the client and thus ensure that the link indeed works in both directions.
- If the server wants to trigger some traffic on the wire and and not
cause a response from the client, it may send an empty data frame
a[]
at any time after the "open" frame.
An example session may look like:
Server -- Client
<---- request for /info url
----> reply for /info, including `server_heartbeat_interval`
option set to, say, 25000 (ie: 25 seconds)
<---- initial session request
"o" ----> open frame
... after at most 25 seconds
"h" ----> heartbeat frame - client must respond
<---- "[]" client responds with an empty data frame
...
"h" ----> heartbeat frame again
<---- "["x"]" client responds with a data frame, as there
was some data to be delivered, that's enough
...
"a[]" ---> at any time the server may send an empty
data frame, it must be ignored by the client
With this scheme if the client doesn't receive data or heartbeat frame
within server_heartbeat_interval
time, it may disconnect abruptly.
Similarly, after sending a heartbeat frame the server may disconnect
if the response (any data coming in, including empty data frame) is
not received in server_heartbeat_interval
milliseconds.
Apart from the above constrains the details of when exactly to send heartbeat or an empty frame are left to server writes an are implementation specific.
Polling transports are a notable exception - a client doesn't need to send an empty frame in such transport, as this is spurious - a new long-polling request needs to be fired any way. For example:
Server -- Client
<---- request for /info url
----> reply for /info, including `server_heartbeat_interval`
option set to, say, 25000 (ie: 25 seconds)
<---- initial session request
"o" ----> open frame
<---- long-polling request
... after at most 25 seconds
"h" ----> heartbeat frame - usually a client must respond,
but in this case hooking long-polling request
is enough
<---- long-polling request
...
"h" ----> heartbeat frame again
<---- long-polling request
...
"a[]" ---> at any time the server may send an empty
data frame, it must be ignored by the client
<---- long-polling request
Without server_heartbeat_interval
option set, the semantics are
identical to SockJS pre 0.3.1.
With server_heartbeat_interval
option set on the 'info' url:
Frame | websockets | streaming | polling |
---|---|---|---|
heartbeat before "o" | N/A | ignored | N/A |
heartbeat after "o" | must reply with data or "[]" | must reply with data or "[]" | ignored (as a long poll must happen) |
empty data frame "a[]" | ignored | ignored | ignored |
Polling transports are buffering outgoing data. Due to the way
browsers work, the data will be received only after all of it got
delivered. In the unlikely case of a slow connection, it is possible
that sending data to the client could take longer than
server_heartbeat_interval
milliseconds. The client is free to
abruptly disconnect in such case.
Because of that, the server must ensure that data sent to the client in a single poll is short enough to be delivered in a reasonably short time.
In other words - sending all the items from the internal buffer during a single poll may not be a good idea.