├── .gitignore ├── eslint.config.js ├── prettier.config.js ├── .editorconfig ├── README.md ├── package.json ├── LICENSE └── NodeJS └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const init = require('eslint-config-metarhia'); 4 | 5 | init[0].rules['class-methods-use-this'] = 'off'; 6 | 7 | module.exports = init; 8 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | printWidth: 80, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | tabWidth: 2, 8 | useTabs: false, 9 | semi: true, 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.mjs,*.ts,*.json,*.yml}] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Websocket 2 | 3 | - Pure node.js server implementation 4 | - Websocket client support in Node.js 21 5 | 6 | Другие материалы: 7 | - [Бесплатный курс по ноде](https://github.com/HowProgrammingWorks/Index/blob/master/Courses/NodeJS.md) 8 | - [Платный курс по ноде](https://github.com/HowProgrammingWorks/Index/blob/master/Courses/NodeJS-2024.md) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patterns", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "lint": "eslint . && prettier -c \"**/*.js\"", 6 | "fix": "eslint . --fix && prettier --write \"**/*.js\"" 7 | }, 8 | "author": "Timur Shemsedinov", 9 | "private": true, 10 | "dependencies": { 11 | "eslint": "^9.12.0", 12 | "eslint-config-metarhia": "^9.1.0", 13 | "prettier": "^3.3.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 How.Programming.Works contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NodeJS/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('node:http'); 4 | const crypto = require('node:crypto'); 5 | 6 | const PORT = 8000; 7 | const HOST = '127.0.0.1'; 8 | const EOL = '\r\n'; 9 | const UPGRADE = [ 10 | 'HTTP/1.1 101 Switching Protocols', 11 | 'Upgrade: websocket', 12 | 'Connection: Upgrade', 13 | 'Sec-WebSocket-Accept: ', 14 | ].join(EOL); 15 | const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 16 | const MASK_LENGTH = 4; 17 | const PING_TIMEOUT = 5000; 18 | const PING = Buffer.from([0x89, 0]); 19 | const OPCODE_SHORT = 0x81; 20 | const LEN_16_BIT = 126; 21 | const MAX_16_BIT = 65536; 22 | const LEN_64_BIT = 127; 23 | 24 | const calcOffset = (frame, length) => { 25 | if (length < LEN_16_BIT) return [2, 6]; 26 | if (length === LEN_16_BIT) return [4, 8]; 27 | return [10, 14]; 28 | }; 29 | 30 | const parseFrame = (frame) => { 31 | const length = frame[1] ^ 0x80; 32 | const [maskOffset, dataOffset] = calcOffset(frame, length); 33 | const mask = frame.subarray(maskOffset, maskOffset + MASK_LENGTH); 34 | const data = frame.subarray(dataOffset); 35 | return { mask, data }; 36 | }; 37 | 38 | const unmask = (buffer, mask) => { 39 | const data = Buffer.allocUnsafe(buffer.length); 40 | buffer.copy(data); 41 | for (let i = 0; i < data.length; i++) { 42 | data[i] ^= mask[i & 3]; 43 | } 44 | return data; 45 | }; 46 | 47 | class Connection { 48 | constructor(socket) { 49 | this.socket = socket; 50 | socket.on('data', (data) => { 51 | this.receive(data); 52 | }); 53 | socket.on('error', (error) => { 54 | console.log(error.code); 55 | }); 56 | setInterval(() => { 57 | socket.write(PING); 58 | }, PING_TIMEOUT); 59 | } 60 | 61 | send(text) { 62 | const data = Buffer.from(text); 63 | let meta = Buffer.alloc(2); 64 | const length = data.length; 65 | meta[0] = OPCODE_SHORT; 66 | if (length < LEN_16_BIT) { 67 | meta[1] = length; 68 | } else if (length < MAX_16_BIT) { 69 | const len = Buffer.from([(length & 0xff00) >> 8, length & 0x00ff]); 70 | meta = Buffer.concat([meta, len]); 71 | meta[1] = LEN_16_BIT; 72 | } else { 73 | const len = Buffer.alloc(8); 74 | len.writeBigInt64BE(BigInt(length), 0); 75 | meta = Buffer.concat([meta, len]); 76 | meta[1] = LEN_64_BIT; 77 | } 78 | const frame = Buffer.concat([meta, data]); 79 | this.socket.write(frame); 80 | } 81 | 82 | receive(data) { 83 | console.log('data: ', data[0], data.length); 84 | if (data[0] !== OPCODE_SHORT) return; 85 | const frame = parseFrame(data); 86 | const msg = unmask(frame.data, frame.mask); 87 | const text = msg.toString(); 88 | this.send(`Echo "${text}"`); 89 | console.log('Message:', text); 90 | } 91 | 92 | accept(key) { 93 | const hash = crypto.createHash('sha1'); 94 | hash.update(key + MAGIC); 95 | const packet = UPGRADE + hash.digest('base64'); 96 | this.socket.write(packet + EOL + EOL); 97 | } 98 | } 99 | 100 | const server = http.createServer((req, res) => { 101 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 102 | res.end('Connect with Websocket'); 103 | }); 104 | 105 | server.on('upgrade', (req, socket, head) => { 106 | const ws = new Connection(socket); 107 | const key = req.headers['sec-websocket-key']; 108 | ws.accept(key); 109 | ws.receive(head); 110 | }); 111 | 112 | server.listen(PORT, HOST); 113 | --------------------------------------------------------------------------------