1"use strict";(self.webpackChunksample_website=self.webpackChunksample_website||[]).push([[4999],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>h});var a=n(7294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function i(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?o(Object(n),!0).forEach((function(t){r(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):o(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function l(e,t){if(null==e)return{};var n,a,r=function(e,t){if(null==e)return{};var n,a,r={},o=Object.keys(e);for(a=0;a<o.length;a++)n=o[a],t.indexOf(n)>=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a<o.length;a++)n=o[a],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var s=a.createContext({}),c=function(e){var t=a.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},p=function(e){var t=c(e.components);return a.createElement(s.Provider,{value:t},e.children)},k="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),k=c(n),u=r,h=k["".concat(s,".").concat(u)]||k[u]||d[u]||o;return n?a.createElement(h,i(i({ref:t},p),{},{components:n})):a.createElement(h,i({ref:t},p))}));function h(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=n.length,i=new Array(o);i[0]=u;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[k]="string"==typeof e?e:r,i[1]=l;for(var c=2;c<o;c++)i[c]=n[c];return a.createElement.apply(null,i)}return a.createElement.apply(null,n)}u.displayName="MDXCreateElement"},4754:(e,t,n)=>{n.r(t),n.d(t,{contentTitle:()=>i,default:()=>k,frontMatter:()=>o,metadata:()=>l,toc:()=>s});var a=n(3117),r=(n(7294),n(3905));const o={title:"How to build a basic Socket.IO client"},i="How to build a basic Socket.IO client",l={type:"mdx",permalink:"/how-to/build-a-basic-client",source:"@site/src/pages/how-to/build-a-basic-client.md",title:"How to build a basic Socket.IO client",description:"In this guide, we will implement a basic Socket.IO client in JavaScript, in order to get a better understanding of the Socket.IO protocol.",frontMatter:{title:"How to build a basic Socket.IO client"}},s=[{value:"Event emitter",id:"event-emitter",level:2},{value:"WebSocket connection",id:"websocket-connection",level:2},{value:"The Engine.IO protocol",id:"the-engineio-protocol",level:2},{value:"Heartbeat",id:"heartbeat",level:3},{value:"Reconnection",id:"reconnection",level:3},{value:"The Socket.IO protocol",id:"the-socketio-protocol",level:2},{value:"Connecting",id:"connecting",level:3},{value:"Sending an event",id:"sending-an-event",level:3},{value:"Receiving an event",id:"receiving-an-event",level:3},{value:"Disconnecting manually",id:"disconnecting-manually",level:3},{value:"Ending notes",id:"ending-notes",level:2}],c={toc:s},p="wrapper";function k(e){let{components:t,...n}=e;return(0,r.kt)(p,(0,a.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"how-to-build-a-basic-socketio-client"},"How to build a basic Socket.IO client"),(0,r.kt)("p",null,"In this guide, we will implement a basic Socket.IO client in JavaScript, in order to get a better understanding of the Socket.IO protocol."),(0,r.kt)("p",null,"We will implement the following features:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"creating a WebSocket connection"),(0,r.kt)("li",{parentName:"ul"},"managing reconnections"),(0,r.kt)("li",{parentName:"ul"},"sending events"),(0,r.kt)("li",{parentName:"ul"},"receiving events"),(0,r.kt)("li",{parentName:"ul"},"disconnecting manually")),(0,r.kt)("p",null,"The official client obviously contains a lot more features:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/client-installation/#browser-support"},"support for old browsers, down to IE9")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/how-it-works/#upgrade-mechanism"},"fallback to HTTP long-polling")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/emitting-events/#acknowledgements"},"acknowledgements")),(0,r.kt)("li",{parentName:"ul"},"binary payloads"),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/namespaces/"},"multiplexing")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/listening-to-events/#catch-all-listeners"},"catch-all listeners")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/connection-state-recovery"},"connection state recovery")),(0,r.kt)("li",{parentName:"ul"},"...")),(0,r.kt)("p",null,"But that should be sufficient to give you a good overview of how the library works under the hood."),(0,r.kt)("p",null,"Our goal is to achieve something like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'import { io } from "./basic-client.js";\n\nconst socket = io();\n\n// connection\nsocket.on("connect", () => {\n // ...\n});\n\n// receiving an event\nsocket.on("foo", (value) => {\n // ...\n});\n\n// sending an event\nsocket.emit("bar", "abc");\n')),(0,r.kt)("p",null,"Ready? Let's do this!"),(0,r.kt)("h2",{id:"event-emitter"},"Event emitter"),(0,r.kt)("p",null,"The Socket.IO API is heavily inspired from the Node.js ",(0,r.kt)("a",{parentName:"p",href:"https://nodejs.org/api/events.html"},"EventEmitter")," class."),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'import { EventEmitter } from "node:events";\n\nconst myEmitter = new EventEmitter();\n\nmyEmitter.on("foo", () => {\n console.log("foo!");\n});\n\nmyEmitter.emit("foo");\n')),(0,r.kt)("p",null,"The library provides a similar API, but between a server and a client:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"server")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'io.on("connection", (socket) => {\n // send a "foo" event to the client\n socket.emit("foo");\n\n // receive a "bar" event from the client\n socket.on("bar", () => {\n // ...\n });\n});\n')),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"client")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'import { io } from "socket.io-client";\n\nconst socket = io();\n\n// receive a "foo" event from the server\nsocket.on("foo", () => {\n // ...\n});\n\n// send a "bar" event to the server\nsocket.emit("bar");\n')),(0,r.kt)("p",null,"The underlying connection between the server and the client (",(0,r.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API"},"WebSocket")," or HTTP long-polling) is abstracted away and managed by the library."),(0,r.kt)("p",null,"Let's create a minimalistic ",(0,r.kt)("inlineCode",{parentName:"p"},"EventEmitter")," class:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"class EventEmitter {\n #listeners = new Map();\n\n on(event, listener) {\n let listeners = this.#listeners.get(event);\n if (!listeners) {\n this.#listeners.set(event, listeners = []);\n }\n listeners.push(listener);\n }
1\n\n emit(event, ...args) {\n const listeners = this.#listeners.get(event);\n if (listeners) {\n for (const listener of listeners) {\n listener.apply(null, args);\n }\n }\n }\n}\n")),(0,r.kt)("p",null,"Our ",(0,r.kt)("inlineCode",{parentName:"p"},"Socket")," class will then extend this class, in order to expose both the ",(0,r.kt)("inlineCode",{parentName:"p"},"on()")," and the ",(0,r.kt)("inlineCode",{parentName:"p"},"emit()")," methods:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"class Socket extends EventEmitter {\n constructor(uri, opts) {\n super();\n }\n}\n")),(0,r.kt)("p",null,"In our constructor, the ",(0,r.kt)("inlineCode",{parentName:"p"},"uri")," argument is either:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"provided by the user:")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'const socket = io("https://example.com");\n')),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"or inferred from the ",(0,r.kt)("a",{parentName:"li",href:"https://developer.mozilla.org/en-US/docs/Web/API/Location"},(0,r.kt)("inlineCode",{parentName:"a"},"window.location"))," object")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"const socket = io();\n")),(0,r.kt)("p",null,"Let's create an entrypoint:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},'export function io(uri, opts) {\n if (typeof uri !== "string") {\n opts = uri;\n uri = location.origin;\n }\n return new Socket(uri, opts);\n}\n')),(0,r.kt)("p",null,"OK, so that's a good start!"),(0,r.kt)("h2",{id:"websocket-connection"},"WebSocket connection"),(0,r.kt)("p",null,"Now, let's create the WebSocket connection to the server:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n+ #uri;\n+ #opts;\n+ #ws;\n\n constructor(uri, opts) {\n super();\n+ this.#uri = uri;\n+ this.#opts = Object.assign({\n+ path: "/socket.io/"\n+ }, opts);\n+ this.#open();\n }\n\n+ #open() {\n+ this.#ws = new WebSocket(this.#createUrl());\n+ }\n+\n+ #createUrl() {\n+ const uri = this.#uri.replace(/^http/, "ws");\n+ const queryParams = "?EIO=4&transport=websocket";\n+ return `${uri}${this.#opts.path}${queryParams}`;\n+ }\n}\n')),(0,r.kt)("p",null,"Reference: ",(0,r.kt)("a",{parentName:"p",href:"https://developer.mozilla.org/en-US/docs/Web/API/WebSocket"},"https://developer.mozilla.org/en-US/docs/Web/API/WebSocket")),(0,r.kt)("p",null,"Some explanations about the ",(0,r.kt)("inlineCode",{parentName:"p"},"createUrl()")," method:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"a WebSocket URL starts with ",(0,r.kt)("inlineCode",{parentName:"li"},"ws://")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"wss://"),", so we handle this in the ",(0,r.kt)("inlineCode",{parentName:"li"},"replace()")," call"),(0,r.kt)("li",{parentName:"ul"},"a Socket.IO URL always contains a specific request path, which defaults to ",(0,r.kt)("inlineCode",{parentName:"li"},"/socket.io/")),(0,r.kt)("li",{parentName:"ul"},"there are two mandatory query parameters:",(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"EIO=4"),": the version of the Engine.IO protocol"),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"transport=websocket"),": the transport used")))),(0,r.kt)("p",null,"So the final URL will look like: ",(0,r.kt)("inlineCode",{parentName:"p"},"wss://example.com/socket.io/?EIO=4&transport=websocket")),(0,r.kt)("h2",{id:"the-engineio-protocol"},"The Engine.IO protocol"),(0,r.kt)("p",null,"The Socket.IO codebase is split into two distinct layers:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"the low-level plumbing: what we call Engine.IO, the engine inside Socket.IO"),(0,r.kt)("li",{parentName:"ul"},"the high-level API: Socket.IO itself")),(0,r.kt)("p",null,"See also:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/how-it-works/"},"How it works")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"/docs/v4/engine-io-protocol"},"The Engine.IO protocol"))),(0,r.kt)("p",null,"When using WebSocket, the format of the messages sent over the wire is simply: ",(0,r.kt)("inlineCode",{parentName:"p"},"<packet type><payload>")),(0,r.kt)("p",null,"Here are the different packet types in the 4th version (hence the ",(0,r.kt)("inlineCode",{parentName:"p"},"EIO=4")," above) of the protocol:"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:null},"Name"),(0,r.kt)("th",{parentName:"tr",align:"center"},"Representation"),(0,r.kt)("th",{parentName:"tr",align:null},"Description"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"OPEN"),(0,r.kt)("td",{parentName:"tr",align:"center"},"0"),(0,r.kt)("td",{parentName:"tr",align:null},"Used during the handshake.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"CLOSE"),(0,r.kt)("td",{parentName:"tr",align:"center"},"1"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to indicate that a transport can be closed.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"PING"),(0,r.kt)("td",{parentName:"tr",align:"center"},"2"),(0,r.kt)("td",{parentName:"tr",align:null},"Used in the heartbeat mechanism.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"PONG"),(0,r.kt)("td",{parentName:"tr",align:"center"},"3"),(0,r.kt)("td",{parentName:"tr",align:null},"Used in the heartbeat mechanism.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"MESSAGE"),(0,r.kt)("td",{parentName:"tr",align:"center"},"4"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to send a payload to the other side.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"UPGRADE"),(0,r.kt)("td",{parentName:"tr",align:"center"},"5"),(0,r.kt)("td",{parentName:"tr",align:null},"Used during the upgrade process (not used here).")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"NOOP"),(0,r.kt)("td",{parentName:"tr",align:"center"},"6"),(0,r.kt)("td",{parentName:"tr",align:null},"Used during the upgrade process (not used here).")))),(0,r.kt)("p",null,"Example:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre"},"4hello\n\nwith:\n\n4 => MESSAGE packet type\nhello => message payload (UTF-8 encoded)\n")),(0,r.kt)("p",null,"Let's handle the WebSocket messages:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}
1,'+const EIOPacketType = {\n+ OPEN: "0",\n+ CLOSE: "1",\n+ PING: "2",\n+ PONG: "3",\n+ MESSAGE: "4",\n+};\n\n+function noop() {}\n\nclass Socket extends EventEmitter {\n [...]\n\n #open() {\n this.#ws = new WebSocket(this.#createUrl());\n+ this.#ws.onmessage = ({ data }) => this.#onMessage(data);\n+ this.#ws.onclose = () => this.#onClose("transport close");\n }\n\n+ #onMessage(data) {\n+ if (typeof data !== "string") {\n+ // TODO handle binary payloads\n+ return;\n+ }\n+\n+ switch (data[0]) {\n+ case EIOPacketType.CLOSE:\n+ this.#onClose("transport close");\n+ break;\n+\n+ default:\n+ this.#onClose("parse error");\n+ break;\n+ }\n+ }\n+\n+ #onClose(reason) {\n+ if (this.#ws) {\n+ this.#ws.onclose = noop;\n+ this.#ws.close();\n+ }\n+ }\n+}\n')),(0,r.kt)("h3",{id:"heartbeat"},"Heartbeat"),(0,r.kt)("p",null,"A heartbeat mechanism is implemented to ensure that the connection between the server and the client is healthy."),(0,r.kt)("p",null,"The server sends two values during the initial handshake: ",(0,r.kt)("inlineCode",{parentName:"p"},"pingInterval")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"pingTimeout")),(0,r.kt)("p",null,"It will then send a PING packet every ",(0,r.kt)("inlineCode",{parentName:"p"},"pingInterval")," ms, and expect a PONG packet back from the client. Let's do this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n+ #pingTimeoutTimer;\n+ #pingTimeoutDelay;\n\n [...]\n\n #onMessage(data) {\n if (typeof data !== "string") {\n // TODO handle binary payloads\n return;\n }\n\n switch (data[0]) {\n+ case EIOPacketType.OPEN:\n+ this.#onOpen(data);\n+ break;\n+\n case EIOPacketType.CLOSE:\n this.#onClose("transport close");\n break;\n\n+ case EIOPacketType.PING:\n+ this.#resetPingTimeout();\n+ this.#send(EIOPacketType.PONG);\n+ break;\n\n default:\n this.#onClose("parse error");\n break;\n }\n }\n\n+ #onOpen(data) {\n+ let handshake;\n+ try {\n+ handshake = JSON.parse(data.substring(1));\n+ } catch (e) {\n+ return this.#onClose("parse error");\n+ }\n+ this.#pingTimeoutDelay = handshake.pingInterval + handshake.pingTimeout;\n+ this.#resetPingTimeout();\n+ }\n+\n+ #resetPingTimeout() {\n+ clearTimeout(this.#pingTimeoutTimer);\n+ this.#pingTimeoutTimer = setTimeout(() => {\n+ this.#onClose("ping timeout");\n+ }, this.#pingTimeoutDelay);\n+ }\n+\n+ #send(data) {\n+ if (this.#ws.readyState === WebSocket.OPEN) {\n+ this.#ws.send(data);\n+ }\n+ }\n\n #onClose(reason) {\n if (this.#ws) {\n this.#ws.onclose = noop;\n this.#ws.close();\n }\n\n+ clearTimeout(this.#pingTimeoutTimer);\n }\n}\n')),(0,r.kt)("h3",{id:"reconnection"},"Reconnection"),(0,r.kt)("p",null,"While we're at it, we will also handle reconnections. WebSockets are awesome, but they can (and they will, in real-life conditions) get disconnected, so we must take care of that:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n [...]\n\n constructor(uri, opts) {\n super();\n this.#uri = uri;\n this.#opts = Object.assign(\n {\n path: "/socket.io/",\n+ reconnectionDelay: 2000,\n },\n opts\n );\n this.#open();\n }\n\n #onClose(reason) {\n if (this.#ws) {\n this.#ws.onclose = noop;\n this.#ws.close();\n }\n\n clearTimeout(this.#pingTimeoutTimer);\n\n+ setTimeout(() => this.#open(), this.#opts.reconnectionDelay);\n }\n}\n')),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"The official Socket.IO client uses a fancy exponential delay with some randomness in order to prevent spikes of load when a lot of clients reconnect at the same time, but we'll keep it simple here and use a constant value.")),(0,r.kt)("p",null,"OK, so let's sum up, we now have a client that can:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"open a WebSocket connection to the server"),(0,r.kt)("li",{parentName:"ul"},"honor the heartbeat mechanism by responding to PING packets"),(0,r.kt)("li",{parentName:"ul"},"automatically reconnect upon failure")),(0,r.kt)("p",null,"That's it for the Engine.IO protocol! Let's dig into the Socket.IO protocol now."),(0,r.kt)("h2",{id:"the-socketio-protocol"},"The Socket.IO protocol"),(0,r.kt)("p",null,"The Socket.IO protocol is built on top of the Engine.IO protocol described ",(0,r.kt)("a",{parentName:"p",href:"#the-engineio-protocol"},"earlier"),', which means that every Socket.IO packet will be prefixed by "4" (the Engine.IO MESSAGE packet type) when sent over the wire.'),(0,r.kt)("p",null,"Reference: ",(0,r.kt)("a",{parentName:"p",href:"/docs/v4/socket-io-protocol"},"the Socket.IO protocol")),(0,r.kt)("p",null,"Without binary elements, the format is the following:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre"},"<packet type>[JSON-stringified payload]\n")),(0,r.kt)("p",null,"Here is the list of available packet types:"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:null},"Type"),(0,r.kt)("th",{parentName:"tr",align:null},"ID"),(0,r.kt)("th",{parentName:"tr",align:null},"Usage"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"CONNECT"),(0,r.kt)("td",{parentName:"tr",align:null},"0"),(0,r.kt)("td",{parentName:"tr",align:null},"Used during the connection to a namespace.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"DISCONNECT"),(0,r.kt)("td",{parentName:"tr",align:null},"1"),(0,r.kt)("td",{parentName:"tr",align:null},"Used when disconnecting from a namespace.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"EVENT"),(0,r.kt)("td",{parentName:"tr",align:null},"2"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to send data to the other side.")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"ACK"),(0,r.kt)("td",{parentName:"tr",align:null},"3"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to acknowledge an event (not used here).")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"CONNECT_ERROR"),(0,r.kt)("td",{parentName:"tr",align:null},"4"),(0,r.kt)("td",{parentName:"tr",align:null},"Used during the connection to a namespace (not used here).")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"BINARY_EVENT"),(0,r.kt)("td",{parentName:"tr",align:null},"5"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to send binary data to the other side (not used here).")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"BINARY_ACK"),(0,r.kt)("td",{parentName:"tr",align:null},"6"),(0,r.kt)("td",{parentName:"tr",align:null},"Used to acknowledge an event (the response includes binary data) (not used here).")))),(0,r.kt)("p",null,"Example:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre"},'2["hello","world"]\n\nwith:\n\n2 => EVENT packet type\n["hello","world"] => JSON.stringified() payload\n')),(0,r.kt)("h3",{id:"connecting"},"Connecting"),(0,r.kt)("p",null,"The client must send a CONNECT packet at the beginning of the Socket.IO session:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}
1,'+const SIOPacketType = {\n+ CONNECT: 0,\n+ DISCONNECT: 1,\n+ EVENT: 2,\n+};\n\nclass Socket extends EventEmitter {\n [...]\n\n #onOpen(data) {\n let handshake;\n try {\n handshake = JSON.parse(data.substring(1));\n } catch (e) {\n return this.#onClose("parse error");\n }\n this.#pingTimeoutDelay = handshake.pingInterval + handshake.pingTimeout;\n this.#resetPingTimeout();\n+ this.#doConnect();\n }\n\n+ #doConnect() {\n+ this.#sendPacket({ type: SIOPacketType.CONNECT });\n+ }\n+\n+ #sendPacket(packet) {\n+ this.#send(EIOPacketType.MESSAGE + encode(packet));\n+ }\n}\n\n+function encode(packet) {\n+ let output = "" + packet.type;\n+\n+ return output;\n+}\n')),(0,r.kt)("p",null,"If the connection is allowed, then the server will send a CONNECT packet back:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n+ id;\n\n [...]\n\n #onMessage(data) {\n switch (data[0]) {\n [...]\n\n+ case EIOPacketType.MESSAGE:\n+ let packet;\n+ try {\n+ packet = decode(data);\n+ } catch (e) {\n+ return this.#onClose("parse error");\n+ }\n+ this.#onPacket(packet);\n+ break;\n }\n }\n\n+ #onPacket(packet) {\n+ switch (packet.type) {\n+ case SIOPacketType.CONNECT:\n+ this.#onConnect(packet);\n+ break;\n+ }\n+ }\n\n+ #onConnect(packet) {\n+ this.id = packet.data.sid;\n+\n+ super.emit("connect");\n+ }\n}\n\n+function decode(data) {\n+ let i = 1; // skip "4" prefix\n+\n+ const packet = {\n+ type: parseInt(data.charAt(i++), 10),\n+ };\n+\n+ if (!isPacketValid(packet)) {\n+ throw new Error("invalid format");\n+ }\n+\n+ return packet;\n+}\n+\n+function isPacketValid(packet) {\n+ switch (packet.type) {\n+ case SIOPacketType.CONNECT:\n+ return typeof packet.data === "object";\n+ default:\n+ return false;\n+ }\n+}\n')),(0,r.kt)("admonition",{type:"note"},(0,r.kt)("p",{parentName:"admonition"},"We are using ",(0,r.kt)("inlineCode",{parentName:"p"},"super.emit(...)")," so that we will be able to override the ",(0,r.kt)("inlineCode",{parentName:"p"},"emit()")," method later to send an event.")),(0,r.kt)("h3",{id:"sending-an-event"},"Sending an event"),(0,r.kt)("p",null,"Let's send some data to the server. We need to track the state of the underlying connection and buffer the packets until the connection is ready:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n+ connected = false;\n\n+ #sendBuffer = [];\n\n [...]\n\n+ emit(...args) {\n+ const packet = {\n+ type: SIOPacketType.EVENT,\n+ data: args,\n+ };\n+\n+ if (this.connected) {\n+ this.#sendPacket(packet);\n+ } else {\n+ this.#sendBuffer.push(packet);\n+ }\n+ }\n\n #onConnect(packet) {\n this.id = packet.data.sid;\n+ this.connected = true;\n\n+ this.#sendBuffer.forEach((packet) => this.#sendPacket(packet));\n+ this.#sendBuffer.slice(0);\n\n super.emit("connect");\n }\n}\n\nfunction encode(packet) {\n let output = "" + packet.type;\n\n+ if (packet.data) {\n+ output += JSON.stringify(packet.data);\n+ }\n\n return output;\n}\n')),(0,r.kt)("h3",{id:"receiving-an-event"},"Receiving an event"),(0,r.kt)("p",null,"Conversely, let's handle the EVENT packets sent by the server:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"},'class Socket extends EventEmitter {\n [...]\n\n #onPacket(packet) {\n switch (packet.type) {\n case SIOPacketType.CONNECT:\n this.#onConnect(packet);\n break;\n\n+ case SIOPacketType.EVENT:\n+ super.emit.apply(this, packet.data);\n+ break;\n }\n }\n}\n\nfunction decode(data) {\n let i = 1; // skip "4" prefix\n\n const packet = {\n type: parseInt(data.charAt(i++), 10),\n };\n\n+ if (data.charAt(i)) {\n+ packet.data = JSON.parse(data.substring(i));\n+ }\n\n if (!isPacketValid(packet)) {\n throw new Error("invalid format");\n }\n\n return packet;\n}\n\nfunction isPacketValid(packet) {\n switch (packet.type) {\n case SIOPacketType.CONNECT:\n return typeof packet.data === "object";\n+ case SIOPacketType.EVENT: {\n+ const args = packet.data;\n+ return (\n+ Array.isArray(args) && args.length > 0 && typeof args[0] === "string"\n+ );\n+ }\n default:\n return false;\n }\n}\n')),(0,r.kt)("h3",{id:"disconnecting-manually"},"Disconnecting manually"),(0,r.kt)("p",null,"And finally, let's handle the few cases where the socket shouldn't try to reconnect:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"when the client calls ",(0,r.kt)("inlineCode",{parentName:"li"},"socket.disconnect()")),(0,r.kt)("li",{parentName:"ul"},"when the server calls ",(0,r.kt)("inlineCode",{parentName:"li"},"socket.disconnect()"))),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-diff"}
1,'class Socket extends EventEmitter {\n+ #reconnectTimer;\n+ #shouldReconnect = true;\n\n [...]\n\n #onPacket(packet) {\n switch (packet.type) {\n case SIOPacketType.CONNECT:\n this.#onConnect(packet);\n break;\n\n+ case SIOPacketType.DISCONNECT:\n+ this.#shouldReconnect = false;\n+ this.#onClose("io server disconnect");\n+ break;\n\n case SIOPacketType.EVENT:\n super.emit.apply(this, packet.data);\n break;\n }\n }\n\n #onClose(reason) {\n if (this.#ws) {\n this.#ws.onclose = noop;\n this.#ws.close();\n }\n\n clearTimeout(this.#pingTimeoutTimer);\n+ clearTimeout(this.#reconnectTimer);\n+\n+ if (this.#shouldReconnect) {\n+ this.#reconnectTimer = setTimeout(\n+ () => this.#open(),\n+ this.#opts.reconnectionDelay\n+ );\n+ }\n- setTimeout(() => this.#open(), this.#opts.reconnectionDelay);\n }\n\n+ disconnect() {\n+ this.#shouldReconnect = false;\n+ this.#onClose("io client disconnect");\n+ }\n}\n\nfunction isPacketValid(packet) {\n switch (packet.type) {\n case SIOPacketType.CONNECT:\n return typeof packet.data === "object";\n+ case SIOPacketType.DISCONNECT:\n+ return packet.data === undefined;\n case SIOPacketType.EVENT: {\n const args = packet.data;\n return (\n Array.isArray(args) && args.length > 0 && typeof args[0] === "string"\n );\n }\n default:\n return false;\n }\n}\n')),(0,r.kt)("h2",{id:"ending-notes"},"Ending notes"),(0,r.kt)("p",null,"That's it for our basic Socket.IO client! So let's recap."),(0,r.kt)("p",null,"We have implemented the following features:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"creating a WebSocket connection"),(0,r.kt)("li",{parentName:"ul"},"managing reconnections"),(0,r.kt)("li",{parentName:"ul"},"sending events"),(0,r.kt)("li",{parentName:"ul"},"receiving events"),(0,r.kt)("li",{parentName:"ul"},"disconnecting manually")),(0,r.kt)("p",null,"Hopefully, you now have a better understanding of how the library works under the hood."),(0,r.kt)("p",null,"The complete source code can be found ",(0,r.kt)("a",{parentName:"p",href:"https://github.com/socketio/socket.io/tree/main/examples/basic-websocket-client"},"there"),"."),(0,r.kt)("p",null,"Thanks for reading!"))}k.isMDXComponent=!0}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.