乐趣区

关于前端:浅尝IM方案在H5活动场景中的应用

本文作者:入云

前言

说起 IM,大家应该都或多或少理解过一些,个别被熟知是在一些聊天场景里利用的比拟多;而个别状况下咱们常接触的业务中大多是做一些接口的查问提交之类的操作,用失常的 Ajax 申请就足以满足需要,比拟难接触到 IM 这种计划。

但如果波及到一些须要频繁更新数据的业务场景,应用惯例接口查问难免会给服务端造成比拟大的性能开销,并且数据更新的提早也会很大;尝试应用 IM 则能够让咱们在业务开发中更好地应答频繁的数据更新场景,以晋升用户体验和业务价值。

近期在做一个多人实时打怪兽的场景,即多名玩家同时攻打一个怪兽,任意一个玩家攻打怪兽,其它玩家须要实时感知到怪兽的状态更新,比方怪兽血量和玩家挫伤排行等信息。本文将从此需要切入,探讨下在相似这种高并发、低提早的业务需要中,如何应用 IM 计划来解决频繁的数据更新问题,也会顺便介绍下 WebSocket 的根本运作流程。

可选的数据更新计划

在议论 IM 之前,对于数据的实时更新,除了应用 IM,还有哪些可选用的计划,可能包含但不限于上面几种:

接口轮询

接口轮询这种形式置信大家都很相熟,次要是应用通过定期发送 HTTP 申请来达到数据更新的形式,实现起来也比较简单,例如一些榜单数据的定时更新:

// 申请榜单接口
const refreshRank = (familyId) => {getMonsterDamageRank({ familyId}).then((res) => {setRank(res);
    }).catch((err) => {Toast.warn(err.message || '服务器忙碌');
    });
};

// 每 3 秒刷新一次接口
setInterval(() => {refreshRank(currentFamily.familyId);
}, 3000);

这里应用 setInterval 每隔 3 秒申请一次榜单数据,用来更新排行榜信息,通常用于实现一些要求数据更新绝对频繁,但又容许有肯定提早的场景;同时轮询也是一种实现起来最简略的计划,但它也有几个比拟大的毛病,比方:

  1. 带宽节约:轮询须要定期向服务器发送申请,即便服务端没有新数据可用,这将会造成大量的带宽和服务器资源节约。
  2. 提早高:数据的更新频率受轮询距离影响,如果轮询间隔时间过长,会导致数据更新的提早较高。
  3. 负载过高:要升高数据的提早,就必须进步接口轮询的频率,但轮询的频率过高,将会导致服务器负载过高,从而影响其余用户的体验。

接口长轮询

长轮询(Long Polling)是一种改良的轮询技术,它的次要思维是在客户端发送申请后,服务端放弃连贯关上,但并不立刻响应,而是在有新数据可用时才响应给客户端。当客户端接管到响应后,再次发动申请,以放弃连贯关上。

相比于传统的轮询,长轮询能够升高网络提早和服务器压力;因为长轮询的响应是异步的,服务器不须要在每个固定工夫距离内返回响应,这样能够缩小不必要的申请。同时,当服务器有新数据可用时,也能够立刻返回响应,从而进步数据的实时性。

如上图,长轮询的实现通常分为上面几个阶段:

  1. 客户端向服务器发动申请。
  2. 服务器接管到申请后,如果没有新数据可用,则放弃连贯关上。
  3. 服务器有新数据可用时,响应给客户端。
  4. 客户端接管到响应后,再次向服务器发动申请。
    ……

上面是应用 Node.js 实现的一个简略的长轮询服务端示例:

const http = require('http');
const messages = [];

// 开始每隔 1 秒查看下 messages 中是否有信息
function waitForNewMessages(response) {const intervalId = setInterval(() => {if (messages.length > 0) { // message 中有音讯之后返回响应
      response.writeHead(200, { 'Content-Type': 'application/json'});
      response.end(JSON.stringify(messages));
      clearInterval(intervalId);
    }
  }, 1000);

  setTimeout(() => { // 30 秒无数据,返回一个空数组
    clearInterval(intervalId);
    response.writeHead(200, { 'Content-Type': 'application/json'});
    response.end(JSON.stringify([]));
  }, 30000);
}

function handleRequest(request, response) {if (request.url === '/messages') {
    /** 申请到“/messages”时,如果有新音讯,则立刻向客户端发送响应
     *  否则,期待一段时间后再次查看是否有新音讯
     * */
    waitForNewMessages(response);
  } else {response.writeHead(404);
    response.end();}
}
const server = http.createServer(handleRequest);
server.listen(3000);

在下面的代码中,咱们应用 setInterval 函数每秒查看一次是否有新音讯。如果有新音讯,咱们立刻向客户端发送响应,并革除定时器。为了避免始终 pending,如果在 30 秒内没有新的音讯,咱们会向客户端发送一个空数组作为响应。这样,客户端就能够在收到新音讯时立刻更新页面。

对于长轮询的实现仍有许多细节须要留神,如连贯放弃、连贯断开重连等问题。此外,长轮询依然须要耗费大量的带宽和服务器资源,因为每个连贯都须要放弃关上状态,能够设想有很多个申请达到服务端,服务端须要开启多个异步来放弃链接在 pending 的状态。

SSE(Server-Sent Events)

SSE 也是一种浏览器与服务器之间实现实时通信的技术。它容许服务器向浏览器发送数据。在 SSE 中,浏览器可通过 EventSource API 来建设与服务器的连贯,并监听来自服务器的事件。服务器通过向客户端发送特定格局的数据(包含事件名称和数据),来触发浏览器的事件监听器。

上面同样应用 Nodejs 来实现一个 Demo:


// Server 端
const http = require('http');

const server = http.createServer((req, res) => {
  // 设置头部信息
  res.writeHead(200, {
    'Content-Type': 'text/event-stream', // 设置响应类型为 SSE
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*' // 容许跨域申请
  });

  // 发送数据到客户端
  setInterval(() => {res.write('data:' + new Date().toISOString() + '\n\n'); // 发送 SSE 音讯
  }, 1000);
});

server.listen(3000, () => {console.log('Server started on port 3000');
});

// Client 端
const sse = new EventSource('http://localhost:3000');

// 监听 SSE 音讯
sse.addEventListener('message', (event) => {console.log(event.data);
});

在客户端应用 new EventSource 拜访“http://localhost:3000”时,服务器会返回一个 SSE 流,这里留神须要将响应头中的 Content-Type 设置为 text/event-stream,示意该响应是 SSE 流;将 Cache-Control 设置为 no-cache,示意浏览器不缓存该响应,Connection 设置为 keep-alive,示意服务器与客户端之间的连贯应该放弃关上状态。

在 SSE 流中,每一条音讯都须要以 data: 结尾,并以两个换行符(\n\n)结尾。在本例中,应用 setInterval() 函数每秒发送一条音讯。

但对于 SSE 而言,也具备上面几个毛病:

  1. 单向传输:只能从服务器向客户端推送数据,无奈实现双向通信。
  2. 只反对纯文本:事件流只能传输一个简略的文本数据流, 并且文本只能应用 UTF-8 格局编码
  3. SSE 对于一些浏览器的反对不够欠缺:比方在 Safari 和 iOS 中,可能会对 SSE 连贯的数量和连接时间等方面进行限度,从而影响 SSE 的稳定性和可靠性

HTTP/2 Server Push

绝对于 HTTP/1.1 而言,HTTP/2 其实也是反对了服务端被动推送的,不过目前 HTTP/2 的被动推送,次要是用于晋升页面加载性能的,它容许服务器在响应申请时向客户端推送事后缓存的资源(例如,CSS、JavaScript 和图像),以缩小申请次数和提早,是一种页面加载的优化伎俩。但思考到其绝对于 WebSocket 而言,目前的安全性和稳定性还有待进一步晋升,用于实现即时通信还不是特地成熟,所以这里就不再赘述了。对与如何实现提前推送动态文件,具体能够参考下 HTTP/2 服务器推送(Server Push)教程。

WebSocket

下面介绍的几种形式,都是基于 HTTP 协定的,而 WebSocket 则是一种新的协定;WebSocket 诞生于 2008 年 6 月,在 2011 年 12 月成为 RFC6455 国际标准,并且 WebSocket 协定是一种专门为实时通信而设计的协定。所以对于实现即时通信而言,WebSocket 能够说是最佳抉择。绝对于下面几种形式,它具备上面几个长处:

  1. 低提早:WebSocket 通过放弃长久连贯,防止了 HTTP 短连贯频繁地建设和敞开连贯的开销,从而升高了提早。
  2. 双向通信:WebSocket 协定反对双向通信,客户端和服务器都能够向对方发送数据,从而实现更加灵便的通信形式。
  3. 跨域反对:WebSocket 协定反对跨域通信,能够在不同源之间传输数据,从而反对更多种场景下的利用。
  4. 更少的数据传输:WebSocket 协定反对二进制数据传输和数据压缩,能够缩小数据传输的提早和带宽耗费。

说到底,下面提到了好几种计划,其实都能够在不同水平上实现数据的实时更新,然而它们跟本次需要中应用到的 IM 计划有什么关系呢?或者说 IM 到底是个什么样的计划呢?上面咱们先明确下 IM 的概念。

IM

IM 具体是指什么

即时通信(Instant Messaging,简称 IM)是一种透过网络进行实时通信的零碎,容许两人或多人应用网络即时的传递文字音讯、文件、语音与视频交换。通常以网站、电脑软件或挪动应用程序的形式提供服务(来自维基百科)

换句话说,咱们只有采纳某种形式,能实现两人或多人之间能够通过网络实时的替换信息,就能够称之为是一种 IM 计划。那么下面所提到的几种实现数据更新形式,都能够用做实现 IM 计划的底层实现计划。

Web 端 IM 的倒退历程

对于 Web 端 IM 的倒退历程,其实大抵都囊括了下面提到的几种实现形式;这些技术通过一直优化,继续晋升了用户体验。其演变过程能够大抵概括为从晚期的轮询技术到长轮询,再倒退到古代的 WebSocket、Server Push 的实现形式。而 WebSocket 的呈现,则实现了更高效、更实时的即时通信。

本次要实现多人打怪兽同步信息的场景,对数据更新的实时性要求十分高,所以本次需要所依赖的 IM 计划,就是基于更稳固的 WebSocket 实现的,所以上面就具体介绍下 WebSocket 和 HTTP 的区别,以及 WebSocket 的运作流程。

WebSocket

WebSocket VS HTTP

WebSocket 尽管是一种新的协定,但同 HTTP 协定一样,WebSocket 协定也是运行在 TCP 协定之上的,与 HTTP 协定同属于应用层网络数据传输协定。那 WebSocket 和 HTTP 到底有哪些不一样呢?

  1. HTTP 属于短连贯,每发动一次申请都须要建设一次连贯,申请完结后立刻敞开连贯,属于“申请 - 响应模式”,即客户端须要被动发送申请能力获取到服务器返回的数据。​即使是咱们下面介绍的“长轮询”,也是须要依赖服务端来“hold”住申请。
  2. HTTP 是一种无状态协定,每个申请都是独立的,服务器不会保留客户端的状态信息。所以每次客户端发送申请,都会在申请头里塞一些相似于 Cookie 这种信息用来标识以后申请属于哪个用户。
  3. 不同于 HTTP,WebSocket 协定中客户端和服务端只须要实现一次握手,两者之间就能够建设持久性的连贯,并能够进行双向的数据传输。

WebSocket 具体是如何建设连贯的

Demo 跑起来看着是挺简略的,但 WebSocket 长链接到底是怎么建设的呢?在介绍连贯建设之前,咱们先来理解下 HTTP 协定申请头中 Upgrade 这么一个字段。

HTTP 协定是一种文本协定,尽管其灵活性很高,但在解决大量数据和多媒体内容时效率较低。将协定降级为 WebSocket 或 HTTP/2 能够反对更多数据格式的传输;所以为了反对将协定降级,在 HTTP/1.1 中新增了 Upgrade 申请头,它容许客户端申请将其连贯降级到另一个协定:

Upgrade: <protocol>

其中,protocol 示意心愿降级到的协定名称,例如 WebSocket、HTTP/2 等。

另外,Upgrade 头部还能够与 Connection 头部一起应用,以批示客户端心愿应用长久连贯。这能够缩小每个申请的开销,从而进步网络性能和效率,要将协定降级为 WebSocket,就须要将这两个字段联合起来:

Connection: Upgrade
Upgrade: WebSocket

这里咱们理解到 WebSocket 协定是通过 HTTP 协定降级而来的,那么具体的长链接的生命周期是怎么的呢?上面是一个大抵的 WebSocket 流程图:

如上图,能够简略的将 WebSocket 的生命周期大抵分为三个阶段:

  1. 通过一次 HTTP 握手建设 WebSocket 长链接(也就是协定降级的过程)
  2. 应用 WebSocket 协定进行数据传输
  3. 任意一方发送敞开帧,对方响应敞开帧后,长链接敞开

握手申请

WebSocket 的建设是通过一次 HTTP 申请握手来实现的,客服端通过发送一个 GET 申请,并在 Request Header 里携带一些协定降级所需的参数,通知服务器对本次 HTTP 申请进行降级。

GET ws://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: nFPKUyeo5Ul58tbe7Dg5lA==

下面是一个 WebSocket 的申请快照,对于 Upgrade 字段下面曾经介绍过,这里看下剩下的几个要害的参数:

  • Sec-WebSocket-Key:是由客户端生成的一次性随机值,该值与服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供根本的防护,比方避免歹意或者无心的连贯。
  • Sec-WebSocket-Version:这里表明 WebSocket 协定的惟一可承受版本是 13。

握手响应

一旦客户端发送了关上 WebSocket 连贯的初始申请,它就会期待服务器的回复。该回复必须有一个 HTTP 101 切换协定的响应代码。HTTP 101 切换协定响应表明,服务器正在切换到客户端在其降级申请头中所指定的协定。同样的,在响应头里也会包含 Upgrade 字段,标识协定已被降级。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 89D1tEKizEJHFrVDhswIIpAf4ww=

此外,响应头中的 Sec-WebSocket-Accept 是一个解决后的 base64 编码,是通过客户端申请头中的 Sec-WebSocket-Key 和 RFC6455 中定义的动态值 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接来生成的,计算步骤为:

  1. Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接
  2. 通过 SHA1 计算出摘要,并转成 base64 字符串

通过这样一个 HTTP 的申请和响应,就表明长链接的握手过程曾经实现,并将协定升级成了 WebSocket 协定,后续单方就能够通过这个长链接通道传输数据了。那数据具体又是怎么传输的呢?

数据传输

WebSocket 在传输数据的过程中,实际上会将大块数据(音讯)分成若干帧进行传输,RFC6455 中给出了帧的概述,这里大抵介绍下一些重要字段:

  • FIN (Final):示意以后帧是否为最初一个片段;1 示意是音讯的最初一个片段,0 示意不是音讯的最初一个片段
  • RSV1, RSV2, RSV3 (Reserved):扩大字段,各占 1 比特,个别状况全为 0
  • opcode:每个帧都有一个操作码,这个操作码决定如何来解释这个帧的有效载荷数据。具体分为以下几个类型:

    链接敞开

    要敞开 WebSocket 连贯,发送端须要发送一个敞开帧(opcode 0x8)。如果连贯的任何一方收到一个敞开帧,它必须发送一个敞开帧作为响应,一旦单方都收到了敞开帧,WebSocket 连贯将会断开。

如何本人实现一个 WebSocket 的降级流程

依据下面的 WebSocket 的流程形容,咱们能够应用 Nodejs 实现一个简略版的协定降级逻辑,并应用浏览器 Api 实现对应的客户端逻辑。

服务端逻辑

以下是一个应用 Node.js 原生模块实现 WebSocket 服务端的例子:

// 导入所需的 Node.js 原生模块
const http = require('http');
const crypto = require('crypto');
// 创立 HTTP 服务器
const server = http.createServer();

// 解析 WebSocket 帧
function parseFrame(buffer) {
    // 这里获取操作码(opcode),示意数据帧的类型
    const opcode = buffer[0] & 0x0f;
    // 这行代码获取负载长度(payload length)。它示意数据帧的理论数据长度。这里仅思考了较短的数据长度,实际上可能须要解决更长的数据长度
    const payloadLength = buffer[1] & 0x7f; 
    // 数据帧的数据局部(payload)是以掩码的模式发送的,须要应用掩码来解码。const mask = buffer.slice(2, 6);
    // 这里获取帧中的理论数据
    const payload = buffer.slice(6);
    let decodedPayload = '';
    if (opcode === 1) { // 文本数据帧
        for (let i = 0; i < payloadLength; i++) {// 这里应用异或操作对数据字节与相应的掩码字节进行解码:payload[i] ^ mask[i % 4]。这里应用模运算(i % 4)确保在掩码的 4 个字节之间循环。// 应用 String.fromCharCode() 将解码后的字节转换为字符,并将解码后的字符增加到 decodedPayload 字符串中。decodedPayload += String.fromCharCode(payload[i] ^ mask[i % 4]);
        }
    } else if (opcode === 8) { // 敞开帧
        return {type: 'close'};
    }

    return {type: 'text', data: decodedPayload};
}

// 依据给定的文本音讯创立一个文本数据帧
function createTextFrame(message) {
    // 依据音讯长度调配一个缓冲区。这里仅解决较短的音讯,因而调配 2 个额定字节用于帧头
    const buffer = Buffer.alloc(2 + message.length);
    // 设置帧头的第一个字节。0x81 示意一个最终帧(FIN = 1)且操作码为文本(opcode = 1)buffer[0] = 0x81;
    // 设置帧头的第二个字节。这里仅解决较短的音讯,所以间接将音讯长度设置为负载长度。这意味着没有掩码(mask = 0)buffer[1] = message.length;
    // 将音讯写入缓冲区。对于每个字符,获取其字符编码(Unicode 编码)并将其增加到缓冲区。for (let i = 0; i < message.length; i++) {buffer[i + 2] = message.charCodeAt(i);
    }
    return buffer;
}

// 向客户端发送音讯
function sendTextMessage(socket, message) {const frame = createTextFrame(message);
    socket.write(frame);
}

// 监听服务器的 upgrade 事件
server.on('upgrade', (req, socket, head) => {
    // 查看 WebSocket 协定和版本
    if (req.headers['upgrade'] !== 'websocket' || req.headers['sec-websocket-version'] !== '13') {socket.destroy();
        return;
    }

    // 获取客户端发送的 Sec-WebSocket-Key
    const key = req.headers['sec-websocket-key'];

    // 计算 Sec-WebSocket-Accept
    const sha1 = crypto.createHash('sha1');
    sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
    const accept = sha1.digest('base64');

    // 构建响应头
    const headers = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept:' + accept,
        '\r\n'
    ];

    // 发送响应头
    socket.write(headers.join('\r\n'));

    // 监听数据
    socket.on('data', (buffer) => {const frame = parseFrame(buffer);

        if (frame.type === 'text') {console.log('Received message:', frame.data);
            // 在此处实现解决收到的文本音讯
            // 向客户端发送音讯
            sendTextMessage(socket, 'Hello, client!');
        } else if (frame.type === 'close') {console.log('Client closed the connection.');
            socket.destroy();}
    });

    // 监听敞开
    socket.on('close', () => {console.log('Socket has been closed.');
    });
});

server.listen(3000, () => {console.log('WebSocket server listening on port 3000');
});

这里创立了一个基于 HTTP 的 WebSocket 服务,并监听 3000 端口,其中细节局部曾经在代码里正文。当客户端发动降级申请时,服务器将在握手过程中验证客户端的申请,并在胜利降级到 WebSocket 连贯后监听来自客户端的数据同时发送一个 ‘Hello, client!’ 作为回复。

客户端逻辑

上面是对应客户端逻辑,应用浏览器原生 Api WebSocket 来实现长链接的建设:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>WebSocket demo</title>
</head>
<body>
  <h1>WebSocket demo</h1>
  <input type="text" id="message">
  <button onclick="sendMessage()">Send</button>

  <button onclick="handleEnd()">End</button>
  <div id="output"></div>
  <script>
    // 创立 WebSocket 连贯
    const ws = new WebSocket('ws://localhost:3000/');

    // 监听音讯
    ws.onmessage = (event) => {const output = document.getElementById('output');
      output.innerHTML += `<p>${event.data}</p>`;
    };

    // 长链接断开
    ws.onclose = () => {const output = document.getElementById('output');
        output.innerHTML += `<p>WebSocket closed</p>`;
    }

    // 发送音讯
    function sendMessage() {const message = document.getElementById('message').value;
      ws.send(message);
    }

    // 敞开长链接
    function handleEnd() {ws.close();
    }
  </script>
</body>
</html>

在客户端创立一个 WebSocket 并连贯到 ws://localhost:3000 服务器,这里注册了长链接的 onmessage 和 onclose 事件;在输入框里输出信息点击发送,会向服务端发送一个音讯,服务端在接管到客户端音讯时,紧接着会向客户端发送一个文本音讯,同时在页面中点击 end 能够将长链接敞开。

客户端:

服务端:

对于浏览器 WebSocket Api 除了罕用回调 onclose、onmessage、onerror、onopen,WebSocket 实例自身还有一些属性能够判断长链接以后的状态(如下图),具体参数可参考 MDN 中的具体介绍「WebSocket」

IM 计划抉择

下面咱们应用 Nodejs 和浏览器 WebSocket Api 实现了一个简略的即时通信,但这还远远达不到能够在生产环境中应用的规范,比方波及到网络异样、掉线重连、较大数据量解决、兼容性解决等问题。

当然 WebSocket 有很多成熟的库能够间接应用,比方 Socket.IO、Ws,这些库都是通过宽泛应用和测试的开源我的项目,具备良好的稳定性和可靠性。对于生产环境应用这些成熟的库能够缩小很多不必要的麻烦。作为本次需要的 IM 计划,咱们则抉择应用的是由 网易云信 提供的 Web IM 即时通讯能力;改计划提供了包含服务端和 Web 端一套残缺的计划,能够疾速集成到咱们的工程中,实现即时通信的能力。

云信 Web SDK 提供了多种常见聊天场景,例如单聊、群聊、聊天室等。本次需要次要波及多人场景,并且战斗不要求持久性,一场战斗由多人同时在线参加,并且在战斗完结后就遣散,所以这里抉择应用的是聊天室场景。对于音讯的流转如下:

客户端集成 SDK

整体计划的集成可参考云信官方网站,这里仅介绍下客户端的集成过程,和所须要留神的问题。

对于客户端这里抉择通过 npm 集成 SDK:

npm install @yxim/nim-web-sdk@latest

SDK 所蕴含的三个文件的阐明如下:

dist/SDK
├── NIM_Web_Chatroom.js       提供聊天室性能,浏览器适配版(UMD 格局)├── NIM_Web_NIM.js       提供 IM 性能,包含单聊、会话和群聊等,但不蕴含聊天室。浏览器适配版(UMD 格局)├── NIM_Web_SDK.js       提供 IM 性能和聊天室性能的集成包,浏览器适配版(UMD 格局)

这里应用的是聊天室能力,可通过单例模式初始化登陆聊天室,如下:

import Chatroom from '@yxim/nim-web-sdk/dist/SDK/NIM_Web_Chatroom';
export class InitChatRoom {static async getRoomInstance({ onChatMsg = () => {}}) {if (!InitChatRoom.instance) {
        InitChatRoom.instance = Chatroom.getInstance({
            appKey: 'appKey',          // 在云信治理后盾查看利用的 appKey
            account: 'account',        // 帐号, 利用内惟一
            token: 'token',            // 帐号的 token, 用于建设连贯
            chatroomId: 'chatroomId',  // 聊天室 id
            chatroomAddresses: [       // 聊天室地址列表
              'address1',
              'address2'
            ],
            onconnect: () => {},       // 长链接建设胜利回调
            onmsgs: (data) => {},      // 音讯触达回调
            ondisconnect: () => {},    // 长链接断开回调
            onwillreconnect: () => {}  // 长链接行将重连
        });
    }
    return InitChatRoom.instance;
  }
}

音讯的过滤

在本次需要中,怪兽血量和状态的更新次要是依赖音讯的推送。另外流动是每天定点凋谢,所以流动开始时会有大量用户同时涌入参加攻打怪兽,在这种状况可能会造成音讯在服务端的沉积,导致音讯触达到客户端时不能保障音讯是按产生的先后工夫达到的。

举个例子,比方 20:01 产生的音讯,因为音讯沉积可能会在 20:02 产生的音讯前面达到,这样就可能会导致怪兽的血量忽大忽小的跳动;或者是怪兽曾经死了,而怪兽掉血的音讯才刚刚达到,此时就须要将这些过期的音讯摈弃掉。

解决形式也比较简单,就是针对每一个音讯体都会增加一个音讯产生的工夫戳,通过这个工夫戳能够将提早触达的音讯过滤掉。

onmsgs: (data) => {    
    // 提早音讯的过滤,判断掉血音讯的工夫是否大于之前的音讯工夫
    const {msgTime} = data;               // 以后音讯产生工夫
    const preTime = monster?.msgTime || 0;  // 上一条音讯工夫
    if (msgTime > preTime) {
      monster.remainingHp = remainHp;       // 更新怪兽残余血量
      monster.damage = damage;
      monster.msgTime = msgTime;
    }
}

总结

本次需要也是首次在日常流动需要中应用 IM 计划,整体看起来也没有预期的那么简单,总的来讲绝对于之前罕用的接口轮询的形式,会缩小很多对服务端的压力,同时 IM 计划更新数据的及时性,也大幅晋升了用户体验;我的项目稳固运行一年多,也验证了 IM 计划在日常需要中的可行性。感兴趣的话欢送下载“心遇 APP”,体验家族打怪兽流动。

参考资料

  • 维基百科
  • 阮一峰:WebSocket 教程
  • RFC6455: WebSocket
  • WebSocket 网络协议

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版