WebSocket --- ws module source code analysis (detailed explanation)

Summary

In this article, I wrote about how to implement a WebSocket communication on the node side and the web side.
Usage of WebSocket on node and client

On the node side, we use the ws module to create WebSocket and WebSocketServer. How does the ws module enable two-way communication with the client?

426 status code

In HTTP, 426 means "Upgrade Required", that is, the client needs to access through the upgraded version of the HTTP protocol. This status code is mainly used in the WebSockets protocol, indicating that the client needs to use the WebSockets protocol to connect to the server.

What does that mean? For example, if we create an HTTP service and write it like this:

const http = require('http')

const server = http.createServer((req, res) => {
    
    
  const body = http.STATUS_CODES[426];
  res.writeHead('426', {
    
    
    'Content-Type': 'text/align',
    'Content-Length': body.length
  })
  res.end(body)
})

server.listen(8080)

It just tells the client that if you access my service, then you have to upgrade the service. That is, use WebSocket to access me!

There is a question. If the client uses WebSocket to access, how does the server respond?

Should we still handle it directly in the callback in createServer?

upgrade event

Here, if the client accesses the server through WebSocket, the upgrade event of the server will be triggered, which means it will enter the following callback function.

server.on('upgrade',(req, socket, head) => {
    
    
  // 固定格式
  const key = req.headers['sec-websocket-key'];
  const digest = createHash('sha1')
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
  .digest('base64');

  const headers = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${
      
      digest}`
  ];
  socket.write(headers.concat('\r\n').join('\r\n'));

  // 客户端发送的消息
  socket.on('data', (data) => {
    
    
    console.log(data.toString());
  })
 
  // 服务端向客户端发送消息
  socket.write('你好')
})

In this callback, two-way communication with the client is carried out through socket.

Transcode

But only in the above example, it seems that the data obtained every time is garbled. This is because the communication messages between WebSockets cannot be directly transcoded through Buffer's toString. Here are some transcoding methods found online:

server.on('upgrade', (req, socket, head) => {
    
    
  const key = req.headers['sec-websocket-key'];
  const digest = createHash('sha1')
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
  .digest('base64');

  const headers = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${
      
      digest}`
  ];
  socket.write(headers.concat('\r\n').join('\r\n'));
  socket.on('data',(data) => {
    
    
    console.log(decodeSocketFrame(data).payloadBuf.toString())
    socket.write(encodeSocketFrame({
    
    
      fin:1,
      opcode:1,
      payloadBuf:Buffer.from('你好')
  }))
  })
})

function decodeSocketFrame (bufData){
    
    
  let bufIndex = 0
  const byte1 = bufData.readUInt8(bufIndex++).toString(2)
  const byte2 = bufData.readUInt8(bufIndex++).toString(2)
  console.log(byte1);
  console.log(byte2);
  const frame =  {
    
    
      fin:parseInt(byte1.substring(0,1),2),
      // RSV是保留字段,暂时不计算
      opcode:parseInt(byte1.substring(4,8),2),
      mask:parseInt(byte2.substring(0,1),2),
      payloadLen:parseInt(byte2.substring(1,8),2),
  }
  // 如果frame.payloadLen为126或127说明这个长度不够了,要使用扩展长度了
  // 如果frame.payloadLen为126,则使用Extended payload length同时为16/8字节数
  // 如果frame.payloadLen为127,则使用Extended payload length同时为64/8字节数
  // 注意payloadLen得长度单位是字节(bytes)而不是比特(bit)
  if(frame.payloadLen==126) {
    
    
      frame.payloadLen = bufData.readUIntBE(bufIndex,2);
      bufIndex+=2;
  } else if(frame.payloadLen==127) {
    
    
      // 虽然是8字节,但是前四字节目前留空,因为int型是4字节不留空int会溢出
      bufIndex+=4;
      frame.payloadLen = bufData.readUIntBE(bufIndex,4);
      bufIndex+=4;
  }
  if(frame.mask){
    
    
      const payloadBufList = []
      // maskingKey为4字节数据
      frame.maskingKey=[bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++]];
      for(let i=0;i<frame.payloadLen;i++) {
    
    
          payloadBufList.push(bufData[bufIndex+i]^frame.maskingKey[i%4]);
      }
      frame.payloadBuf = Buffer.from(payloadBufList)
  } else {
    
    
      frame.payloadBuf = bufData.slice(bufIndex,bufIndex+frame.payloadLen)
  }
  return frame
}

function encodeSocketFrame (frame){
    
    
  const frameBufList = [];
  // 对fin位移七位则为10000000加opcode为10000001
  const header = (frame.fin<<7)+frame.opcode;
  frameBufList.push(header)
  const bufBits = Buffer.byteLength(frame.payloadBuf);
  let payloadLen = bufBits;
  let extBuf;
  if(bufBits>=126) {
    
    
      //65536是2**16即两字节数字极限
      if(bufBits>=65536) {
    
    
          extBuf = Buffer.allocUnsafe(8);
          buf.writeUInt32BE(bufBits, 4);
          payloadLen = 127;
      } else {
    
    
          extBuf = Buffer.allocUnsafe(2);
          buf.writeUInt16BE(bufBits, 0);
          payloadLen = 126;
      }
  }
  let payloadLenBinStr = payloadLen.toString(2);
  while(payloadLenBinStr.length<8){
    
    payloadLenBinStr='0'+payloadLenBinStr;}
  frameBufList.push(parseInt(payloadLenBinStr,2));
  if(bufBits>=126) {
    
    
      frameBufList.push(extBuf);
  }
  frameBufList.push(...frame.payloadBuf)
  return Buffer.from(frameBufList)
}

Implementation of WebSocketServer

With the above foundation, you basically know how two-way communication is achieved. Let’s take a look at the implementation of WebSocketServer.
When we use it, we do it in this way:

const WebSocketServer = require('ws')

const wss = new WebSocketServer('8080');
wss.on('connection', (ws) => {
    
    
  
})

We know that connection is the callback of httpServer. Why can it be used in WebSocketServer?

export default class WebSocketServer {
    
    
  constructor(port) {
    
    
    this._server = http.createServer((req, res) => {
    
    
      const body = http.STATUS_CODES[426];
      res.writeHead('426', {
    
    
        'Content-Type': 'text/align',
        'Content-Length': body.length
      })
      res.end(body)
    })

    this._server.listen(port);

    const connectionEmit = this.emit.bind(this, 'connection');
    const closeEmit = this.emit.bind(this, 'close');
    // 其他事件,都是http能监听到的;
    const map = {
    
    
      connection: connectionEmit,
      close: closeEmit
    }

    for(let emitName in map) {
    
    
      this._server.on(emitName, map[emitName])
    }
  }
}

In WebSocketServer, if the client triggers an http event, it will forward it to the WebSocket instance.
Then deal with your own logic.

Guess you like

Origin blog.csdn.net/weixin_46726346/article/details/134501070