Écriture de votre serveur WebSocket sans dépendance dans Node.js



Node.js est un outil populaire pour créer des applications client-serveur. Utilisé correctement, Node.js est capable de gérer un grand nombre de requêtes réseau en utilisant un seul thread. Sans aucun doute, les E / S réseau sont l'une des forces de cette plate-forme. Il semblerait que lors de l'utilisation de Node.js pour écrire du code côté serveur pour une application qui utilise activement divers protocoles réseau, les développeurs devraient savoir comment ces protocoles fonctionnent, mais ce n'est souvent pas le cas. Cela est dû à un autre point fort de Node.js, c'est son gestionnaire de packages NPM, dans lequel vous pouvez trouver une solution toute faite pour presque toutes les tâches. En utilisant des packages prêts à l'emploi, nous simplifions notre vie, réutilisons le code (et c'est correct), mais en même temps nous nous cachons, derrière l'écran des bibliothèques, l'essence des processus en cours.Dans cet article, nous allons essayer de comprendre le protocole WebSocket en implémentant une partie de la spécification sans utiliser de dépendances externes. Bienvenue au chat.





, , 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!');






Bien sûr, si votre application a besoin de WebSockets, et que vous en avez probablement besoin, vous ne devriez pas implémenter le protocole vous-même sauf en cas de nécessité absolue. Vous pouvez toujours choisir une solution appropriée parmi la variété de bibliothèques de npm. Mieux vaut réutiliser du code déjà écrit et testé. Mais comprendre comment cela fonctionne "sous le capot" vous donnera toujours beaucoup plus que simplement utiliser le code de quelqu'un d'autre. L'exemple ci-dessus est disponible sur github




All Articles