前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >engine.io-client原理分析

engine.io-client原理分析

作者头像
theanarkh
发布2021-05-08 16:03:31
4860
发布2021-05-08 16:03:31
举报
文章被收录于专栏:原创分享原创分享

首先我们看一下入口

代码语言:javascript
复制
module.exports = (uri, opts) => new Socket(uri, opts);

我们看到主要是新建了一个Socket对象。接下来看一下Socket对象初始化的逻辑。

代码语言:javascript
复制
constructor(uri, opts = {}) {
   // 忽略一系列参数初始化
   this.open();
 }

初始化系列参数后,执行了open函数。

代码语言:javascript
复制
open() {
    // 只分析websocket
    let transport = "websocket";;
    this.readyState = "opening";
    transport = this.createTransport(transport);
    transport.open();
    this.setTransport(transport);
}

open函数主要的逻辑是创建了一个底层的通道,比如websocekt、xhr。这里以websocket为例。创建通道只是新建了一个js对象,还需要主动“打开”它。最后设置Socket的底层数据通道为新建的通道。我们首先看一下通道的设计。为了兼容,engine.io支持了多种底层通道,比如原生的websocket、xhr,jsonp等等。在设计上使用了面向对象的思想。基类逻辑不多,主要是定义了通用逻辑,具体实现在子类里实现。后面我们慢慢分析。言归正传,新建了一个websocket对象后,执行了open函数。open函数是在通道基类里定义的,接着执行了子类的doOpen。

代码语言:javascript
复制
doOpen() {
    try {
      this.ws = new WebSocket(uri, protocols);
    } catch (err) {
      return this.emit("error", err);
    }

    this.addEventListeners();
  }

 addEventListeners() {
    const self = this;

    this.ws.onopen = function() {
      self.onOpen();
    };
    this.ws.onclose = function() {
      self.onClose();
    };
    this.ws.onmessage = function(ev) {
      self.onData(ev.data);
    };
    this.ws.onerror = function(e) {
      self.onError("websocket error", e);
    };
  }

doOpen主要是新建了一个原生websocket对象(发起一个websocket连接),然后监听其各种事件。transport.open()就结束了。接着分析this.setTransport(transport)。

代码语言:javascript
复制
setTransport(transport) {
    const self = this;
    // 设置socket的底层消息通道
    this.transport = transport;
    // 监听底层通道的各种事件
    transport
      .on("drain", function() {
        self.onDrain();
      })
      .on("packet", function(packet) {
        self.onPacket(packet);
      })
      .on("error", function(e) {
        self.onError(e);
      })
      .on("close", function() {
        self.onClose("transport close");
      });
  }

在open的时候,websocke通道监听了原生websocket的事件,socket又监听了websocket通道的事件,一层套一层,如图所示。

至此,一切准备就绪,等待websocket连接成功。当连接成功后,服务端会发送一个数据包给客户端(socketio相关的,和底层websocket协议没关系)。我们看一下websocket收到数据时的处理逻辑。

代码语言:javascript
复制
this.ws.onmessage = function(ev) {
     self.onData(ev.data);
};

function onData(data) {
    const packet = parser.decodePacket(data, this.socket.binaryType);
    this.onPacket(packet);
}

onPacket(packet) {
   this.emit("packet", packet);
}

在websocket之上,engineio有自己的协议,收到websocket上抛的数据时,engineio首先根据自己的协议进行解析。如下图所示。

下面我们看看engineio协议(在engine.io-parser中实现)。engineio定义了五种数据包类型。

代码语言:javascript
复制
const PACKET_TYPES = Object.create(null); // no Map = no polyfill
PACKET_TYPES["open"] = "0";
PACKET_TYPES["close"] = "1";
PACKET_TYPES["ping"] = "2";
PACKET_TYPES["pong"] = "3";
PACKET_TYPES["message"] = "4";

为了方便,这里以nodejs端的代码为例子。

代码语言:javascript
复制
const encodePacket = ({ type, data }, supportsBinary, callback) => {
  if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
    // 转成Buffer
    const buffer = toBuffer(data);
    return callback(encodeBuffer(buffer, supportsBinary));
  }
  // plain string
  return callback(PACKET_TYPES[type] + (data || ""));
};

// only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data, supportsBinary) => {
  return supportsBinary ? data : "b" + data.toString("base64");
};

我们看到engineio协议是比较简单的。同样 ,当engineio层收到websocket上抛的数据时,就会按照一样的协议格式进行解析。最终触发packet事件,到达socket层。我们看看socket层的处理。

代码语言:javascript
复制
// Socket is live - any packet counts
      this.emit("heartbeat");

      switch (packet.type) {
        // 使用长轮询升级到websocket协议时,服务器返回open类型的包,开始继续握手
        case "open":
          this.onHandshake(JSON.parse(packet.data));
          break;
        // 心跳机制
        case "ping":
          this.resetPingTimeout();
          this.sendPacket("pong");
          this.emit("pong");
          break;

        case "error":
          const err = new Error("server error");
          err.code = packet.data;
          this.onError(err);
          break;
        // 上抛给engineio上层
        case "message":
          this.emit("data", packet.data);
          this.emit("message", packet.data);
          break;
      }

这里主要分析open包的分支,当websocket连接成功后,服务端会push一个open包。比如

代码语言:javascript
复制
0{"sid":"qp_YtPOEPMybnTuuAAK_","upgrades":[],"pingInterval":25000,"pingTimeout":30000}

从刚才engineio协议中我们可以看到,0是对应open类型。后面是engineio的数据部分。我们继续看onHandshake。

代码语言:javascript
复制
onHandshake(data) {
    this.id = data.sid;
    this.transport.query.sid = data.sid;
    // 判断可以升级到哪些协议,如果支持websocket,则不需要升级了
    this.upgrades = this.filterUpgrades(data.upgrades);
    // 心跳机制的配置
    // 多久服务器会ping一次
    this.pingInterval = data.pingInterval;
    // 超过pingInterval还没ping,再过多久则认为超时了,关闭连接
    this.pingTimeout = data.pingTimeout;
    // 再open一次,和新的协议握手,如果支持websocket,则不需要升级了
    this.onOpen();
    // In case open handler closes socket
    if ("closed" === this.readyState) return;
    // 开启心跳机制
    this.resetPingTimeout();
  }

  resetPingTimeout() {
    clearTimeout(this.pingTimeoutTimer);
    this.pingTimeoutTimer = setTimeout(() => {
      this.onClose("ping timeout");
    }, this.pingInterval + this.pingTimeout);
  }

至此,engineio的处理过程就完成了,剩下的就是上层的数据通信了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档