Skip to content
On this page

WebSocket数据帧格式以及服务端数据推送

内容参考

WebSocket协议中,数据是通过数据帧来传递的,协议规定了数据帧的格式,服务端要想给客户端推送数据,必须将要推送的数据组装成一个数据帧,这样客户端才能接收到正确的数据;同样,服务端接收到客户端发送的数据时,必须按照帧的格式来解包,才能真确获取客户端发来的数据。

数据帧格式

  1. FIN

1个bit位,用来标记当前数据帧是不是最后一个数据帧,因为一个消息可能会分成多个数据帧来传递,当然,如果只需要一个数据帧的话,第一个数据帧也就是最后一个。

  1. RSV1, RSV2, RSV3

这三个,各占用一个bit位,根据RFC的介绍,这三个bit位是用做扩展用途,没有这个需求的话设置位0。

  1. Opcode

故名思议,操作码,占用4个bit位,也就是一个16进制数,它用来描述要传递的数据是什么或者用来干嘛的,只能为下面这些值:

  • 0x0 denotes a continuation frame 标示当前数据帧为分片的数据帧,也就是当一个消息需要分成多个数据帧来传送的时候,需要将opcode设置位0x0。
  • 0x1 denotes a text frame 标示当前数据帧传递的内容是文本
  • 0x2 denotes a binary frame 标示当前数据帧传递的是二进制内容,不要转换成字符串
  • 0x8 denotes a connection close 标示请求关闭连接
  • 0x9 denotes a ping 标示Ping请求
  • 0xA denotes a pong 标示Pong数据包,当收到Ping请求时自动给回一个Pong
  • 目前协议中就规定了这么多,0x30x7以及0xB0xF都是预留作为其它用途的。
  1. MASK

占用一个bit位,标示数据有没有使用掩码,RFC中有说明,服务端发送给客户端的数据帧不能使用掩码,客户端发送给服务端的数据帧必须使用掩码。

如果一个帧的数据使用了掩码,那么在Maksing-key部分必须是一个32个bit位的掩码,用来给服务端解码数据。

  1. Payload len

数据的长度,默认位7个bit位。

如果数据的长度小于125个字节(注意:是字节)则用默认的7个bit来标示数据的长度。

如果数据的长度为126个字节,则用后面相邻的2个字节来保存一个16bit位的无符号整数作为数据的长度。

如果数据的长度大于126个字节,则用后面相邻的8个字节来保存一个64bit位的无符号整数作为数据的长度。

  1. Masking-key

数据掩码,如果MASK设置位0,则该部分可以省略,如果MASK设置位1,怎Masking-key位一个32位的掩码。用来解码客户端发送给服务端的数据帧。

  1. Payload data

该部分,也是最后一部分,是帧真正要发送的数据,可以是任意长度。

解析数据帧

js
// 当读取socket的数据不足以解析成一个完整的消息时, 存到cacheData, 等待下一次读取socket数据
let cacheData = [];
function decodeDataFrame(socket, data) {
  if (cacheData.length > 0) {
    data = Buffer.concat([cacheData, data]);
    cacheData = [];
  }
  // 数据长度不足以继续解析, 缓存数据
  // 一个Socket数据帧至少2个字节
  if (data.length < 2) {
    cacheData = data;
    return;
  }
  let index = 0;
  const firstByte = data.readUInt8(index++);
  const secondByte = data.readUInt8(index++);
  // 参考数据帧格式图进行理解
  const frame = {
    fin: (firstByte >> 7),
    rsv: (firstByte >> 4) & 0x7,
    opcode: firstByte & 0xf,
    mask: (secondByte >> 7),
    payloadLen: secondByte & 0x7f, // & 01111111
    maskingKey: null,
  }
  if (frame.payloadLen >= 126) {
    // payloadLen等于126,说明后2个字节存储数据长度
    if (frame.payloadLen === 126) {
      // 数据长度不足以继续解析, 缓存数据
      if (data.length < index + 2) {
        cacheData = data;
        return;
      }
      // 使用大端读取方式, 因为TCP数据传输使用的就是大端字节序(网络字节序)
      frame.payloadLen = data.readUInt16BE(index);
      index += 2;
    }
    // payloadLen等于127,说明后8个字节存储数据长度
    if (frame.payloadLen === 127) {
      if (data.length < index + 8) {
        cacheData = data;
        return;
      }
      // 一般情况下, 数据长度不会超过2^32次,所以只读后四个字节即可
      data.readUInt32BE(index);
      index += 4;
      frame.payloadLen = data.readUInt32BE(index);
      index += 4;
    }
  }
  if (frame.mask) {
    // 数据长度不足以继续解析, 缓存数据
    // 4 为掩码数据对应的字节数.
    if (data.length < index + 4 + frame.payloadLen) {
      cacheData = data;
      return;
    }
    const parsedBytes = [];
    // 获取掩码数据
    const maskByte = [data[index++], data[index++], data[index++], data[index++]];
    // 解析数据
    for (let i = 0; i < frame.payloadLen; i++) {
      parsedBytes.push(data[index++] ^ maskByte[i % 4]);
    }
    // 解析完成, 合成数据
    frame.payloadBuffer = Buffer.from(parsedBytes);
  } else {
    // 数据长度不足以继续解析, 缓存数据
    if (data.length < index + frame.payloadLen) {
      cacheData = data;
      return;
    }
    // 截取数据
    frame.payloadBuffer = data.subarray(index, index + frame.payloadLen);
    // 如果还有多余的数据, 继续解析
    if (data.length - index - frame.payloadLen > 0) {
      return Buffer.concat([frame.payloadBuffer, decodeDataFrame(socket, data.subarray(index + frame.payloadLen))]);
    }
  }
  return frame.payloadBuffer;
}

生成关键帧

js
function encodeDataFrame(config) {
  const bufArr = [];
  const firstByte = Buffer.alloc(1);
  const secondByte = Buffer.alloc(1);
  bufArr.push(firstByte, secondByte);
  // 往第一个字节写入数据:FIN + rsv1 + rsv2 + rsv3 + opcode
  firstByte.writeUInt8((config.fin << 7) + (config.rsv << 4) + config.opcode);
  // mask + payloadLen
  // 获取要发送的消息体长度
  const payloadLen = config.payloadBuffer.length;
  if (payloadLen > 125) {
    if (payloadLen > 65535) {
      // 2 ^ 7 - 1 = 127 : 01111111 通过最后一位来判断用多少字节存储数据
      // 消息体长度大于65535,需要用8字节表示
      secondByte.writeUInt8((config.mask << 7) + 127); // 头部第二个字节
      const lenByte = Buffer.alloc(8);
      lenByte.writeUInt32BE(0) // 因为4个字节可表示的消息体长度已经达到2^32次, 所以前四个字节设为0, 
      lenByte.writeUInt32BE(payloadLen.length, 4);
      bufArr.push(lenByte);
    } else {
      // 2 ^ 7 - 2 = 126 : 01111110 通过最后一位来判断用多少字节存储数据
      // 消息体长度小于65535,需要用2字节表示
      secondByte.writeUInt8((config.mask << 7) + 126); // 头部第二个字节
      const lenByte = Buffer.alloc(2);
      lenByte.writeUInt16BE(payloadLen.length);
      bufArr.push(lenByte)
    }
  } else {
    // 消息体长度小于125,需要用1字节表示
    secondByte.writeUInt8((config.mask << 7) + payloadLen); // 头部第二个字节
  }
  // 服务器消息不需要掩码
  // payloadData
  bufArr.push(config.payloadBuffer);
  return Buffer.concat(bufArr);
}

服务端推送数据

客户端代码:

html
<body>
  <div>learn ws</div>
  <script>
    const ws = new WebSocket('ws://localhost:8080');
    ws.onopen = function () {
      console.info('connected');
      // 连接完成后, 向服务器发送数据
      ws.send(JSON.stringify({
        msg: 'hello ws!'
      }));
    };
    ws.onmessage = (msg) => {
      console.info(JSON.parse(msg.data));
    }
  </script>
</body>

服务端代码:

js
const net = require('net');
const { encodeDataFrame, decodeDataFrame } = require('./handleFrame');

const server = net.createServer(socket => {
  // 说明TCP连接已建立
  console.log('tcp client connected');
  socket.on('data', data => {
    const dataString = data.toString();
    // 匹配Sec-WebSocket-Key字段,判断是否是websocket协议转化的握手包
    const key = getWebSocketKey(dataString);
    if (key) {
      // 判断是否是websocket协议切换握手包
    } else {
      // websocket数据帧
      // 解码数据帧
      const res = decodeDataFrame(socket, data);
      // 解码后,触发自定义用户事件,把从客户端来的数据返回给客户端
      socket.emit('message', res);
    }
  });

  socket.on('message', (msg) => {
    console.info('收到客户端的信息:', msg.toString());
    // 给客户端发送消息,生成数据帧
    socket.write(encodeDataFrame({
      fin: 1, // 用来标识这是消息的最后一段, 一个消息可能分成多段发送
      rsv: 0,  // 默认是0, 用来设置自定义协议, 设置的话双方都必须实现
      opcode: 1, // 操作码, 用来描述该段消息
      mask: 0, // 标识是否需要根据掩码做混淆息计算, 如果为1, 那么会有4个字节存储掩码, 服务器向客户端发送数据不用做混淆运算
      payloadBuffer: msg // 消息的内容
    }))
  })

  socket.on('end', () => {
    console.log('client disconnected');
  })
})

server.listen(8080, () => {
  console.log('start in: localhost:8080')
});

MIT Licensed | Copyright © 2021 - 2022