Menulis server WebSocket gratis ketergantungan Anda di Node.js



Node.js adalah alat yang populer untuk membangun aplikasi klien-server. Digunakan dengan benar, Node.js mampu menangani permintaan jaringan dalam jumlah besar hanya dengan menggunakan satu utas. Tidak diragukan lagi, jaringan I / O adalah salah satu kekuatan platform ini. Tampaknya saat menggunakan Node.js untuk menulis kode sisi server untuk aplikasi yang secara aktif menggunakan berbagai protokol jaringan, pengembang harus mengetahui cara kerja protokol ini, tetapi ini sering kali tidak terjadi. Ini karena poin kuat lain dari Node.js, ini adalah manajer paket NPM-nya, di mana Anda dapat menemukan solusi siap pakai untuk hampir semua tugas. Dengan menggunakan paket yang sudah jadi, kita menyederhanakan hidup kita, menggunakan kembali kode (dan ini benar), tetapi pada saat yang sama kita bersembunyi dari diri kita sendiri, di balik layar pustaka, inti dari proses yang sedang berlangsung.Pada artikel ini, kami akan mencoba memahami protokol WebSocket dengan mengimplementasikan bagian spesifikasi tanpa menggunakan dependensi eksternal. Selamat datang di cat.





, , WebSocket . , , http, , . http . Http request/reply — , . (, http 2.0). , . , , http, . RFC6202, , . WebSocket 2008 , . , WebSocket 2011 13 RFC6455. OSI http tcp. WebSocket http. WebSocket , , , . . , WebSocket 2009 , , Google Chrome 4 . , , . WebSocket :



  1. (handshake)




, , WebSocket, http . , GET . , , , , . http , . typescript ts-node.



import * as http from 'http';
import * as stream from 'stream';

export class SocketServer {
  constructor(private port: number) {
    http
      .createServer()
      .on('request', (request: http.IncomingMessage, socket: stream.Duplex) => {
        console.log(request.headers);
      })
      .listen(this.port);
      console.log('server start on port: ', this.port);
  }
}

new SocketServer(8080);


8080. .



const socket = new WebSocket('ws://localhost:8080');


WebSocket, . readyState. :



  • 0
  • 1 — .
  • 2
  • 3


readyState, 0, 3. , . WebSocket API



:



{
  host: 'localhost:8080',
  connection: 'Upgrade',
  pragma: 'no-cache',
  'cache-control': 'no-cache',
  'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
  upgrade: 'websocket',
  origin: 'chrome-search://local-ntp',
  'sec-websocket-version': '13',
  'accept-encoding': 'gzip, deflate, br',
  'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
  'sec-websocket-key': 'h/k2aB+Gu3cbgq/GoSDOqQ==',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits'
}


, http RFC2616. http GET, upgrade , . , 101, — . WebSocket , :



  • sec-websocket-version . 13
  • sec-websocket-extensions , . ,
  • sec-websocket-protocol , . , , . — , .
  • sec-websocket-key . . .


, , 101, sec-websocket-accept, , sec-websocket-key :



  1. sec-websocket-key 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  2. sha-1
  3. base64


Upgrade: WebSocket Connection: Upgrade. , . sec-websocket-key node.js crypto. .



import * as crypto from 'crypto';


SocketServer



private HANDSHAKE_CONSTANT = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
constructor(private port: number) {
  http
    .createServer()
    .on('upgrade', (request: http.IncomingMessage, socket: stream.Duplex) => {
      const clientKey = request.headers['sec-websocket-key'];
      const handshakeKey = crypto
        .createHash('sha1')
        .update(clientKey + this.HANDSHAKE_CONSTANT)
        .digest('base64');
      const responseHeaders = [
        'HTTP/1.1 101',
        'upgrade: websocket',
        'connection: upgrade',
        `sec-webSocket-accept: ${handshakeKey}`,
        '\r\n',
      ];
      socket.write(responseHeaders.join('\r\n'));
    })
    .listen(this.port);
  console.log('server start on port: ', this.port);
}


http Node.js upgrade , . , , 1. . .





. — . . , , , .. . , , , ( ). , , , . .





, , . .



. 2



0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
FIN RSV1 RSV2 RSV3 OPCODE MASK


  • FIN . 1, , 0, . .
  • RSV1, RSV2, RSV3 .
  • OPCODE 4 . : . . , UTF8, . 3 ping, pong, close. .

    • 0 , —
    • 1
    • 2
    • 8
    • 9 Ping
    • xA Pong
  • MASK — . 0, , 1, . , , . , , .
  • 7 , .


. 0 12



  • <= 125, , , . ,
  • = 126 2
  • = 127 8


0, 2, 8 0, 4




, , . . — 4 , . , XOR. , , XOR.



, WebSocket .





, . WebSocket , . Ping. , . Ping, , . , Pong , Ping. ,



private MASK_LENGTH = 4; //  .   
private OPCODE = {
  PING: 0x89, //     Ping
  SHORT_TEXT_MESSAGE: 0x81, //     ,    125 
};
private DATA_LENGTH = {
  MIDDLE: 128, // ,         
  SHORT: 125, //    
  LONG: 126, // ,   2    
  VERY_LONG: 127, // ,   8    
};


Ping



private ping(message?: string) {
  const payload = Buffer.from(message || '');
  const meta = Buffer.alloc(2);
  meta[0] = this.OPCODE.PING;
  meta[1] = payload.length;
  return Buffer.concat([meta, payload]);
}


, , . - Ping. , . , , . .



private CONTROL_MESSAGES = {
  PING: Buffer.from([this.OPCODE.PING, 0x0]),
};
private connections: Set<stream.Duplex> = new Set();


, Ping 5 , .



setInterval(() => socket.write(this.CONTROL_MESSAGES.PING), heartbeatTimeout);
this.connections.add(socket);


. . , , . , , , , , .



private decryptMessage(message: Buffer) {
  const length = message[1] ^ this.DATA_LENGTH.MIDDLE; // 1
  if (length <= this.DATA_LENGTH.SHORT) {
    return {
      length,
      mask: message.slice(2, 6), // 2
      data: message.slice(6),
    };
  }
  if (length === this.DATA_LENGTH.LONG) {
    return {
      length: message.slice(2, 4).readInt16BE(), // 3
      mask: message.slice(4, 8),
      data: message.slice(8),
    };
  }
  if (length === this.DATA_LENGTH.VERY_LONG) {
    return {
      payloadLength: message.slice(2, 10).readBigInt64BE(), // 4
      mask: message.slice(10, 14),
      data: message.slice(14),
    };
  }
  throw new Error('Wrong message format');
}


  1. . XOR , 128 , 10000000. , , , 1.
  2. 126,
  3. 127,


. ,



private unmasked(mask: Buffer, data: Buffer) {
  return Buffer.from(data.map((byte, i) => byte ^ mask[i % this.MASK_LENGTH]));
}


XOR . 4 . .



public sendShortMessage(message: Buffer, socket: stream.Duplex) {
  const meta = Buffer.alloc(2);
  meta[0] = this.OPCODE.SHORT_TEXT_MESSAGE;
  meta[1] = message.length;
  socket.write(Buffer.concat([meta, message]));
}


. , .



socket.on('data', (data: Buffer) => {
  if (data[0] === this.OPCODE.SHORT_TEXT_MESSAGE) { //       
    const meta = this.decryptMessage(data);
    const message = this.unmasked(meta.mask, meta.data);
    this.connections.forEach(socket => {
      this.sendShortMessage(message, socket);
    });
  }
});

this.connections.forEach(socket => {
  this.sendShortMessage(
    Buffer.from(`   .    ${this.connections.size}`),
    socket,
  );
});


. .



const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = ({ data }) => console.log(data);




socket.send('Hello world!');






Tentu saja, jika aplikasi Anda memerlukan WebSockets, dan kemungkinan besar Anda membutuhkannya, Anda tidak boleh mengimplementasikan protokol sendiri kecuali benar-benar diperlukan. Anda selalu dapat memilih solusi yang sesuai dari berbagai pustaka di npm. Lebih baik menggunakan kembali kode yang sudah ditulis dan diuji. Namun memahami cara kerjanya "di balik terpal" akan selalu memberi Anda lebih dari sekadar menggunakan kode orang lain. Contoh di atas tersedia di github




All Articles