如何直接连接到Aply(以及为什么你可能不应该)--第二部分

176 阅读15分钟

本系列的第一部分中,我们介绍了如何绕过Ably的SDK,使用websocat 连接到Ably的后端,其中每条JSON信息都需要手工输入。

在第二部分中,我们将通过切换到NodeJS来解决这个繁琐的问题,并且我们将继续构建我们自己的最小的Ably(NodeJS)SDK的过程。在本教程结束时,我们将在一个简单的命令行应用程序中实现Ably的基本功能,这将使我们能够连接到Ably并发送消息。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

如果你还没有安装NodeJS(如果你不确定,可以运行node --version ),你可以从NodeJS网站下载一个副本。

为了减少一些模板,我在www.github.com/ably/protoc… 建立了一个Git仓库,
其中包含了你完成本教程其余部分所需的所有代码。如果你不确定如何克隆一个Git仓库,请查看"克隆仓库 "指南

设置项目

一旦你克隆了项目,你需要从NPM安装它的依赖项。你可以通过在项目的根目录下运行npm install 命令来完成。这个项目使用了几个NPM依赖项,使生活变得更容易:

  • ws- NodeJS的一个简单的WebSocket客户端。
  • chalk- 使得打印出彩色输出到控制台变得容易。
  • commander- 解析命令行参数并自动生成帮助页面。对于在NodeJS中构建命令行界面非常有用。
  • dotenv- 允许我们从.env 文件加载环境变量。我们将用它来向程序提供我们的Aply API密钥。
  • inquirer- 允许我们在命令行上提示交互式输入。

最后,为了使程序能够与Ably进行验证,我们需要用我们的Ably API密钥创建一个.env 文件。在项目的根目录下创建一个名为.env 的文件,并在其中添加以下内容,确保用你自己的Ably API密钥替换下面的<your_api_key> 字符串(你可以在任何时候从Ably仪表板上修改、撤销或创建新的API密钥)。

ABLY_API_KEY=<your_api_key>

检查项目代码

src 目录包含四个文件:

  • index.js - 这是该程序的基本入口,它有一些简单的代码。
    • 来解析.env 文件和它收到的命令行参数;以及
    • 用正确的选项来启动程序。
  • App.js - 这里是大部分代码的所在。这个文件的主要目的是。
    • 处理按键事件;以及
    • 打印我们从Ably收到的信息。
  • protocolActions.js - 这只是一个对象,它包含了协议消息动作编号与名称的映射。
  • Ably.js - 这个文件目前只包含一个空的类,但在本教程结束时,我们将把它变成一个可以的类。
    • 连接到Ably。

    • 附加和分离通道。

    • 发布实时消息;以及

    • 进入/离开通道。

运行程序(有错误)

你有几种方法可以运行该程序。如果你想把程序添加到你的全局PATH中,那么你可以在项目目录中运行npm link ,之后你可以简单地使用ably-cli 命令运行它。如果你不想把程序添加到你的PATH中,那么你可以在项目目录内运行以下程序:

node .

当你第一次运行该程序时,你应该看到一个错误信息:
this.ably.listen is not a function 。这是因为程序在Ably 类的一个实例上调用了listen 方法(见App.js第23行),而该类还没有实现该方法。我们可以通过添加一个空的实现来轻松绕过这个错误。

class Ably {
   listen() {}
}
 
module.exports = Ably;

一旦你为listen 添加了一个实现,你就应该能够在没有任何错误的情况下运行该程序。它应该向你显示一个像下面的截图一样的帮助信息。你可以在任何时候按q ,退出程序并返回到shell。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

除了q ,这些键盘快捷键都还没有实现,但我们将在本教程的其余部分实现所有这些快捷键。

构造函数

为了最终得到一个实现了上述所有Ably功能的Ably 类,我们需要做的第一件事是设置一个WebSocket连接。Ably 类接受一个连接参数对象作为参数。例如,要用API密钥和无心跳的方式进行连接,你可以这样调用构造函数:

new Ably({
 key: '<your api key>',
 heartbeats: 'false',
});

程序会自动接收我们放在.env 里面的API密钥,所以我们不需要自己处理调用构造器的问题。然而,我们需要接受这些连接参数,并创建一个WebSocket URL,就像我们使用websocat ,连接到Ably那样。

首先:我们需要导入ws NPM模块。要做到这一点,我们只需在我们的Ably.js 文件的顶部添加一个require语句:

const WebSocket = require('ws');

来自ws 的WebSocket类需要一个WebSocket URL作为构造参数,因此我们需要构造一个URL并将其传入。有几种方法可以做到这一点。例如,你可以使用NodeJS的内置URL库。然而,你的方法并不是特别重要,所以这里有一个我们可以使用的简单实现的例子:

constructor(options) {
       let url = 'wss://realtime.ably.io?';
       url += Object.entries(options).map(([key, value]) => `${key}=${value}`).join('&');
       this.ws = new WebSocket(url);
   }

如果你不熟悉ES6 JavaScript,这可能看起来有点奇怪,但它所做的是采取基本URL、
wss://realtime.ably.io (加上? 连接器),将每个选项作为查询参数添加,使用结果URL来调用WebSocket构造函数,最后将连接存储在一个名为ws 的类字段中。现在,连接被存储为一个类字段,我们可以使用this.ws ,从该类范围内的任何地方访问它。

监听方法

现在,当我们运行程序时,它仍然不会实现任何键盘快捷键,但在幕后,它还是应该与Ably建立一个WebSocket连接。为了验证我们是否连接成功,我们需要实现listen 方法。

listen 方法需要一个回调,每次我们从Ably收到协议消息时,都应该用该消息调用回调。为了在我们每次收到WebSocket消息时运行代码,我们需要使用this.ws.on 方法。该方法需要两个参数:第一个参数是事件类型,第二个参数是回调,在事件发出时执行。当我们收到WebSocket消息时发出的事件被称为message ,当我们收到该消息时,我们需要将其从JSON解析为一个对象,以便我们可以读取其属性。

我们对listen 方法的实现应该是这样的:

listen(callback) {
       this.ws.on('message', (msg) => {
           const message = JSON.parse(msg)
           callback(message);
   });
}

一旦你把这个实现添加到Ably 类中,再试着运行程序。如果一切都正确,你应该看到一个CONNECTED 消息。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

这不算什么(目前),但我们现在已经创建了一个能够通过WebSocket连接连接到Ably的JavaScript类。

发送协议消息

在我们使我们的Ably 类能够附加到通道之前,让我们添加一个快速辅助方法,以便以后更容易发送协议消息。由于我们总是要将协议消息作为JSON字符串发送,这个方法允许我们将任何协议消息作为JavaScript对象传入,以正确的格式通过WebSocket连接发送:

sendProtocolMessage(message) => {
       this.ws.send(JSON.stringify(message));
}

附加到通道

现在我们准备尝试附加到一个实时通道。你可能已经从最初的帮助信息中注意到,附加到一个通道的键盘快捷键是a 。在程序运行时按a ,它应该提示你输入一个频道名称。输入一个你选择的频道名称,然后按Enter 。由于我们还没有在我们的Ably 类中实现附加到一个通道,这将给我们一个错误:

TypeError: this.ably.getChannel is not a function

你可以看到在App.jsonKeypress 方法中是如何处理按键事件的。在这种情况下,当我们按下a ,程序将调用ably.getChannel ,并提供通道名称作为参数。getChannel 将返回一个Channel 实例,并对该实例调用attach 方法。最后,App 类将把通道实例存储在一个数组中,这样我们就能始终知道我们连接到哪个通道。

让我们先在Ably.js 文件中创建一个简单的Channel 类:

class Channel {
   constructor(channelName, ably) {
       this.name = channelName;
       this.ws = ws;
   }
   attach() {}
}

现在,这个类只是存储了一个频道名称和一个Ably 类实例,也有一个空的附加方法。

现在我们可以为Ably 类实现getChannel 方法,以便能够返回一个具有指定名称的Channel 类的新实例

getChannel(channelName) {
   return new Channel(channelName, this);
}

注意我们是如何将Ably 实例传递给Channel 构造函数的。这是为了让我们能够访问
sendProtocolMessage 方法,以便从Channel 类的方法中发送消息。

我们最后可以继续实现attach 方法。正如本帖第一部分所提到的,附加到一个通道的动作编号是10,我们还需要在channel 字段中发送通道的名称:

attach() {
       this.ably.sendProtocolMessage({
           action: 10,
           channel: this.name,
   });
}

如果你像上面那样在你的Channel 类中实现了attach 方法,你应该能够使用程序附加到一个频道。当程序运行时,按下a 键,它将要求你输入一个通道名称。如果你输入任何有效的通道名称并按下Enter ,你应该看到一条信息,说你已经连接到该通道。你可以在App.js 中检查代码,看看当你按下
a
键时,什么代码正在运行:

How to connect to Ably directly (and why you probably shouldn't) – Part 2

脱离通道

脱离频道的工作方式与附加频道的工作方式基本相同。我们只需要使用动作编号12而不是10,除此之外,代码看起来是一样的:

detach() {
       this.ably.sendProtocolMessage({
           action: 12,
           channel: this.name,
   });
}

一旦你把这个方法添加到你的Channel 类中,你应该能够在程序运行时按下d ,如果你连接到任何通道,它将提示你选择你想从哪个通道脱离。使用方向键选择一个通道,然后按Enter ,发送协议信息。不久之后,你应该看到一条消息,告诉你你已经从该频道脱离了。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

在一个频道上发布消息

还记得第一部分中的消息序列吗?为了发布消息,你需要提供一个消息序列,这是一个与每条消息一起发送的唯一数字。Ably使用这个数字来告诉我们哪条消息成功发布了。为了确保我们每次发布到一个频道时都发送一个不同的消息序列,让我们在Channel 类中添加一个msgSerial 字段:

constructor(name, ably) {
       this.name = name;
       this.ably = ably;
       this.msgSerial = 0;
}

这个msgSerial 字段将从0 开始,每次我们发布消息时,我们将使用该字段的当前值,然后在使用后立即递增,以便我们下次发送不同的消息。

现在我们准备在Channel 类上实现publish 方法。发布消息的动作编号是15。我们需要确保发送通道名称和消息序列(我们可以使用++ 操作符来返回当前值并同时递增),并且消息需要像我们在第一部分中那样以数组形式发送。我们使用{ data } 作为ES6的缩写,即{ data: data }

publish(data) {
       this.ably.sendProtocolMessage({
           action: 15,
           channel: this.name,
           msgSerial: this.msgSerial++,
           messages: [
           { data
           }
       ],
   });
}

现在,如果你再次运行程序并附加到一个频道,你可以按m ,向该频道发布一条消息。程序会提示你选择一个频道并输入信息,然后你应该看到一个ACK和信息。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

输入存在感

存在感Ably的一个核心功能,它允许客户端知道在一个频道上有哪些其他客户端存在。

How to connect to Ably directly (and why you probably shouldn't) – Part 2 hbspt.cta.load(6939709, '85f436af-7315-44c9-aa12-d764f40b294d', {"region":"na1"});

通常情况下,使用ably-js ,你会使用channel.presence.enterchannel.presence.leave 方法来进入和离开一个通道上的存在。然而,为了简单起见,本程序使用channel.enterPresence
channel.leavePresence

在我们继续在程序中实现在场功能之前,我们需要理解在场动作的概念。它实际上比较简单:类似于我们用编号的动作来表示不同种类的协议信息,当我们发送或接收一个在场信息时,我们也会发送一个编号来表示该信息所代表的在场动作。进入信道临场感、离开信道、更新临场感数据等都有不同的动作编号。请看Ably文档中的所有在场行动的列表

发送临场信息类似于发布信息。对于你发送的每一条消息,你都需要发送一个消息序列,这样当Ably确认收到消息时,它就可以具体说明是哪条消息被收到。你还必须在一个数组中发送存在信息,就像你向通道发布信息时那样,这样你就可以在一个协议信息中发送多个存在信息(但为了简单起见,我们一次只发送一个)。

现在我们准备实现enterPresence 方法。临场信息的动作号是14 。它被放在临场信息中一个叫做action 的字段中。同时,进入一个频道的在场动作编号是2

下面是所产生的实现应该是这样的:

enterPresence() {
       this.ably.sendProtocolMessage({
           action: 14,
           channel: this.name,
           msgSerial: this.msgSerial++,
           presence: [
           { action: 2
           }
       ],
   });
}

一旦你添加了这个实现,继续用程序附加到一个信道上,并试着按e ,进入在场状态。你应该看到一个带有以下错误信息的NACK。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

客户端ID

出现这个信息的原因是,要在一个通道上输入存在,你需要指定一个clientId 。阅读更多关于 clientId 的含义。可以发送一个明确的clientId 作为在场协议消息的一部分,但你也可以发送一个clientId 作为一个连接参数。

幸运的是,该程序已经被设置为使用一个命令行选项来做这件事。如果你用ably-cli 命令运行程序,只需运行ably-cli --clientId <your_clientId> 。如果你用node . 运行程序,则使用node . --clientId <your_clientId> ,确保用你选择的clientId 替换<your_clientId>。这将发送一个clientId ,作为初始连接的一部分,并且该clientId 将适用于你在该连接期间发送的所有协议信息。

如果你用上述明确指定的clientId 来运行程序,你现在应该能够在一个频道上输入存在。当你在一个信道上输入存在时,你应该收到一个ACK和一个来自Ably的存在消息,表明在该信道上发生了一个存在事件。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

离开存在感

一旦你实现了enterPresence 方法,leavePresence 是很容易的。实现应该几乎是完全一样的,唯一的区别是在信道上离开存在的消息动作是4 (进入存在是2 ):

leavePresence() {
       this.ably.sendProtocolMessage({
           action: 14,
           channel: this.name,
           msgSerial: this.msgSerial++,
           presence: [
           { action: 4
           }
       ],
   });
}

现在试着用程序在一个频道上输入在场信息,然后按l (小写的 "L")来离开这个频道。你应该收到另一个ACK和另一个存在信息,如下图所示。

How to connect to Ably directly (and why you probably shouldn't) – Part 2

把它放在一起

如果你跟着上面所有的代码走,那么你刚刚实现了一个你自己的超轻量级的Ably SDK,能够发布和订阅消息,并且能够在一个频道上进入和离开存在。你可以试着打开两个终端窗口,验证一下你是否能从程序的一个实例中发送消息,并在另一个实例中接收消息。

如果你没有一起编码,但想玩玩最终的代码库,可以从GitHub上的final-program分支查看本教程的完整代码

那么,为什么我需要一个SDK?

希望通过阅读,你已经了解到使Ably工作的底层协议实际上是相当简单的,而且自己实现起来也不难。因此,你可能想知道为什么你需要使用我们众多(20多个)Ably SDK中的一个。

实际上,有很多很好的理由让你在生产中使用Ably SDK而不是推出自己的解决方案。以下是其中几个原因。

  • Ably SDKs有相当多的逻辑来恢复连接,如果它由于任何类型的连接问题而掉线。要考虑到客户端可能连接的所有不同类型的网络条件是相当困难的。
  • Ably SDKs可以在不支持WebSockets的环境下退回到各种实时网络协议,例如在Internet Explorer等旧的网络浏览器中。
  • Ably SDKs经常更新,有新的功能、错误修复和安全补丁。如果你直接连接到Ably,你需要在内部完成所有这些工作,否则就会错过。更多信息请参见Ably SDK的功能规格

总之,在没有SDK的情况下直接连接到Aply是一种有趣的方式,可以更多地了解Aply在引擎盖下的工作方式。但是,当你需要将Ably构建成一个真实的应用程序时,你最好使用我们的一个SDK。