Node.jsで依存関係のないWebSocketサーバーを作成する



Node.jsは、クライアントサーバーアプリケーションを構築するための一般的なツールです。 Node.jsを正しく使用すると、1つのスレッドを使用して多数のネットワーク要求を処理できます。間違いなく、ネットワークI / Oはこのプラットフォームの強みの1つです。 Node.jsを使用して、さまざまなネットワークプロトコルを積極的に使用するサーバー側のアプリケーションコードを作成する場合、開発者はこれらのプロトコルがどのように機能するかを知っている必要があるように思われますが、そうではないことがよくあります。これは、Node.jsのもう1つの長所によるものです。これは、ほぼすべてのタスクに対応する既製のソリューションを見つけることができるNPMパッケージマネージャーです。既製のパッケージを使用して、私たちは私たちの生活を簡素化し、コードを再利用します(そしてこれは正しいです)が、同時に、ライブラリの画面の後ろに、行われているプロセスの本質を隠します。この記事では、外部の依存関係を使用せずに仕様の一部を実装することにより、WebSocketプロトコルを理解しようとします。猫へようこそ。





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






もちろん、アプリケーションでWebSocketが必要であり、おそらく必要な場合は、どうしても必要な場合を除いて、プロトコルを自分で実装しないでください。npmのさまざまなライブラリからいつでも適切なソリューションを選択できます。すでに作成およびテストされたコードを再利用することをお勧めします。しかし、それが「内部で」どのように機能するかを理解することは、他の誰かのコードを使用するだけではなく、常にあなたに多くを与えるでしょう。上記の例はgithubで入手できます




All Articles