Escribiendo su servidor WebSocket sin dependencia en Node.js



Node.js es una herramienta popular para crear aplicaciones cliente-servidor. Si se usa correctamente, Node.js es capaz de manejar una gran cantidad de solicitudes de red usando solo un hilo. Sin lugar a dudas, la E / S de red es uno de los puntos fuertes de esta plataforma. Parecería que cuando se usa Node.js para escribir código del lado del servidor para una aplicación que usa activamente varios protocolos de red, los desarrolladores deberían saber cómo funcionan estos protocolos, pero este no suele ser el caso. Esto se debe a otro punto fuerte de Node.js, es su administrador de paquetes NPM, en el que puede encontrar una solución lista para usar para casi cualquier tarea. Usando paquetes prefabricados, simplificamos nuestra vida, reutilizamos el código (y esto es correcto), pero al mismo tiempo nos escondemos de nosotros mismos, detrás de la pantalla de las bibliotecas, la esencia de los procesos en curso.En este artículo, intentaremos comprender el protocolo WebSocket implementando parte de la especificación sin utilizar dependencias externas. Bienvenido a 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!');






Por supuesto, si su aplicación necesita WebSockets, y lo más probable es que los necesite, no debe implementar el protocolo usted mismo a menos que sea absolutamente necesario. Siempre puede elegir una solución adecuada de la variedad de bibliotecas en npm. Es mejor reutilizar el código ya escrito y probado. Pero entender cómo funciona "bajo el capó" siempre le dará mucho más que simplemente usar el código de otra persona. El ejemplo anterior está disponible en github




All Articles