{"id":16446078,"url":"https://github.com/michaldziuba03/websockets","last_synced_at":"2025-10-03T18:20:28.368Z","repository":{"id":144524418,"uuid":"491282699","full_name":"michaldziuba03/websockets","owner":"michaldziuba03","description":"Websocket protocol implementation for server from scratch.","archived":false,"fork":false,"pushed_at":"2023-01-22T07:28:13.000Z","size":741,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-02T22:54:19.021Z","etag":null,"topics":["protocol","real-time","realtime","websocket","websockets","websockets-server","ws"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michaldziuba03.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-11T21:53:28.000Z","updated_at":"2023-10-05T18:59:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"35a706b7-3b7b-4172-a7b6-365893bd228c","html_url":"https://github.com/michaldziuba03/websockets","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/michaldziuba03/websockets","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaldziuba03%2Fwebsockets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaldziuba03%2Fwebsockets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaldziuba03%2Fwebsockets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaldziuba03%2Fwebsockets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michaldziuba03","download_url":"https://codeload.github.com/michaldziuba03/websockets/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaldziuba03%2Fwebsockets/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260933432,"owners_count":23084956,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["protocol","real-time","realtime","websocket","websockets","websockets-server","ws"],"created_at":"2024-10-11T09:46:21.371Z","updated_at":"2025-10-03T18:20:23.345Z","avatar_url":"https://github.com/michaldziuba03.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Websocket protocol \n#### Simplified websocket protocol implementation for server in TypeScript.\n\nIt is not intended for use in production. Use community approved projects instead.\n\n\u003cimg \n  width=\"620px\" \n  src=\"https://user-images.githubusercontent.com/43048524/168474871-c24d0ead-dac3-4e31-aa78-3d9b9e90f8b6.jpg\"\n  alt=\"hide-pain-harold-computer\" \n/\u003e\n\n## Painful process\nWebsocket protocol was harder to implement than I initially thought. I learned a lot about networking and dealing with bitwise operations. Although my implementation is not perfect, it can handle larger files (like images and videos).\n\n### Two main challenges\nHandling large WS frames is hard because we have to deal with frame fragmentation over TCP data stream. Second major challenge is parsing WS frame.\n\n### Good resources to learn how Websocket works\n1. Wikipedia: https://en.wikipedia.org/wiki/WebSocket\n2. WebSockets Crash Course - Handshake, Use-cases, Pros \u0026 Cons and more: https://youtu.be/2Nt-ZrNP22A\n3. WebSocket Tutorial - How WebSockets Work: https://youtu.be/pNxK8fPKstc\n4. WebSocket RFC: https://www.rfc-editor.org/rfc/rfc6455\n\n\n## How it works?\n\n### First stage - WebSocket handshake over HTTP protocol\nClient sends special HTTP request with special headers:\n```yml\nGET /chat HTTP/1.1\nHost: server.example.com\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\nSec-WebSocket-Protocol: chat, superchat\nSec-WebSocket-Version: 13\nOrigin: http://example.com\n```\n\nServer response:\n```yml\nHTTP/1.1 101 Switching Protocols\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=\nSec-WebSocket-Protocol: chat\n```\n\nAs you can see there are a few WS specific headers like: `Sec-WebSocket-Key`, `Sec-WebSocket-Protocol`, `Sec-WebSocket-Version`. \nWhat is `Sec-WebSocket-Key`? It's an unique key generated by client. Server needs it for generating `Sec-WebSocket-Accept` header.\n\nAccording to official RFC and Wikipedia, `Sec-WebSocket-Accept` value is base64 encoded SHA-1 hash of `Sec-WebSocket-Key` combined with fixed UUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B1`.\n\n```ts\nfunction createWsAcceptKey(wsKey: string): string {\n    const uuid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // constant UUID definied in WS docs\n    const dataToHash = wsKey + uuid\n\n    return createHash('sha1')\n        .update(Buffer.from(dataToHash))\n        .digest('base64');\n}\n```\n\n#### Finalizing WebSocket handshake\nJust send required headers like `Upgrade`, `Connection` and `Sec-WebSocket-Accept`. Remember about HTTP status code `101` (Switching Protocols) and body with extra blank line at the end.\n```ts\nfunction finalizeHandshake(res: ServerResponse, wsAcceptKey: string) {\n    res.statusCode = 101;\n    \n    // set headers:\n    res.setHeader('Upgrade', 'websocket');\n    res.setHeader('Connection', 'Upgrade');\n    res.setHeader('Sec-WebSocket-Accept', wsAcceptKey);\n    \n    res.write('\\r\\n');\n    res.end();\n}\n```\n\n### Second stage - parsing WebSocket frame\n\u003cimg src=\"https://user-images.githubusercontent.com/43048524/168477955-780ff531-b2e3-4746-bc9a-549204d6c8c9.png\" /\u003e\n\nImportant reference: https://www.rfc-editor.org/rfc/rfc6455#section-5.2\n\n\n1 byte = 8 bits\n\n#### Important concepts for parsing frame\n1. What is endianess? (https://www.freecodecamp.org/news/what-is-endianness-big-endian-vs-little-endian/)\n2. Bitwise operators (https://en.wikipedia.org/wiki/Bitwise_operation)\n\n#### Reuse TCP connection socket\n```ts\nconst server = new Server((req, res) =\u003e {\n  ...\n  finalizeHandshake(res, wsAcceptKey);\n   \n  // We have to reuse socket from HTTP request. Now we can operate on TCP level\n  req.socket.on('data', (buff) =\u003e {})\n});\n ```\n\n#### Parsing first byte of frame\n![image](https://user-images.githubusercontent.com/43048524/168479784-566fb245-4e01-4088-a043-0c35fe40c7d8.png)\n\nRead first byte from buffer:\n```ts\nreq.socket.on('data', (buff) =\u003e {\n  let byteOffset = 0;\n  const firstByte = buff.readUint8(byteOffset);\n})\n```\n\n#### How to read bits (1 byte = 8 bits)?\nOur `firstByte` variable is interpreted by Node.js as decimal number - How to get all information from byte? We have to use bitwise operators for operations on bits.\n\n##### How to read n-bit? Example:\n```ts\n...\nconst firstByte = buff.readUint8(byteOffset); // 129 as decimal = 10000001 as binary\n\nconst firstBit = (firstByte \u003e\u003e 7) \u0026 0x1; // 1\nconst secondBit = (firstByte \u003e\u003e 6) \u0026 0x1; // 0\nconst thirdBit = (firstByte \u003e\u003e 5) \u0026 0x1; // 0\n```\n\n##### How to read last n-bits? Example:\n```ts\n...\nconst firstByte = buff.readUint8(byteOffset); // 129 as decimal = 10000001 as binary\nconst lastFourBits = firstByte \u0026 15;  // 15 as decimal = 00001111 as binary\n\nconsole.log(lastFourBits) // 1 as decimal = 0001 as binary\n```\n\n#### Let's actually parse first byte\n```ts\nlet byteOffset = 0;\nconst firstByte = buff.readUint8(byteOffset);\n\nconst fin = Boolean((firstByte \u003e\u003e 7) \u0026 0x1);\n\nconst rsv1 = (firstByte \u003e\u003e 6) \u0026 0x1;\nconst rsv2 = (firstByte \u003e\u003e 5) \u0026 0x1;\nconst rsv3 = (firstByte \u003e\u003e 4) \u0026 0x1;\n\nconst opcode = firstByte \u0026 15;\n```\n\n`fin` - our first bit in frame. Indicates that this is the final fragment in a message. `0` - false, `1` - true;\n\n`rsv1`, `rsv2`, `rsv3` - we don't really care about those reserved fields. They are useful for extending WebSocket protocol.\n\nAccording to RFC: \"MUST be 0 unless an extension is negotiated that defines meanings for non-zero values.\";\n\n`opcode` - four bits. Defines the type of payload data.\n\nAccording to RFC: \n- `0x0`  denotes a continuation frame\n- `0x1` denotes a text frame\n- `0x2` denotes a binary frame\n- `0x8` denotes a connection close\n- `0x9` denotes a ping\n- `0xA` denotes a pong\n\n#### Parsing second byte of frame\n![image](https://user-images.githubusercontent.com/43048524/168483384-c0449989-50ea-4def-bfcd-c4f345d49b1c.png)\n\n```ts\n...\nbyteOffset++;   // 1\nconst secondByte = buff.readUInt8(byteOffset);\n\nconst mask = Boolean((secondByte \u003e\u003e 7) \u0026 0x1);\nlet payloadLen = secondByte \u0026 127;\n```\n\n`mask` - (1 bit). Defines whether the payload is masked. If set to 1, a masking key is present in masking-key, and this is used to unmask the payload.\n\nMore about `MASK`: https://security.stackexchange.com/questions/113297/whats-the-purpose-of-the-mask-in-a-websocket\n\n`payloadLen` - (7 last bits). the length of payload data. \n\nAccording to RFC: \"if 0-125, that is the payload length.  If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length.  If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.\"\n\n#### payloadLen === 126 case\n![image](https://user-images.githubusercontent.com/43048524/168484792-f6cf7738-8d20-413d-bbf3-d7d4eea59fd8.png)\n\nIf payloadLen is equal `126`, the following 2 bytes interpreted as a 16-bit integer are the payload length. `2 bytes = 16 bits`. \n\n`readUint16BE(offset)` reads the following 16 bits in the big-endian format (most common format in networking).\n\n```ts\n...\nlet payloadLen = secondByte \u0026 127;\n\nbyteOffset++;\n\nif (payloadLen === 126) {\n  payloadLen = buff.readUint16BE(byteOffset);\n\n  byteOffset += 2; // because we read 16 bits (2 bytes).\n}\n```\n\n#### payloadLen === 127 case\n![image](https://user-images.githubusercontent.com/43048524/168486584-60f09fcd-6f7f-4f48-be9d-a11a174bf473.png)\n\nIf payloadLen is equal 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length.\n\nIn JavaScript it is hard to support 64-bit payload length because it's can be Bigint value (not regular JS number type), so we will support only 32-bit integers:\n\n```ts\n...\nif (payloadLen === 127) {\n  const first32bits = buff.readUInt32BE(byteOffset);\n  const second32bits = buff.readUInt32BE(byteOffset + 4);\n\n  if (first32bits !== 0) {\n    throw new Error('Payload with 8 byte length is not supported');\n  }\n\n  payloadLen = second32bits;\n  byteOffset += 8; // because we read 64 bits (8 bytes).\n}\n```\n\nBtw - 64-bit is ridiculously big length (we are talking about SINGLE frame).\n\n#### Reading Masking-key\n![image](https://user-images.githubusercontent.com/43048524/168487900-b84a0342-0c04-4c14-bad0-21224e62e2aa.png)\n\nThat part of frame depends on `mask` value. Web browsers ALWAYS mask their frames, so expect `mask` to be `true`.\nIf `mask` is `false`, we skip `Masking-key` part.\n\nAccording to RFC: \"All frames sent from the client to the server are masked by a 32-bit value that is contained within the frame. This field is present if the mask bit is set to 1 and is absent if the mask bit is set to 0.\"\n\nI recommend keeping `maskingKey` as four byte Buffer, instead just 32-bit decimal. It will make unmasking payload process easier (in my opinion).\n```ts\n...\nlet maskingKey = Buffer.alloc(4);\nif (mask) {\n  maskingKey = buff.slice(byteOffset, byteOffset + 4);\n  byteOffset += 4; // because we read 4 bytes.\n}\n```\n\n#### Reading Payload-Data\n![image](https://user-images.githubusercontent.com/43048524/168488647-9ac72c36-23da-4e2b-be13-fdb071c261ad.png)\n\n```ts\nconst rawPayload = buff.slice(byteOffset); // we basically read remaining part of buffer\nconst payload = mask ? unmask(rawPayload, payloadLen, maskingKey) : rawPayload;\n```\nNow, we have to implement `unmask` function.\n\n#### Unmask and RFC definition\nAccording to RFC:\n\"The masking does not affect the length of the \"Payload data\".  To\n   convert masked data into unmasked data, or vice versa, the following\n   algorithm is applied.  The same algorithm applies regardless of the\n   direction of the translation, e.g., the same steps are applied to\n   mask the data as to unmask the data.\n\n   Octet i of the transformed data (\"transformed-octet-i\") is the XOR of\n   octet i of the original data (\"original-octet-i\") with octet at index\n   i modulo 4 of the masking key (\"masking-key-octet-j\"):\n\n     j = i MOD 4\n     transformed-octet-i = original-octet-i XOR masking-key-octet-j\n\n   The payload length, indicated in the framing as frame-payload-length,\n   does NOT include the length of the masking key.  It is the length of\n   the \"Payload data\", e.g., the number of bytes following the masking\n   key.\"\n   \n\u003cbr /\u003e\n\nWhat `octet` is? Octet just like byte, is equal 8 bits (https://en.wikipedia.org/wiki/Octet_(computing))\n\n`1 octet` = `1 byte` = `8 bits`\n\nWhat `XOR` means? It's `^` Bitwise operator (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_XOR)\n\nWhat `MOD` means? Modulo operator `%` (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder)\n\n\n#### Unmasking implementation\n1. Allocate `payloadLen` bytes of memory for unmasked payload.\n2. Loop over each byte of payload.\n3. Just like in RFC: declare `j` variable equal `i MOD 4` (`i % 4`).\n4. Just like in RFC: `unmasked-payload[i] = masked-payload[i] XOR masking-key[j]`\n5. Write unmasked byte to allocated `payload` Buffer with index equal `i`.\n6. We have unmasked payload - we can already READ content.\n\n```ts\nfunction unmask(rawPayload: Buffer, payloadLen: number, maskingKey: Buffer) {\n    const payload = Buffer.alloc(payloadLen);\n    \n    for (let i = 0; i \u003c payloadLen; i++) {\n        const j = i % 4;\n        const decoded = rawPayload[i] ^ (maskingKey[j]);\n\n        payload.writeUInt8(decoded, i);\n    }\n\n    return payload;\n}\n```\n\n#### Frame in our TypeScript code\nWe want to represent frame as regular object.\n```ts\ninterface IFrame {\n  fin: boolean;\n  rsv1: number;\n  rsv2: number;\n  rsv3: number;\n  opcode: number;\n  mask: boolean;\n  payloadLen: number;\n  payload: Buffer;\n  frameLen: number;\n}\n\n...\n...\n\nconst rawPayload = buff.slice(byteOffset);\nconst payload = mask ? unmask(rawPayload, payloadLen, maskingKey) : rawPayload;\n\nconst frame: IFrame = {\n  fin,\n  rsv1,\n  rsv2,\n  rsv3,\n  opcode,\n  mask,\n  payloadLen,\n  payload,\n  frameLen: byteOffset + payload.byteLength,\n}\n\n```\n\n### Third stage - we did it wrong\n\nMy current code:\n```ts\nconst server = new Server((req, res) =\u003e {\n    const wsKey = req.headers['sec-websocket-key'];\n    const wsAcceptKey = createWsAcceptKey(wsKey!);\n    finalizeHandshake(res, wsAcceptKey);\n     \n    // We have to reuse socket from HTTP request. Now we can operate on TCP level\n    req.socket.on('data', (buff) =\u003e {\n        let byteOffset = 0;\n        const firstByte = buff.readUint8(byteOffset);\n\n        const fin = Boolean((firstByte \u003e\u003e 7) \u0026 0x1);\n\n        const rsv1 = (firstByte \u003e\u003e 6) \u0026 0x1;\n        const rsv2 = (firstByte \u003e\u003e 5) \u0026 0x1;\n        const rsv3 = (firstByte \u003e\u003e 4) \u0026 0x1;\n\n        const opcode = firstByte \u0026 15;\n\n        byteOffset++;   // 1\n        const secondByte = buff.readUInt8(byteOffset);\n\n        const mask = Boolean((secondByte \u003e\u003e 7) \u0026 0x1);\n        let payloadLen = secondByte \u0026 127;\n\n        byteOffset++;\n\n        if (payloadLen === 126) {\n            payloadLen = buff.readUint16BE(byteOffset);\n\n            byteOffset += 2; // because we read 16 bits (2 bytes).\n        }\n\n        if (payloadLen === 127) {\n            const first32bits = buff.readUInt32BE(byteOffset);\n            const second32bits = buff.readUInt32BE(byteOffset + 4);\n          \n            if (first32bits !== 0) {\n              throw new Error('Payload with 8 byte length is not supported');\n            }\n          \n            payloadLen = second32bits;\n            byteOffset += 8; // because we read 64 bits (8 bytes).\n        }\n\n        let maskingKey = Buffer.alloc(4);\n        if (mask) {\n            maskingKey = buff.slice(byteOffset, byteOffset + 4);\n            byteOffset += 4; // because we read 4 bytes.\n        }\n\n        const rawPayload = buff.slice(byteOffset); // we basically read remaining part of buffer\n        const payload = mask ? unmask(rawPayload, payloadLen, maskingKey) : rawPayload;\n\n        const frame: IFrame = {\n            fin,\n            rsv1,\n            rsv2,\n            rsv3,\n            opcode,\n            mask,\n            payloadLen,\n            payload,\n            frameLen: byteOffset + payload.byteLength,\n          }\n\n        console.log(frame.payload.toString('utf-8'));\n    })\n});\n\nserver.listen(8080);\n```\n\nLet's test our WebSocket server - connect to server with browser dev tools.\n\n![image](https://user-images.githubusercontent.com/43048524/168587204-e7dbf48d-002a-43d2-ae40-c50b6386158c.png)\n\nServer:\n\n![image](https://user-images.githubusercontent.com/43048524/168587454-0c40c622-5fd5-41a7-b8f3-0b916efb9922.png)\n\n\nEverything looks fine. What we did wrong? Okay, let's send many messages in short amount of time.\n\n![image](https://user-images.githubusercontent.com/43048524/168587743-0ecc3dfa-9714-4ef7-84dd-37b002903b41.png)\n\nServer:\n\n![image](https://user-images.githubusercontent.com/43048524/168587862-444c854e-469d-47c3-bbb6-b06f938287af.png)\n\n#### Where our second message?\nAdd `console.log(buff)` and observe Buffer value;\n```ts\nconst server = new Server((req, res) =\u003e {\n    const wsKey = req.headers['sec-websocket-key'];\n    const wsAcceptKey = createWsAcceptKey(wsKey!);\n    finalizeHandshake(res, wsAcceptKey);\n     \n    // We have to reuse socket from HTTP request. Now we can operate on TCP level\n    req.socket.on('data', (buff) =\u003e {\n        console.log(buff);\n        let byteOffset = 0;\n        const firstByte = buff.readUint8(byteOffset);\n ```\n \n Client:\n \n ![image](https://user-images.githubusercontent.com/43048524/168588732-6c6e5593-8c90-4848-a0fe-a62dea283ff2.png)\n\nServer:\n \n ![image](https://user-images.githubusercontent.com/43048524/168588600-11847f11-0c06-466d-a886-84e10102c11e.png)\n\n#### Explanation\nWeb browser to send TCP packets uses Nagle's algorithm (https://en.wikipedia.org/wiki/Nagle%27s_algorithm) - for better efficiency.\n\nLets assume both WS frames have 17 byte size.\nEvery TCP packet has 40-byte header.\n\nIf browser send each frame as seperate packet - it will transfer 57 + 57 = 114 bytes.\n\nIf browser send both frames in one packet - it will transfer 57+17=74 bytes.\n\n#### Let's fix that problem\n1. Create class `WebsocketParser` - copy and paste parsing code to `readFrame` method and make a few changes.\n```ts\nclass WebsocketParser {\n  parsedFrames: IFrame[] = [];\n  \n  public readFrame(buff: Buffer) {\n    let byteOffset = 0;\n    const firstByte = buff.readUint8(byteOffset);\n    \n    ....\n    \n    const rawPayload = buff.slice(byteOffset, byteOffset+payloadLen); // read buffer ONLY from byteOffset to payloadLen. \n    const remainingBuff = buff.slice(byteOffset+payloadLen); // reamaining buffer - maybe our second frame?\n    const payload = mask ? unmask(rawPayload, payloadLen, maskingKey) : rawPayload;\n\n    const frame: IFrame = {\n      fin,\n      rsv1,\n      rsv2,\n      rsv3,\n      opcode,\n      mask,\n      payloadLen,\n      payload,\n      frameLen: byteOffset + payload.byteLength,\n    }\n\n    this.parsedFrames.push(frame);\n    \n    return remainingBuff;\n  }\n}\n\n```\n\n2. Update our `data` handler\n```ts\nconst server = new Server((req, res) =\u003e {\n    const wsKey = req.headers['sec-websocket-key'];\n    const wsAcceptKey = createWsAcceptKey(wsKey!);\n    finalizeHandshake(res, wsAcceptKey);\n\n    const parser = new WebsocketParser();\n\n    req.socket.on('data', (buff) =\u003e {\n        let remainingBuff = parser.readFrame(buff);\n        \n        while (remainingBuff.byteLength \u003e 0) {\n            remainingBuff = parser.readFrame(remainingBuff);\n        }\n\n        for (const frame of parser.parsedFrames) {\n            console.log(frame.payload.toString('utf-8'));\n        }\n        \n        parser.parsedFrames = [];\n    })\n});\n```\n\n3. Test\n\nClient:\n\n![image](https://user-images.githubusercontent.com/43048524/168601640-e29abd3a-c5ad-451c-b30f-0685820395c9.png)\n\nServer:\n\n![image](https://user-images.githubusercontent.com/43048524/168601561-e5dc15dc-417e-4655-b47f-066c462a1836.png)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaldziuba03%2Fwebsockets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichaldziuba03%2Fwebsockets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaldziuba03%2Fwebsockets/lists"}