PageSourceSearch

https://socket.io/assets/js/e12fe05d.1fc528e2.js

js socket.io collected 2026-09-24 07:25:42 UTC 29,439 bytes, 1 lines download raw bytes

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.