mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-08 22:48:20 -05:00
Merge remote-tracking branch 'engine.io-protocol/v3'
Source: https://github.com/socketio/engine.io-protocol/tree/v3
This commit is contained in:
1
docs/engine.io-protocol/v3-test-suite/.gitignore
vendored
Executable file
1
docs/engine.io-protocol/v3-test-suite/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
30
docs/engine.io-protocol/v3-test-suite/index.html
Normal file
30
docs/engine.io-protocol/v3-test-suite/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Test suite for the Engine.IO protocol</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/mocha@9/mocha.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="mocha"></div>
|
||||
|
||||
<script src="https://unpkg.com/mocha@9/mocha.js"></script>
|
||||
<script src="https://unpkg.com/chai@4/chai.js" ></script>
|
||||
<script src="https://unpkg.com/chai-string@1/chai-string.js" ></script>
|
||||
|
||||
<script class="mocha-init">
|
||||
mocha.setup("bdd");
|
||||
mocha.checkLeaks();
|
||||
</script>
|
||||
|
||||
<script type="module" src="test-suite.js"></script>
|
||||
|
||||
<script class="mocha-exec">
|
||||
mocha.run();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
10
docs/engine.io-protocol/v3-test-suite/node-imports.js
Normal file
10
docs/engine.io-protocol/v3-test-suite/node-imports.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import fetch from "node-fetch";
|
||||
import { WebSocket } from "ws";
|
||||
import chai from "chai";
|
||||
import chaiString from "chai-string";
|
||||
|
||||
chai.use(chaiString);
|
||||
|
||||
globalThis.fetch = fetch;
|
||||
globalThis.WebSocket = WebSocket;
|
||||
globalThis.chai = chai;
|
||||
1936
docs/engine.io-protocol/v3-test-suite/package-lock.json
generated
Normal file
1936
docs/engine.io-protocol/v3-test-suite/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
docs/engine.io-protocol/v3-test-suite/package.json
Normal file
18
docs/engine.io-protocol/v3-test-suite/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "engine.io-protocol-test-suite",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier -w *.js",
|
||||
"test": "mocha test-suite.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.6",
|
||||
"chai-string": "^1.5.0",
|
||||
"mocha": "^9.2.1",
|
||||
"node-fetch": "^3.2.0",
|
||||
"prettier": "^2.5.1",
|
||||
"ws": "^8.5.0"
|
||||
}
|
||||
}
|
||||
595
docs/engine.io-protocol/v3-test-suite/test-suite.js
Normal file
595
docs/engine.io-protocol/v3-test-suite/test-suite.js
Normal file
@@ -0,0 +1,595 @@
|
||||
const isNodejs = typeof window === "undefined";
|
||||
|
||||
if (isNodejs) {
|
||||
// make the tests runnable in both the browser and Node.js
|
||||
await import("./node-imports.js");
|
||||
}
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const URL = "http://localhost:3000";
|
||||
const WS_URL = URL.replace("http", "ws");
|
||||
|
||||
const PING_INTERVAL = 300;
|
||||
const PING_TIMEOUT = 200;
|
||||
|
||||
function sleep(delay) {
|
||||
return new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
function waitFor(socket, eventType) {
|
||||
return new Promise((resolve) => {
|
||||
socket.addEventListener(
|
||||
eventType,
|
||||
(event) => {
|
||||
resolve(event);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function decodePayload(payload) {
|
||||
const firstColonIndex = payload.indexOf(":");
|
||||
const length = payload.substring(0, firstColonIndex);
|
||||
const packet = payload.substring(firstColonIndex + 1);
|
||||
return [length, packet];
|
||||
}
|
||||
|
||||
async function initLongPollingSession(supportsBinary = false) {
|
||||
const response = await fetch(`${URL}/engine.io/?EIO=3&transport=polling` + (supportsBinary ? "" : "&b64=1"));
|
||||
const text = await response.text();
|
||||
const [, content] = decodePayload(text);
|
||||
return JSON.parse(content.substring(1)).sid;
|
||||
}
|
||||
|
||||
describe("Engine.IO protocol", () => {
|
||||
describe("handshake", () => {
|
||||
describe("HTTP long-polling", () => {
|
||||
it("successfully opens a session", async () => {
|
||||
const response = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling`
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
|
||||
const text = await response.text();
|
||||
const [length, content] = decodePayload(text);
|
||||
|
||||
expect(length).to.eql(content.length.toString());
|
||||
expect(content).to.startsWith("0");
|
||||
|
||||
const value = JSON.parse(content.substring(1));
|
||||
|
||||
expect(value.sid).to.be.a("string");
|
||||
expect(value.upgrades).to.eql(["websocket"]);
|
||||
expect(value.pingInterval).to.eql(PING_INTERVAL);
|
||||
expect(value.pingTimeout).to.eql(PING_TIMEOUT);
|
||||
expect(value.maxPayload).to.be.oneOf([undefined, 1000000]);
|
||||
});
|
||||
|
||||
it("fails with an invalid 'transport' query parameter", async () => {
|
||||
const response = await fetch(`${URL}/engine.io/?EIO=3`);
|
||||
|
||||
expect(response.status).to.eql(400);
|
||||
|
||||
const response2 = await fetch(`${URL}/engine.io/?EIO=3&transport=abc`);
|
||||
|
||||
expect(response2.status).to.eql(400);
|
||||
});
|
||||
|
||||
it("fails with an invalid request method", async () => {
|
||||
const response = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling`,
|
||||
{
|
||||
method: "post",
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket", () => {
|
||||
it("successfully opens a session", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.startsWith("0");
|
||||
|
||||
const value = JSON.parse(data.substring(1));
|
||||
|
||||
expect(value.sid).to.be.a("string");
|
||||
expect(value.upgrades).to.eql([]);
|
||||
expect(value.pingInterval).to.eql(PING_INTERVAL);
|
||||
expect(value.pingTimeout).to.eql(PING_TIMEOUT);
|
||||
expect(value.maxPayload).to.be.oneOf([undefined, 1000000]);
|
||||
|
||||
socket.close();
|
||||
});
|
||||
|
||||
it("fails with an invalid 'EIO' query parameter", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?transport=websocket`
|
||||
);
|
||||
|
||||
if (isNodejs) {
|
||||
socket.on("error", () => {});
|
||||
}
|
||||
|
||||
waitFor(socket, "close");
|
||||
|
||||
const socket2 = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=abc&transport=websocket`
|
||||
);
|
||||
|
||||
if (isNodejs) {
|
||||
socket2.on("error", () => {});
|
||||
}
|
||||
|
||||
waitFor(socket2, "close");
|
||||
});
|
||||
|
||||
it("fails with an invalid 'transport' query parameter", async () => {
|
||||
const socket = new WebSocket(`${WS_URL}/engine.io/?EIO=3`);
|
||||
|
||||
if (isNodejs) {
|
||||
socket.on("error", () => {});
|
||||
}
|
||||
|
||||
waitFor(socket, "close");
|
||||
|
||||
const socket2 = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=abc`
|
||||
);
|
||||
|
||||
if (isNodejs) {
|
||||
socket2.on("error", () => {});
|
||||
}
|
||||
|
||||
waitFor(socket2, "close");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("message", () => {
|
||||
describe("HTTP long-polling", () => {
|
||||
it("sends and receives a payload containing one plain text packet", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "6:4hello",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(200);
|
||||
|
||||
const postContent = await pushResponse.text();
|
||||
|
||||
expect(postContent).to.eql("ok");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pollContent = await pollResponse.text();
|
||||
|
||||
expect(pollContent).to.eql("6:4hello");
|
||||
});
|
||||
|
||||
it("sends and receives a payload containing several plain text packets", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "6:4test16:4test26:4test3",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(200);
|
||||
|
||||
const postContent = await pushResponse.text();
|
||||
|
||||
expect(postContent).to.eql("ok");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pollContent = await pollResponse.text();
|
||||
|
||||
expect(pollContent).to.eql("6:4test16:4test26:4test3");
|
||||
});
|
||||
|
||||
it("sends and receives a payload containing plain text and binary packets (base64 encoded)", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "6:4hello10:b4AQIDBA==",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(200);
|
||||
|
||||
const postContent = await pushResponse.text();
|
||||
|
||||
expect(postContent).to.eql("ok");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pollContent = await pollResponse.text();
|
||||
|
||||
expect(pollContent).to.eql("6:4hello10:b4AQIDBA==");
|
||||
});
|
||||
|
||||
it("sends and receives a payload containing plain text and binary packets (binary)", async () => {
|
||||
const sid = await initLongPollingSession(true);
|
||||
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "6:4hello10:b4AQIDBA==",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(200);
|
||||
|
||||
const postContent = await pushResponse.text();
|
||||
|
||||
expect(postContent).to.eql("ok");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const buffer = await pollResponse.arrayBuffer();
|
||||
|
||||
// 0 => string
|
||||
// 6 => byte length
|
||||
// 255 => delimiter
|
||||
// 52 => 4 (MESSAGE packet type)
|
||||
// 104 101 108 108 111 => "hello"
|
||||
// 1 => binary
|
||||
// 5 => byte length
|
||||
// 255 => delimiter
|
||||
// 4 => 4 (MESSAGE packet type)
|
||||
// 1 2 3 4 => binary message
|
||||
expect(buffer).to.eql(Uint8Array.from([0, 6, 255, 52, 104, 101, 108, 108, 111, 1, 5, 255, 4, 1, 2, 3, 4]).buffer);
|
||||
});
|
||||
|
||||
it("closes the session upon invalid packet format", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
try {
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "abc",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(400);
|
||||
} catch (e) {
|
||||
// node-fetch throws when the request is closed abnormally
|
||||
}
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(400);
|
||||
});
|
||||
|
||||
// FIXME CORS error
|
||||
it.skip("closes the session upon duplicate poll requests", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const pollResponses = await Promise.all([
|
||||
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`),
|
||||
sleep(5).then(() => fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}&t=burst`)),
|
||||
]);
|
||||
|
||||
expect(pollResponses[0].status).to.eql(200);
|
||||
|
||||
const content = await pollResponses[0].text();
|
||||
|
||||
expect(content).to.eql("1:1");
|
||||
|
||||
// the Node.js implementation uses HTTP 500 (Internal Server Error), but HTTP 400 seems more suitable
|
||||
expect(pollResponses[1].status).to.be.oneOf([400, 500]);
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket", () => {
|
||||
it("sends and receives a plain text packet", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
await waitFor(socket, "open");
|
||||
|
||||
await waitFor(socket, "message"); // handshake
|
||||
|
||||
socket.send("4hello");
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql("4hello");
|
||||
|
||||
socket.close();
|
||||
});
|
||||
|
||||
it("sends and receives a binary packet", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
socket.binaryType = "arraybuffer";
|
||||
|
||||
await waitFor(socket, "message"); // handshake
|
||||
|
||||
socket.send(Uint8Array.from([4, 1, 2, 3, 4]));
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql(Uint8Array.from([4, 1, 2, 3, 4]).buffer);
|
||||
|
||||
socket.close();
|
||||
});
|
||||
|
||||
it("closes the session upon invalid packet format", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
await waitFor(socket, "message"); // handshake
|
||||
|
||||
socket.send("abc");
|
||||
|
||||
await waitFor(socket, "close");
|
||||
|
||||
socket.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("heartbeat", function () {
|
||||
this.timeout(5000);
|
||||
|
||||
describe("HTTP long-polling", () => {
|
||||
it("sends ping/pong packets", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "1:2",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(200);
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pollContent = await pollResponse.text();
|
||||
|
||||
expect(pollContent).to.eql("1:3");
|
||||
}
|
||||
});
|
||||
|
||||
it("closes the session upon ping timeout", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
await sleep(PING_INTERVAL + PING_TIMEOUT);
|
||||
|
||||
const pushResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`,
|
||||
{
|
||||
method: "post",
|
||||
body: "1:2",
|
||||
}
|
||||
);
|
||||
|
||||
expect(pushResponse.status).to.eql(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket", () => {
|
||||
it("sends ping/pong packets", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
const x = await waitFor(socket, "message"); // handshake
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
socket.send("2");
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql("3");
|
||||
}
|
||||
|
||||
socket.close();
|
||||
});
|
||||
|
||||
it("closes the session upon ping timeout", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
await waitFor(socket, "close"); // handshake
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("close", () => {
|
||||
describe("HTTP long-polling", () => {
|
||||
it("forcefully closes the session", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const [pollResponse] = await Promise.all([
|
||||
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`),
|
||||
fetch(`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`, {
|
||||
method: "post",
|
||||
body: "1:1",
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pullContent = await pollResponse.text();
|
||||
|
||||
expect(pullContent).to.eql("1:6");
|
||||
|
||||
const pollResponse2 = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse2.status).to.eql(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket", () => {
|
||||
it("forcefully closes the session", async () => {
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket`
|
||||
);
|
||||
|
||||
await waitFor(socket, "message"); // handshake
|
||||
|
||||
socket.send("1");
|
||||
|
||||
await waitFor(socket, "close");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("upgrade", () => {
|
||||
it("successfully upgrades from HTTP long-polling to WebSocket", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
||||
);
|
||||
|
||||
await waitFor(socket, "open");
|
||||
|
||||
// send probe
|
||||
socket.send("2probe");
|
||||
|
||||
const probeResponse = await waitFor(socket, "message");
|
||||
|
||||
expect(probeResponse.data).to.eql("3probe");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(200);
|
||||
|
||||
const pollContent = await pollResponse.text();
|
||||
|
||||
expect(pollContent).to.eql("1:6"); // "noop" packet to cleanly end the HTTP long-polling request
|
||||
|
||||
// complete upgrade
|
||||
socket.send("5");
|
||||
|
||||
socket.send("4hello");
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql("4hello");
|
||||
});
|
||||
|
||||
it("ignores HTTP requests with same sid after upgrade", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
||||
);
|
||||
|
||||
await waitFor(socket, "open");
|
||||
|
||||
socket.send("2probe");
|
||||
const res = await waitFor(socket, "message");
|
||||
expect(res.data).to.eql("3probe");
|
||||
|
||||
socket.send("5");
|
||||
|
||||
const pollResponse = await fetch(
|
||||
`${URL}/engine.io/?EIO=3&transport=polling&sid=${sid}`
|
||||
);
|
||||
|
||||
expect(pollResponse.status).to.eql(400);
|
||||
|
||||
socket.send("4hello");
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql("4hello");
|
||||
});
|
||||
|
||||
it("ignores WebSocket connection with same sid after upgrade", async () => {
|
||||
const sid = await initLongPollingSession();
|
||||
|
||||
const socket = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
||||
);
|
||||
|
||||
await waitFor(socket, "open");
|
||||
|
||||
socket.send("2probe");
|
||||
const res = await waitFor(socket, "message");
|
||||
expect(res.data).to.eql("3probe");
|
||||
|
||||
socket.send("5");
|
||||
|
||||
const socket2 = new WebSocket(
|
||||
`${WS_URL}/engine.io/?EIO=3&transport=websocket&sid=${sid}`
|
||||
);
|
||||
|
||||
await waitFor(socket2, "close");
|
||||
|
||||
socket.send("4hello");
|
||||
|
||||
const { data } = await waitFor(socket, "message");
|
||||
|
||||
expect(data).to.eql("4hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
545
docs/engine.io-protocol/v3.md
Normal file
545
docs/engine.io-protocol/v3.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Engine.IO Protocol
|
||||
|
||||
This document describes the Engine.IO protocol. For a reference JavaScript
|
||||
implementation, take a look at
|
||||
[engine.io-parser](https://github.com/learnboost/engine.io-parser),
|
||||
[engine.io-client](https://github.com/learnboost/engine.io-client)
|
||||
and [engine.io](https://github.com/learnboost/engine.io).
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- [Revision](#revision)
|
||||
- [Anatomy of an Engine.IO session](#anatomy-of-an-engineio-session)
|
||||
- [Sample session](#sample-session)
|
||||
- [Sample session with WebSocket only](#sample-session-with-websocket-only)
|
||||
- [URLs](#urls)
|
||||
- [Encoding](#encoding)
|
||||
- [Packet](#packet)
|
||||
- [0 open](#0-open)
|
||||
- [1 close](#1-close)
|
||||
- [2 ping](#2-ping)
|
||||
- [3 pong](#3-pong)
|
||||
- [4 message](#4-message)
|
||||
- [5 upgrade](#5-upgrade)
|
||||
- [6 noop](#6-noop)
|
||||
- [Payload](#payload)
|
||||
- [Transports](#transports)
|
||||
- [Polling](#polling)
|
||||
- [XHR](#xhr)
|
||||
- [JSONP](#jsonp)
|
||||
- [WebSocket](#websocket)
|
||||
- [Transport upgrading](#transport-upgrading)
|
||||
- [Timeouts](#timeouts)
|
||||
- [Difference between v2 and v3](#difference-between-v2-and-v3)
|
||||
- [Test suite](#test-suite)
|
||||
|
||||
## Revision
|
||||
|
||||
This is revision **3** of the Engine.IO protocol.
|
||||
|
||||
The revision 2 can be found here: https://github.com/socketio/engine.io-protocol/tree/v2
|
||||
|
||||
## Anatomy of an Engine.IO session
|
||||
|
||||
1. Transport establishes a connection to the Engine.IO URL .
|
||||
2. Server responds with an `open` packet with JSON-encoded handshake data:
|
||||
- `sid` session id (`String`)
|
||||
- `upgrades` possible transport upgrades (`Array` of `String`)
|
||||
- `pingTimeout` server configured ping timeout, used for the client
|
||||
to detect that the server is unresponsive (`Number`)
|
||||
- `pingInterval` server configured ping interval, used for the client
|
||||
to detect that the server is unresponsive (`Number`)
|
||||
3. Server must respond to periodic `ping` packets sent by the client
|
||||
with `pong` packets.
|
||||
4. Client and server can exchange `message` packets at will.
|
||||
5. Polling transports can send a `close` packet to close the socket, since
|
||||
they're expected to be "opening" and "closing" all the time.
|
||||
|
||||
### Sample session
|
||||
|
||||
- Request n°1 (open packet)
|
||||
|
||||
```
|
||||
GET /engine.io/?EIO=3&transport=polling&t=N8hyd6w
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: text/plain; charset=UTF-8
|
||||
96:0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}
|
||||
```
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
96 => number of characters (not bytes)
|
||||
: => separator
|
||||
0 => "open" packet type
|
||||
{"sid":... => the handshake data
|
||||
```
|
||||
|
||||
Note: the `t` query param is used to ensure that the request is not cached by the browser.
|
||||
|
||||
- Request n°2 (message in):
|
||||
|
||||
`socket.send('hey')` is executed on the server:
|
||||
|
||||
```
|
||||
GET /engine.io/?EIO=3&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: text/plain; charset=UTF-8
|
||||
4:4hey
|
||||
```
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
4 => number of characters
|
||||
: => separator
|
||||
4 => "message" packet type
|
||||
hey => the actual message
|
||||
```
|
||||
|
||||
- Request n°3 (message out)
|
||||
|
||||
`socket.send('hello'); socket.send('world');` is executed on the client:
|
||||
|
||||
```
|
||||
POST /engine.io/?EIO=3&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
|
||||
> Content-Type: text/plain; charset=UTF-8
|
||||
6:4hello6:4world
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: text/plain; charset=UTF-8
|
||||
ok
|
||||
```
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
6 => number of characters of the 1st packet
|
||||
: => separator
|
||||
4 => "message" packet type
|
||||
hello => the 1st message
|
||||
6 => number of characters of the 2nd packet
|
||||
: => separator
|
||||
4 => "message" message type
|
||||
world => the 2nd message
|
||||
```
|
||||
|
||||
- Request n°4 (WebSocket upgrade)
|
||||
|
||||
```
|
||||
GET /engine.io/?EIO=3&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
|
||||
< HTTP/1.1 101 Switching Protocols
|
||||
```
|
||||
|
||||
WebSocket frames:
|
||||
|
||||
```
|
||||
< 2probe => probe request
|
||||
> 3probe => probe response
|
||||
> 5 => "upgrade" packet type
|
||||
> 4hello => message (not concatenated)
|
||||
> 4world
|
||||
> 2 => "ping" packet type
|
||||
< 3 => "pong" packet type
|
||||
> 1 => "close" packet type
|
||||
```
|
||||
|
||||
### Sample session with WebSocket only
|
||||
|
||||
In that case, the client only enables WebSocket (without HTTP polling).
|
||||
|
||||
```
|
||||
GET /engine.io/?EIO=3&transport=websocket
|
||||
< HTTP/1.1 101 Switching Protocols
|
||||
```
|
||||
|
||||
WebSocket frames:
|
||||
|
||||
```
|
||||
< 0{"sid":"lv_VI97HAXpY6yYWAAAC","pingInterval":25000,"pingTimeout":5000} => handshake
|
||||
< 4hey
|
||||
> 4hello => message (not concatenated)
|
||||
> 4world
|
||||
< 2 => "ping" packet type
|
||||
> 3 => "pong" packet type
|
||||
> 1 => "close" packet type
|
||||
```
|
||||
|
||||
|
||||
## URLs
|
||||
|
||||
An Engine.IO url is composed as follows:
|
||||
|
||||
```
|
||||
/engine.io/[?<query string>]
|
||||
```
|
||||
|
||||
- The `engine.io` pathname should only be changed by higher-level
|
||||
frameworks whose protocol sits on top of engine's.
|
||||
|
||||
- The query string is optional and has six reserved keys:
|
||||
|
||||
- `transport`: indicates the transport name. Supported ones by default are
|
||||
`polling`, `websocket`.
|
||||
- `j`: if the transport is `polling` but a JSONP response is required, `j`
|
||||
must be set with the JSONP response index.
|
||||
- `sid`: if the client has been given a session id, it must be included
|
||||
in the querystring.
|
||||
- `b64`: if the client doesn't support XHR2, `b64=1` is sent in the query string
|
||||
to signal the server that all binary data should be sent base64 encoded.
|
||||
- `EIO`: the version of the protocol
|
||||
- `t`: a hashed-timestamp used for cache-busting
|
||||
|
||||
*FAQ:* Is the `/engine.io` portion modifiable?
|
||||
|
||||
Provided the server is customized to intercept requests under a different
|
||||
path segment, yes.
|
||||
|
||||
*FAQ:* What determines whether an option is going to be part of the path
|
||||
versus being encoded as part of the query string? In other words, why
|
||||
is the `transport` not part of the URL?
|
||||
|
||||
It's convention that the path segments remain *only* that which allows to
|
||||
disambiguate whether a request should be handled by a given Engine.IO
|
||||
server instance or not. As it stands, it's only the Engine.IO prefix
|
||||
(`/engine.io`) and the resource (`default` by default).
|
||||
|
||||
## Encoding
|
||||
|
||||
There's two distinct types of encodings
|
||||
|
||||
- packet
|
||||
- payload
|
||||
|
||||
### Packet
|
||||
|
||||
An encoded packet can be UTF-8 string or binary data. The packet encoding format for a string is as follows
|
||||
|
||||
```
|
||||
<packet type id>[<data>]
|
||||
```
|
||||
example:
|
||||
```
|
||||
2probe
|
||||
```
|
||||
For binary data the encoding is identical. When sending binary data, the packet
|
||||
type id is sent in the first byte of the binary contents, followed by the
|
||||
actual packet data. Example:
|
||||
|
||||
```
|
||||
4|0|1|2|3|4|5
|
||||
```
|
||||
|
||||
In the above example each byte is separated by a pipe character and shown as an
|
||||
integer. So the above packet is of type message (see below), and contains
|
||||
binary data that corresponds to an array of integers with values 0, 1, 2, 3, 4
|
||||
and 5.
|
||||
|
||||
The packet type id is an integer. The following are the accepted packet
|
||||
types.
|
||||
|
||||
#### 0 open
|
||||
|
||||
Sent from the server when a new transport is opened (recheck)
|
||||
|
||||
#### 1 close
|
||||
|
||||
Request the close of this transport but does not shutdown the connection itself.
|
||||
|
||||
#### 2 ping
|
||||
|
||||
Sent by the client. Server should answer with a pong packet containing the same data
|
||||
|
||||
example
|
||||
1. client sends: ```2probe```
|
||||
2. server sends: ```3probe```
|
||||
|
||||
#### 3 pong
|
||||
|
||||
Sent by the server to respond to ping packets.
|
||||
|
||||
#### 4 message
|
||||
|
||||
actual message, client and server should call their callbacks with the data.
|
||||
|
||||
##### example 1
|
||||
|
||||
1. server sends: ```4HelloWorld```
|
||||
2. client receives and calls callback ```socket.on('message', function (data) { console.log(data); });```
|
||||
|
||||
##### example 2
|
||||
|
||||
1. client sends: ```4HelloWorld```
|
||||
2. server receives and calls callback ```socket.on('message', function (data) { console.log(data); });```
|
||||
|
||||
#### 5 upgrade
|
||||
|
||||
Before engine.io switches a transport, it tests, if server and client can communicate over this transport.
|
||||
If this test succeed, the client sends an upgrade packets which requests the server to flush its cache on
|
||||
the old transport and switch to the new transport.
|
||||
|
||||
#### 6 noop
|
||||
|
||||
A noop packet. Used primarily to force a poll cycle when an incoming websocket connection is received.
|
||||
|
||||
##### example
|
||||
1. client connects through new transport
|
||||
2. client sends ```2probe```
|
||||
3. server receives and sends ```3probe```
|
||||
4. client receives and sends ```5```
|
||||
5. server flushes and closes old transport and switches to new.
|
||||
|
||||
### Payload
|
||||
|
||||
A payload is a series of encoded packets tied together. The payload encoding format is as follows when only strings are sent and XHR2 is not supported:
|
||||
|
||||
```
|
||||
<length1>:<packet1>[<length2>:<packet2>[...]]
|
||||
```
|
||||
* length: length of the packet in __characters__
|
||||
* packet: actual packets as descriped above
|
||||
|
||||
When XHR2 is not supported, the same encoding principle is used also when
|
||||
binary data is sent, but it is sent as base64 encoded strings. For the purposes of decoding, an identifier `b` is
|
||||
put before a packet encoding that contains binary data. A combination of any
|
||||
number of strings and base64 encoded strings can be sent. Here is an example of
|
||||
base 64 encoded messages:
|
||||
|
||||
```
|
||||
<length of base64 representation of the data + 1 (for packet type)>:b<packet1 type><packet1 data in b64>[...]
|
||||
```
|
||||
|
||||
When XHR2 is supported, a similar principle is used, but everything is encoded
|
||||
directly into binary, so that it can be sent as binary over XHR. The format is
|
||||
the following:
|
||||
|
||||
```
|
||||
<0 for string data, 1 for binary data><Any number of numbers between 0 and 9><The number 255><packet1 (first type,
|
||||
then data)>[...]
|
||||
```
|
||||
|
||||
If a combination of UTF-8 strings and binary data is sent, the string values
|
||||
are represented so that each character is written as a character code into a
|
||||
byte.
|
||||
|
||||
The payload is used for transports which do not support framing, as the polling protocol for example.
|
||||
|
||||
- Example without binary:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"data": "hello"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"data": "€"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
is encoded to:
|
||||
|
||||
```
|
||||
6:4hello2:4€
|
||||
```
|
||||
|
||||
Please note that we are not counting bytes, but characters, hence 2 (1 + 1) instead of 4 (1 + 3).
|
||||
|
||||
- Example with binary (both the client and the transport support binary):
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"data": "€"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"data": buffer <01 02 03 04>
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
is encoded to:
|
||||
|
||||
```
|
||||
buffer <00 04 ff 34 e2 82 ac 01 04 ff 01 02 03 04>
|
||||
|
||||
with:
|
||||
|
||||
00 => string header
|
||||
04 => string length in bytes
|
||||
ff => separator
|
||||
34 => "message" packet type ("4")
|
||||
e2 82 ac => "€"
|
||||
01 => binary header
|
||||
04 => buffer length in bytes
|
||||
ff => separator
|
||||
01 02 03 04 => buffer content
|
||||
```
|
||||
|
||||
- Example with binary (either the client or the transport does not support binary):
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"type": "message",
|
||||
"data": "€"
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"data": buffer <01 02 03 04>
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
is encoded to:
|
||||
|
||||
```
|
||||
2:4€10:b4AQIDBA==
|
||||
|
||||
with
|
||||
|
||||
2 => number of characters of the 1st packet
|
||||
: => separator
|
||||
4 => "message" packet type
|
||||
€
|
||||
10 => number of characters of the 2nd packet
|
||||
: => separator
|
||||
b => indicates a base64 packet
|
||||
4 => "message" packet type
|
||||
AQIDBA== => buffer content encoded in base64
|
||||
```
|
||||
|
||||
## Transports
|
||||
|
||||
An engine.io server must support three transports:
|
||||
|
||||
- websocket
|
||||
- polling
|
||||
- jsonp
|
||||
- xhr
|
||||
|
||||
### Polling
|
||||
|
||||
The polling transport consists of recurring GET requests by the client
|
||||
to the server to get data, and POST requests with payloads from the
|
||||
client to the server to send data.
|
||||
|
||||
#### XHR
|
||||
|
||||
The server must support CORS responses.
|
||||
|
||||
#### JSONP
|
||||
|
||||
The server implementation must respond with valid JavaScript. The URL
|
||||
contains a query string parameter `j` that must be used in the response.
|
||||
`j` is an integer.
|
||||
|
||||
The format of a JSONP packet.
|
||||
|
||||
```
|
||||
`___eio[` <j> `]("` <encoded payload> `");`
|
||||
```
|
||||
|
||||
To ensure that the payload gets processed correctly, it must be escaped
|
||||
in such a way that the response is still valid JavaScript. Passing the
|
||||
encoded payload through a JSON encoder is a good way to escape it.
|
||||
|
||||
Example JSONP frame returned by the server:
|
||||
|
||||
```
|
||||
___eio[4]("packet data");
|
||||
```
|
||||
|
||||
##### Posting data
|
||||
|
||||
The client posts data through a hidden iframe. The data gets to the server
|
||||
in the URI encoded format as follows:
|
||||
|
||||
```
|
||||
d=<escaped packet payload>
|
||||
```
|
||||
|
||||
In addition to the regular qs escaping, in order to prevent
|
||||
inconsistencies with `\n` handling by browsers, `\n` gets escaped as `\\n`
|
||||
prior to being POSTd.
|
||||
|
||||
### WebSocket
|
||||
|
||||
Encoding payloads _should not_ be used for WebSocket, as the protocol
|
||||
already has a lightweight framing mechanism.
|
||||
|
||||
In order to send a payload of messages, encode packets individually
|
||||
and `send()` them in succession.
|
||||
|
||||
## Transport upgrading
|
||||
|
||||
A connection always starts with polling (either XHR or JSONP). WebSocket
|
||||
gets tested on the side by sending a probe. If the probe is responded
|
||||
from the server, an upgrade packet is sent.
|
||||
|
||||
To ensure no messages are lost, the upgrade packet will only be sent
|
||||
once all the buffers of the existing transport are flushed and the
|
||||
transport is considered _paused_.
|
||||
|
||||
When the server receives the upgrade packet, it must assume this is the
|
||||
new transport channel and send all existing buffers (if any) to it.
|
||||
|
||||
The probe sent by the client is a `ping` packet with `probe` sent as data.
|
||||
The probe sent by the server is a `pong` packet with `probe` sent as data.
|
||||
|
||||
Moving forward, upgrades other than just `polling -> x` are being considered.
|
||||
|
||||
## Timeouts
|
||||
|
||||
The client must use the `pingTimeout` and the `pingInterval` sent as part
|
||||
of the handshake (with the `open` packet) to determine whether the server
|
||||
is unresponsive.
|
||||
|
||||
The client sends a `ping` packet. If no packet type is received within
|
||||
`pingTimeout`, the client considers the socket disconnected. If a `pong`
|
||||
packet is actually received, the client will wait `pingInterval` before
|
||||
sending a `ping` packet again.
|
||||
|
||||
Since the two values are shared between the server and the client, the server
|
||||
will also be able to detect whether the client becomes unresponsive when it
|
||||
does not receive any data within `pingTimeout + pingInterval`.
|
||||
|
||||
## Difference between v2 and v3
|
||||
|
||||
- add support for binary data
|
||||
|
||||
v2 is included in Socket.IO v0.9, while v3 is included in Socket.IO v1/v2.
|
||||
|
||||
## Test suite
|
||||
|
||||
The test suite in the `test-suite/` directory lets you check the compliance of a server implementation.
|
||||
|
||||
Usage:
|
||||
|
||||
- in Node.js: `npm ci && npm test`
|
||||
- in a browser: simply open the `index.html` file in your browser
|
||||
|
||||
For reference, here is expected configuration for the JavaScript server to pass all tests:
|
||||
|
||||
```js
|
||||
import { listen } from "engine.io";
|
||||
|
||||
const server = listen(3000, {
|
||||
pingInterval: 300,
|
||||
pingTimeout: 200,
|
||||
allowEIO3: true,
|
||||
maxPayload: 1e6,
|
||||
cors: {
|
||||
origin: "*"
|
||||
}
|
||||
});
|
||||
|
||||
server.on("connection", socket => {
|
||||
socket.on("data", (...args) => {
|
||||
socket.send(...args);
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user