mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-13 00:48:12 -05:00
Connection state recovery allows a client to reconnect after a
temporary disconnection and restore its state:
- id
- rooms
- data
- missed packets
Usage:
```js
import { Server } from "socket.io";
const io = new Server({
connectionStateRecovery: {
// default values
maxDisconnectionDuration: 2 * 60 * 1000,
skipMiddlewares: true,
},
});
io.on("connection", (socket) => {
console.log(socket.recovered); // whether the state was recovered or not
});
```
Here's how it works:
- the server sends a session ID during the handshake (which is
different from the current `id` attribute, which is public and can be
freely shared)
- the server also includes an offset in each packet (added at the end
of the data array, for backward compatibility)
- upon temporary disconnection, the server stores the client state for
a given delay (implemented at the adapter level)
- upon reconnection, the client sends both the session ID and the last
offset it has processed, and the server tries to restore the state
A few notes:
- the base adapter exposes two additional methods, persistSession() and
restoreSession(), that must be implemented by the other adapters in
order to allow the feature to work within a cluster
See: f5294126a8
- acknowledgements are not affected, because it won't work if the
client reconnects on another server (as the ack id is local)
- any disconnection that lasts longer than the
`maxDisconnectionDuration` value will result in a new session, so users
will still need to care for the state reconciliation between the server
and the client
Related: https://github.com/socketio/socket.io/discussions/4510
149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
import { createServer } from "http";
|
|
import { io as ioc } from "socket.io-client";
|
|
import { join } from "path";
|
|
import { exec } from "child_process";
|
|
import { Server } from "..";
|
|
import expect from "expect.js";
|
|
import {
|
|
createClient,
|
|
eioHandshake,
|
|
eioPoll,
|
|
eioPush,
|
|
getPort,
|
|
} from "./support/util";
|
|
|
|
describe("close", () => {
|
|
it("should be able to close sio sending a srv", (done) => {
|
|
const httpServer = createServer().listen(0);
|
|
const io = new Server(httpServer);
|
|
const port = getPort(io);
|
|
const net = require("net");
|
|
const server = net.createServer();
|
|
|
|
const clientSocket = createClient(io, "/", { reconnection: false });
|
|
|
|
clientSocket.on("disconnect", () => {
|
|
expect(io.sockets.sockets.size).to.equal(0);
|
|
server.listen(port);
|
|
});
|
|
|
|
clientSocket.on("connect", () => {
|
|
expect(io.sockets.sockets.size).to.equal(1);
|
|
io.close();
|
|
});
|
|
|
|
server.once("listening", () => {
|
|
// PORT should be free
|
|
server.close((error) => {
|
|
expect(error).to.be(undefined);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it("should be able to close sio sending a srv", (done) => {
|
|
const io = new Server(0);
|
|
const port = getPort(io);
|
|
const net = require("net");
|
|
const server = net.createServer();
|
|
|
|
const clientSocket = ioc("ws://0.0.0.0:" + port, {
|
|
reconnection: false,
|
|
});
|
|
|
|
clientSocket.on("disconnect", () => {
|
|
expect(io.sockets.sockets.size).to.equal(0);
|
|
server.listen(port);
|
|
});
|
|
|
|
clientSocket.on("connect", () => {
|
|
expect(io.sockets.sockets.size).to.equal(1);
|
|
io.close();
|
|
});
|
|
|
|
server.once("listening", () => {
|
|
// PORT should be free
|
|
server.close((error) => {
|
|
expect(error).to.be(undefined);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("graceful close", () => {
|
|
function fixture(filename) {
|
|
return (
|
|
'"' +
|
|
process.execPath +
|
|
'" "' +
|
|
join(__dirname, "fixtures", filename) +
|
|
'"'
|
|
);
|
|
}
|
|
|
|
it("should stop socket and timers", (done) => {
|
|
exec(fixture("server-close.ts"), done);
|
|
});
|
|
});
|
|
|
|
describe("protocol violations", () => {
|
|
it("should close the connection when receiving several CONNECT packets", async () => {
|
|
const httpServer = createServer();
|
|
const io = new Server(httpServer);
|
|
|
|
httpServer.listen(0);
|
|
|
|
const sid = await eioHandshake(httpServer);
|
|
// send a first CONNECT packet
|
|
await eioPush(httpServer, sid, "40");
|
|
// send another CONNECT packet
|
|
await eioPush(httpServer, sid, "40");
|
|
// session is cleanly closed (not discarded, see 'client.close()')
|
|
// first, we receive the Socket.IO handshake response
|
|
await eioPoll(httpServer, sid);
|
|
// then a close packet
|
|
const body = await eioPoll(httpServer, sid);
|
|
expect(body).to.be("6\u001e1");
|
|
|
|
io.close();
|
|
});
|
|
|
|
it("should close the connection when receiving an EVENT packet while not connected", async () => {
|
|
const httpServer = createServer();
|
|
const io = new Server(httpServer);
|
|
|
|
httpServer.listen(0);
|
|
|
|
const sid = await eioHandshake(httpServer);
|
|
// send an EVENT packet
|
|
await eioPush(httpServer, sid, '42["some event"]');
|
|
// session is cleanly closed, we receive a close packet
|
|
const body = await eioPoll(httpServer, sid);
|
|
expect(body).to.be("6\u001e1");
|
|
|
|
io.close();
|
|
});
|
|
|
|
it("should close the connection when receiving an invalid packet", async () => {
|
|
const httpServer = createServer();
|
|
const io = new Server(httpServer);
|
|
|
|
httpServer.listen(0);
|
|
|
|
const sid = await eioHandshake(httpServer);
|
|
// send a CONNECT packet
|
|
await eioPush(httpServer, sid, "40");
|
|
// send an invalid packet
|
|
await eioPush(httpServer, sid, "4abc");
|
|
// session is cleanly closed (not discarded, see 'client.close()')
|
|
// first, we receive the Socket.IO handshake response
|
|
await eioPoll(httpServer, sid);
|
|
// then a close packet
|
|
const body = await eioPoll(httpServer, sid);
|
|
expect(body).to.be("6\u001e1");
|
|
|
|
io.close();
|
|
});
|
|
});
|
|
});
|