基于net模块,从零实现websocket(ws模块)

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于net模块,从零实现websocket(ws模块)相关的知识,希望对你有一定的参考价值。

Websocket

  • Websokcet是H5开始提供的一种浏览器与服务器进行全双工通讯的网络技术
  • 通俗的讲,就是在客户端和服务器有一个持久的链接,两边可以在任意时间开始发送数据。
  • 属于应用层协议,它是基于TCP传输协议的,并复用HTTP的握手通道。

websocket连接

  • websocket服用了http的握手通道。具体就是,客户端通过http请求,与websocket服务端协商升级协议,协议升级完毕之后,后续的数据交换则遵守Websocket的协议。


  • 请求体中Upgrade表示升级为websocket协议,Scc-WebSocket-Key发送key,Sec-WebSocket-Version表示版本号
  • 响应体中,Connection表示同意升级,Sec-WebSocket-Accept是根据客户端发送的key计算出来的。

    先走HTTP协议,再走webSocket协议。
客户端,申请协议升级
  • 首先客户端发起协议升级请求
  • 请求采用的是标准的HTTP报文格式,且只支持GET方法.
GET ws://localhost:8080/ HTTP/1.1
Connection: Upgrade
Host: localhost:8080
Origin: http://localhost:3000
Sec-WebSocket-Key: hGDHklcMkYR51H2A17ikxw== //提供key,服务端根据key计算出sec-websocket-accept,提供基本的防护。比u防止恶意连接。
Sec-WebSocket-Version: 13 //版本
Upgrade: websocket //表示升级协议
服务端:响应协议升级
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: vbhpq1GJDvx0/yZjL+KV8LArKlo=
Upgrade: websocket

状体码101表示协议切换。

Sec-Websocket-Accept计算
const crypto = require('crypto')
const CODE = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" //固定的code
function toAcceptKey(wsKey)
    return crypto.createHash('sha1').update(wsKey+CODE).digest('base64')

数据帧格式

  • websocket客户端,服务端通信的最小单位是帧,由1个或者多个帧组成一条完整的消息。
  • 发送端将消息切割成多个帧,并发送给服务端。
  • 接收端,接收消息帧,并将关联的帧重新组装成完成的信息。
bit和byte
  • 1比特就是一个位(bit),位是数据存储的最小单元。
  • 一个字节,也就是一个byte,就等于8个bit。
  • 一个英文字母是1字节
  • 一个中文汉字是2字节。
位运算符
// 按位 与& 两个数同为1,结果才是1
const A = 0b11110000;
const B = 0b00001111;
const C = 0b11110000
console.log((A & C).toString(2));  11110000

// 按位 或|  两个数只要有一个为1,结果就是1
console.log((A | B).toString(2));   11111111
//按位 或^ 两个数有一个不同,结果就是1,都相同,结果就是0
console.log((A ^ B).toString(2));  11111111
一个数据帧格式
  • 单位是bit,比如FIN RSV1各占1比特,opcode占4比特
  • FIN,1个比特,如果是1,表示这是所有分片中最后一个分片了,如果是0,就表示后续还有分片。
  • RSV ,各占1比特,保留位。一般是0
  • opcode 4个比特,操作代码,由4个比特,.每一个值都有特殊的含义。

    这里主要注意%x1文本帧,%x2二进制帧。
  • Mask: 1个比特,这个是指明“payload data”是否被计算掩码,客户端发送给服务端的时候需要掩码操作,Mask为1,并且有一个Masking-key。服务端发送给客户端的数据,Mask为0,不需要掩码。
  • Payload len,数据的长度
  • Masking-key:0或4给二字节。所有从客户端传送到服务端的数据帧,数据载荷都做了掩码操作,MASK为1,且携带了Masking-key。如果MASK为0,则表示没有Masking-key
  • Payload data,帧真正要发送的数据,可以是任意长度,但尽管理论上帧的大小没有限制,但发送的数据不能太大,否则会导致无法高效利用网络带宽,正如上面所说Websocket提供分片。
Payload len

数据的长度计算,也是特殊的。

  • pl = x,为0-125的时候,数据的长度就是x字节。
  • pl = x,为126的时候,后续两个字节代表一个16位的无符号整数。该无符号整数的值为数据的长度。
  • pl = x 为127,后续8个字节代表一个64的无符号整数。该无符号整数的值为数据的长度。
  • pl如果占用了多个字节,pl的二进制表达采用网络序(bg endian,重要的位在前。)
  • 大端序和小端序。大端序在前,小端序在后。

function getLength(buffer) 
  const byte = buffer.readUInt8(0); //Buffer對象讀取一個無符號的8位整數,0表示从0开始读取。就是将一个字节转为10进制,变成10进制
  const str = byte.toString(2); //变成2进制
  // 截取掉第一位, mark标记
  let length = parseInt(str.substring(1), 2);

  // 如果x < 125,那么x就是数据的长度,
  if (length < 125) 
   else if (length === 126) 
    // 126的话 后续两个字节代表一个16位的无符号整数。该无符号整数的值为数据的长度。
    length = buffer.readUInt16BE(1); //跳过第一位开始读取大端序16位整数,也就是参数后面两个 0b00000000, 0b00000001
   else 
    //最大值就只有127了,因为2的7次方-1是127,而payload length只有7个字节。
    // 后续8个字节代表一个64的无符号整数。该无符号整数的值为数据的长度。
    length = buffer.readBigUInt64BE(1);
  
  return length;


console.log(getLength(Buffer.from([0b00011110, 0b00000000, 0b0000001])));

getLength接受一个buffer,他会去掉前一个bit,也就是Mark标记,然后计算7个bit的长度,根据长度计算出payload length的数值。

掩码算法

当Mark为1的时候,就需要进行掩码操作。

  • 掩码键也就是Masking-key是由客户端挑选出来的,32bit的随机数,掩码操作不会影响数据长度。
  • 掩码和反掩码操作都采用了如下算法:对索引i模以4得到结果,并对原来的索引进行异或操作(简答地说就是每个4数比较)。
数据: 				0101
masking-key: 		1010  
结果就是:			1111	(数据 异或^ masking-key)
masking-key: 		1010
还原:				0101	(结果 异或^ masking-key)
function unmask(buffer, mask) 
  for (let i = 0; i < buffer.length; i++) 
    buffer[i] ^= mask[i % 4];
  
  return buffer


const mask = Buffer.from([1, 0, 1, 0]);
const buffe1r = Buffer.from([0,1,0,1,0,1,0,1]);
console.log(unmask(buffe1r, mask));

实现ws模块。

  • 基础TCP传输层协议,实现一个websocket服务器。
第一步,升级协议
class Server extends EventEmitter 
  constructor(options) 
    super(options);
    this.options = options;
    //创建TCP服务器,只管消息传输,不管解析
    this.server = net.createServer(this.listener);
    this.port = options.port || 8888;
    this.server.listen(this.port, () => 
      console.log(`the server is running $this.port`);
    );
  
  listener = (socket) => 
    // socket是一个套接字,就是用它来发送和接收消息。
    //保持长连接
    socket.setKeepAlive(true);
    // 实现ws的message和send事件
    socket.on("data", (chunk) => 
      chunk = chunk.toString();
      // Chunk是包含请求头和请求体的字符串
      /**
       * chunk 就是
       * GET / HTTP/1.1\\r\\n
       * Host: xxx \\r\\n
       * ...
       * Sec-WebSocket-Key: xxx \\r\\n
       * .... \\r\\n
       * \\r\\n
       *
       * 请求体
       */
      // 接手到客户端发送给服务器的数据之后
      //如果有Upgrade: websocket,表示客户端需要升级协议
      if (chunk.toString().match(/Upgrade: websocket/)) 
        this.upgradeProtocol(socket, chunk);
      
    );

    //连接成功之后触发connection事件,并且传递socket对象
    this.emit("connection", socket);
  ;

  // 升级协议
  upgradeProtocol = (socket, chunk) => 
    const rows = chunk.split("\\r\\n").slice(1, -2); //第一行不要,最后一个不要
    const headers = toHeaders(rows); //获取请求头对象
    const wsKey = headers["Sec-WebSocket-Key"];
    const acceptKey = toAcceptKey(wsKey);
    /** 响应体
        HTTP/1.1 101 Switching Protocols\\r\\n
        Connection: Upgrade\\r\\n
        Sec-WebSocket-Accept: vbhpq1GJDvx0/yZjL+KV8LArKlo=\\r\\n
        Upgrade: websocket\\r\\n
       */
    const response = [
      "HTTP/1.1 101 Switching Protocols",
      "Connection: Upgrade",
      `Sec-WebSocket-Accept: $acceptKey`,
      "Upgrade: websocket",
      "testHeader: myWs",
      "\\r\\n",
    ].join("\\r\\n");
    socket.write(response); //返回给客户端
  ;

通过net模块创建TCP连接,然后根据请求头是否包含Upgrade决定要不要升级协议,如果升级,调用upgradeProtocol,解析请求体,获取key,然后制造响应体,返回。结果:

正常连接成功,协议升级完毕。

第二部,解析ws数据帧,触发sockek.on(‘message’)事件
socket.on("data", (chunk) => 
      const firstChunk = chunk.toString();
      // firstChunk是包含请求头和请求体的字符串
      /**
       * chunk 就是
       * GET / HTTP/1.1\\r\\n
       * Host: xxx \\r\\n
       * ...
       * Sec-WebSocket-Key: xxx \\r\\n
       * .... \\r\\n
       * \\r\\n
       *
       * 请求体
       */
      // 接手到客户端发送给服务器的数据之后
      //如果有Upgrade: websocket,表示客户端需要升级协议
      if (firstChunk.match(/Upgrade: websocket/)) 
        this.upgradeProtocol(socket, firstChunk);
       else 
        // 如果没有,就是已经建立了ws连接,正常传送数据了
        this.onmessage(socket, chunk);
      
    );

如果没有Upgrade,表示已经建立起了ws连接,需要处理数据帧了。

 onmessage = (socket, chunk) => 
    // ws通信单位是数据帧,开始解析
    // 与是两个都是1才会是1,如果第一个是1,那么结果是0b00000000,如果第一个是0,那么结果是 0b00000001
    let FIN = (chunk[0] & 0b10000000) === 0b10000000;
    const opcode = chunk[0] & 0b00001111; // 取出后四位,得到操作码的十进制
    const masked = (chunk[1] & 0b10000000) === 0b10000000; //是否需要掩码
    const payloadLenght = chunk[1] & 0b01111111; // 取出后7为数据长度
    let payload;
    if (masked) 
      // 假设payloadLength长度小于126
      const maskingKey = chunk.slice(2, 6);
      payload = chunk.slice(6, 6 + payloadLenght);
      payload = unmask(payload, maskingKey); //经过反掩码拿到真实数据
     else 
      payload = chunk.slice(6, 6 + payloadLenght);
    
    if (FIN) 
      //如果为结束帧
      switch (opcode) 
        case 1:
          // 文本
          socket.emit("message", payload.toString("utf8"));
          break;
        case 2:
          socket.emit("message", payload);
          // 二进制帧
          break;
        default:
          break;
      
    
  ;

这里只对文本和二进制做处理,如上,先解析数据帧,得到对应的字段,然后通过判断,反掩码等等,获取到最终客户端发送的payload,然后触发message事件。
效果:

// 服务端
wsServer.on("connection", (socket) => 
  //socket是套接字;
  socket.on("message", (message) => 
    console.log("客户端发送的message", message);
  );
);

客户端

正常接收到了。

第三步,实现socket.send事件。

send事件会发送数据给客户端,所以需要自己拼接数据帧。


    socket.send = function (payload) 
      let opcode = Buffer.isBuffer(payload) ? 2 : 1;
      payload = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
      let length = payload.length;
      let buffer = Buffer.alloc(length + 2); //响应体长度,需要再加2个字节是因为,数据帧固定前面有两个字节做标识。就是FIN,opcode那些
      buffer[0] = 0b10000000 | opcode; // 第一个字节的内容共,FIN+opcode
      buffer[1] = length; //第二个字节放长度,因为第二个字节的第一个bit是Mark,服务端发送的mark为0.
      payload.copy(buffer, 2); // 将payload拷贝到buffer的第二位开始
      console.log('buffer',buffer.toString());
      socket.write(buffer);
    ;
  • 创建数据帧较为容易,因为不需要MaskingKey,也不需要Mark,更不需要根据payloadLength考虑后面几个字节的事情。
  • 直接创建一个buffer,拼死FIN和opcode,加上length,再拼上数据即可返回。效果:


    客户端正常收到。
ws全部代码:
const net = require("net"); //实现TCP协议的模块

const  EventEmitter  = require("events");
const 
  unmask,
  getLength,
  toAcceptKey,
  toHeaders,
 = require("../toAcceptKey.js");
const crypto = require("crypto");

// node很多库都是继承EventEmitter,实现事件订阅发布,解耦
class Server extends EventEmitter 
  constructor(options) 
    super(options);
    this.options = options;
    //创建TCP服务器,只管消息传输,不管解析
    this.server = net.createServer(this.listener);
    this.port = options.port || 8888;
    this.server.listen(this.port, () => 
      console.log(`the server is running $this.port`);
    );
  
  listener = (socket) => 
    // socket是一个套接字,就是用它来发送和接收消息。
    //保持长连接
    socket.setKeepAlive(true);
    // 实现ws的message和send事件
    socket.on("data", (chunk) => 
      const firstChunk = chunk.toString();
      // firstChunk是包含请求头和请求体的字符串
      /**
       * chunk 就是
       * GET / HTTP/1.1\\r\\n
       * Host: xxx \\r\\n
       * ...
       * Sec-WebSocket-Key: xxx \\r\\n
       * .... \\r\\n
       * \\r\\n
       *
       * 请求体
       */
      // 接手到客户端发送给服务器的数据之后
      //如果有Upgrade: websocket,表示客户端需要升级协议
      if (firstChunk.match(/Upgrade: websocket/)) 
        this.upgradeProtocol(socket, firstChunk);
       else 
        // 如果没有,就是已经建立了ws连接,正常传送数据了
        this.onmessage(socket, chunk);
      
    );

    //连接成功之后触发connection事件,并且传递socket对象
    this.emit("connection", socket);

    socket.send = function (payload) 
      let opcode = Buffer.isBuffer(payload) ? 2 : 1;
      payload = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
      let length = payload.length;
      let buffer = Buffer.alloc(length + 2); //响应体长度,需要再加2个字节是因为,数据帧固定前面有两个字节做标识。就是FIN,opcode那些
      buffer[0] = 0b10000000 | opcode; // 第一个字节的内容共,FIN+opcode
      buffer[1] = length; //第二个字节放长度,因为第二个字节的第一个bit是Mark,服务端发送的mark为0.
      payload.copy(buffer, 2); // 将payload拷贝到buffer的第二位开始
      console.log('buffer',buffer.toString());
      socket.write(buffer);
    ;
  ;

  onmessage = (socket, chunk) => 
    // ws通信单位是数据帧,开始解析
    // 与是两个都是1才会是1,如果第一个是1,那么结果是0b00000000,如果第一个是0,那么结果是 0b00000001
    let FIN = (chunk[0] & 0b10000000) === 0b10000000;
    const opcode = chunk[0] & 0b00001111; // 取出后四位,得到操作码的十进制
    const masked = (chunk[1] & 0b10000000) === 0b10000000; //是否需要掩码
    const payloadLenght = chunk[1] & 0b01111111; // 取出后7为数据长度
    let payload;
    if (masked) 
      // 假设payloadLength长度小于126
      const maskingKey = chunk.slice(2, 6);
      payload = chunk.slice(6, 6 + payloadLenght);
      payload = unmask(payload, maskingKey); //经过反掩码拿到真实数据
     else 
      payload = chunk.slice(6, 6 + payloadLenght);
    
    if (FIN) 
      //如果为结束帧
      switch (opcode) 
        case 1:
          // 文本
          socket.emit("message", payload.toString("utf8"));
          break;
        case 2:
          socket.emit("message", payload);
          // 二进制帧
          break;
        default:
          break;
      
    
  ;

  // 升级协议
  upgradeProtocol = (socket, chunk) => 
    const rows = chunk.split("\\r\\n").slice(1, -2); //第一行不要,最后一个不要
    const headers = toHeaders(rows); //获取请求头对象
    const wsKey = headers["Sec-WebSocket-Key"];
    const acceptKey = toAcceptKey(wsKey);
    /** 响应体
        HTTP/1.1 101 Switching Protocols\\r\\n
        Connection: Upgrade\\r\\n
        Sec-WebSocket-Accept: vbhpq1GJDvx0/yZjL+KV8LArKlo=\\r\\n
        Upgrade: websocket\\r\\n
       */
    const response = [
      "HTTP/1.1 101 Switching Protocols",
      "Connection: Upgrade",
      `Sec-WebSocket-Accept: $acceptKey`,
      "Upgrade: websocket",
      "testHeader: myWs", //用于测试的头部
      "\\r\\n",
    ].join("\\r\\n");
    socket.write(response); //返回给客户端
  ;


module.exports =  Server ;

代码仓库在:https://gitee.com/fine509/websocket

以上是关于基于net模块,从零实现websocket(ws模块)的主要内容,如果未能解决你的问题,请参考以下文章

node+ws模块实现websocket

❤️一个聊天室案例带你了解Node.js+ws模块是如何实现websocket通信的

❤️一个聊天室案例带你了解Node.js+ws模块是如何实现websocket通信的!

❤️一个聊天室案例带你了解Node.js+ws模块是如何实现websocket通信的!

Apache配置WebSocket代理

Python测试基于websocket协议的即时通讯接口多测师