To read more articles in this series please visit myMaking a blog, sample code please visithere.

Native implementation of WebSocket applications

The previous section used socket. IO to implement WebSocket, which is also a common approach in development.

This section uses the Net module of Nodejs and the WebSocket API on the Web side to implement the WebSocket server.

Example codes: /lesson20/server.js, /lesson20/index.html

1. Create a Net server on the server

// net module const net = require('net'// The net module is used to create the server, which returns a raw socket object, different from the socket object in socket. IO. const server = net.createServer((socket) => { }) server.listen(8080)Copy the code

2. Create a WebSocket link on the Web

Create a WebSocket connection, at which point the console’s Network module can see an HTTP connection in a pending state.

This connection is an HTTP request with the following content added to the request header of a normal HTTP request:

Sec-WebSocket-Extensions: permessage-deflate; Client_max_window_bits // Extended information

Sec-websocket-key: O3PKSb95qaSB7/+XfaTg7Q== // Sends a Key to the server to verify whether the server supports WebSocket

Sec-websocket-version: 13 // WebSocket Version

Upgrade: websocket // Inform the server that the communication protocol will be upgraded to Websocket. If the server supports the Upgrade, go to the next step

const ws = new WebSocket('ws://localhost:8080/')
Copy the code

3. The server uses socket.once to trigger a data event to process HTTP request header data

socket.once('data', (buffer) => {// Received HTTP header data const STR = buffer.tostring () console.log(STR)})Copy the code

The print result is as follows:

GET/HTTP/1.1 Host: localhost:8080 Connection: Upgrade Pragma: no-cache cache-control: no-cache Upgrade: Websocket Origin: file:// sec-websocket-version: 13 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; X64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36 Accept-encoding: gzip, deflate, br Accept-Language: zh-CN,zh; Q = 0.9 cookies: _ga = GA1.1.1892261700.1545540050; _gid = GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAAB Sec-WebSocket-Key: JStOineTIKaQskxefzer7Q== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsCopy the code

Convert the carriage return character to \r\n and the result is as follows:

GET/HTTP/1.1\r\nHost: localhost:8080\r\nConnection: Upgrade\r\nPragma: no-cache\r\ ncache-control: no-cache\r\nUpgrade: websocket\r\nOrigin: file://\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla / 5.0 (Windows NT 10.0; Win64; X64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\r\ naccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh; Q = 0.9 \ r \ nCookie: _ga = GA1.1.1892261700.1545540050; _gid = GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAAB\r\nSec-WebSocket-Key: dRB1xDJ/vV+IAGnG7TscNQ==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\nCopy the code

By looking at the request header data, you can see that the data is displayed as a key: value, which can be converted to an object format by string slicing.

4. Convert the request header string into an object

Create a parseHeader method to handle the request header.

functionParseHeader (STR) {// Cut the request header data into an array by pressing the return character to get each row of datalet arr = str.split('\r\n').filter(item => item) // The first line of data is GET/HTTP/1.1 and can be discarded. Arr.shift () console.log(arr) /*'Host: localhost:8080'.'Connection: Upgrade'.'Pragma: no-cache'.'Cache-Control: no-cache'.'Upgrade: websocket'.'Origin: file://'.'Sec-WebSocket-Version: 13'.'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'.'Accept-Encoding: gzip, deflate, br'.'Accept-Language: zh-CN,zh; Q = 0.9 '.'cookies: _ga = GA1.1.1892261700.1545540050; _gid = GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAAB'.'Sec-WebSocket-Key: jqxd7P0Xx9TGkdMfogptRw=='.'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits'] * /letForEach ((item) => {// required":"Cut the array into keys and valueslet [name, value] = item.split(':') / / remove the useless Spaces, a property name to lowercase name = name. Replace (/ ^ \ | \ s + $s/g,' ').toLowerCase()
    value = value.replace(/^\s|\s+$/g, ' '}}}}}}}}return headers
}
Copy the code

The print result is as follows:

{ host: 'localhost',
  connection: 'Upgrade',
  pragma: 'no-cache'.'cache-control': 'no-cache',
  upgrade: 'websocket',
  origin: 'file'.'sec-websocket-version': '13'.'user-agent':
   'the Mozilla / 5.0 (Windows NT 10.0; Win64; X64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'.'accept-encoding': 'gzip, deflate, br'.'accept-language': 'zh-CN,zh; Q = 0.9 ',
  cookie:
   '_ga = GA1.1.1892261700.1545540050; _gid = GA1.1.585339125.1552405260 '.'sec-websocket-key': 'TipyPZNW+KNvV3fePNpriw=='.'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits' }
Copy the code

5. Determine whether the request is a WebSocket request based on the request header parameters

According to the headers [‘ upgrade ‘]! == ‘webSocket’ to check whether the HTTP connection can be upgraded to webSocket. If it can be upgraded, it indicates a WebSocket request.

According to the headers [‘ SEC – websocket – version]! == ’13’, check whether the WebSocket version is 13 to avoid compatibility problems due to different versions.

socket.once('data', (buffer) => {// Received HTTP header data const STR = buffer.toString() console.log(STR) // 4. Const headers = parseHeader(STR) console.log(headers) // 5. Determines whether the request is a WebSocket connectionif (headers['upgrade']! = ='websocket'// If the current request is not a WebSocket connection, close the connection console.log('Non-Websocket connection')
    socket.end()
  } else if (headers['sec-websocket-version']! = ='13') {// Check whether the WebSocket version is 13, in case of other versions, resulting in compatibility error console.log('WebSocket version error ')
    socket.end()
  } else{// request is WebSocket connection, further processing}})Copy the code

6. Verify sec-websocket-key to complete the connection

According to the protocol, the front end returns a request header to complete the process of establishing a WebSocket connection.

Reference: tools.ietf.org/html/rfc645…

If the client verification result is correct, the Network module in the console can see that the STATUS code of the HTTP request changes to 101 Switching Protocols and the ws-onopen event on the client is triggered.

socket.once('data', (buffer) => {// Received HTTP header data const STR = buffer.toString() console.log(STR) // 4. Const headers = parseHeader(STR) console.log(headers) // 5. Determines whether the request is a WebSocket connectionif (headers['upgrade']! = ='websocket'// If the current request is not a WebSocket connection, close the connection console.log('Non-Websocket connection')
    socket.end()
  } else if (headers['sec-websocket-version']! = ='13') {// Check whether the WebSocket version is 13, in case of other versions, resulting in compatibility error console.log('WebSocket version error ')
    socket.end()
  } else{/ / 6. Check the Sec - WebSocket - Key, complete the connection / * agreement specified in the check with GUID, refer to the following link: https://tools.ietf.org/html/rfc6455# section - 5.5.2
        https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol
      */
      const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
      const key = headers['sec-websocket-key']
      const hash = crypto.createHash('sha1') // Create a hash object with the signature algorithm sha1 hash.update('${key}${GUID}'// Connect key to GUID, update tohash
      const result = hash.digest('base64') // Base64 string const header = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept:${result}\ r \ n \ r \ n ` / / generated request header for front-end check socket. Write (header) / / returns the HTTP headers, inform the client check result, HTTP status code 101 indicates switching protocol: https://httpstatuses.com/101. // If the client verification result is correct, the Network module in the console can see that the STATUS code of the HTTP request changes to 101 Switching Protocols, and the client ws. Onopen event is triggered. Console. log(header) // Handle chat data}})Copy the code

7. After the connection is established, data from the client is received and processed through the data event

Once a connection is started, as you can see in the console’s Network module, it remains in the Pending state until the connection is disconnected.

In this case, the data on the client can be processed through the data event. However, the data exchanged between the two parties is binary and can be used normally only after being processed according to its format.

The format is as follows:

Processing received data:

function decodeWsFrame(data) {
  let start = 0;
  let frame = {
    isFinal: (data[start] & 0x80) === 0x80,
    opcode: data[start++] & 0xF,
    masked: (data[start] & 0x80) === 0x80,
    payloadLen: data[start++] & 0x7F,
    maskingKey: ' ',
    payloadData: null
  };

  if (frame.payloadLen === 126) {
    frame.payloadLen = (data[start++] << 8) + data[start++];
  } else if (frame.payloadLen === 127) {
    frame.payloadLen = 0;
    for (leti = 7; i >= 0; --i) { frame.payloadLen += (data[start++] << (i * 8)); }}if (frame.payloadLen) {
    if (frame.masked) {
      const maskingKey = [
        data[start++],
        data[start++],
        data[start++],
        data[start++]
      ];

      frame.maskingKey = maskingKey;

      frame.payloadData = data
        .slice(start, start + frame.payloadLen)
        .map((byte, idx) => byte ^ maskingKey[idx % 4]);
    } else {
      frame.payloadData = data.slice(start, start + frame.payloadLen);
    }
  }

  console.dir(frame)
  return frame;
}
Copy the code

Processing outgoing data:

functionencodeWsFrame(data) { const isFinal = data.isFinal ! == undefined ? data.isFinal :true, opcode = data.opcode ! == undefined ? data.opcode : 1, payloadData = data.payloadData ? Buffer.from(data.payloadData) : null, payloadLen = payloadData ? payloadData.length : 0;let frame = [];

  if (isFinal) frame.push((1 << 7) + opcode);
  else frame.push(opcode);

  if (payloadLen < 126) {
    frame.push(payloadLen);
  } else if (payloadLen < 65536) {
    frame.push(126, payloadLen >> 8, payloadLen & 0xFF);
  } else {
    frame.push(127);
    for (let i = 7; i >= 0; --i) {
      frame.push((payloadLen & (0xFF << (i * 8))) >> (i * 8));
    }
  }

  frame = payloadData ? Buffer.concat([Buffer.from(frame), payloadData]) : Buffer.from(frame);

  console.dir(decodeWsFrame(frame));
  return frame;
}
Copy the code

Processing chat data:

socket.once('data', (buffer) => {// Received HTTP header data const STR = buffer.toString() console.log(STR) // 4. Const headers = parseHeader(STR) console.log(headers) // 5. Determines whether the request is a WebSocket connectionif (headers['upgrade']! = ='websocket'// If the current request is not a WebSocket connection, close the connection console.log('Non-Websocket connection')
    socket.end()
  } else if (headers['sec-websocket-version']! = ='13') {// Check whether the WebSocket version is 13, in case of other versions, resulting in compatibility error console.log('WebSocket version error ')
    socket.end()
  } else{/ / 6. Check the Sec - WebSocket - Key, complete the connection / * agreement specified in the check with GUID, refer to the following link: https://tools.ietf.org/html/rfc6455# section - 5.5.2
        https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol
      */
      const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
      const key = headers['sec-websocket-key']
      const hash = crypto.createHash('sha1') // Create a hash object with the signature algorithm sha1 hash.update('${key}${GUID}'// Connect key to GUID, update tohash
      const result = hash.digest('base64') // Base64 string const header = 'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept:${result}\ r \ n \ r \ n ` / / generated request header for front-end check socket. Write (header) / / returns the HTTP headers, inform the client check result, HTTP status code 101 indicates switching protocol: https://httpstatuses.com/101. // If the client verification result is correct, the Network module in the console can see that the STATUS code of the HTTP request changes to 101 Switching Protocols, and the client ws. Onopen event is triggered. Console. log(header) // 7. After establishing a connection, receive data from the client through the data event and process socket.on('data'. (buffer) => { const data = decodeWsFrame(buffer) console.log(data) console.log(data.payloadData && Data. PayloadData. ToString ()) / / opcode of 8, said the client disconnected launchedif(data.opcode === 8) {socket.end() // Disconnect from the client}else{// The processing of data received from the client. By default, the received data is returned. Socket. write(encodeWsFrame({payloadData: 'The server received the following message:${data.payloadData ? data.payloadData.toString() : ''}'}))}})}}Copy the code

A simple WebSocket-based chat application has been created. After starting the server, you can open index.html to see the effect.