mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-09 15:08:12 -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