deepstream.io是一个开放(MIT协议)高效的实时通信服务器,可以快速、安全的同步数据,能用于移动、Web、物联网场景。 在智慧教室系统中可担任BaaS(Backend as a Service)角色,可以简单的看为消息队列的高级应用。为了在软件架构中引入deepstream,下面根据其v5.0版本翻译文档。

概览

  • CS架构,S端用Node.js开发,C端提供js/java/c++/swift版本
  • 可以在(多个)C端和S端之间同步数据
  • 可以在C端间(经过S端)发送事件,且在事件的安全性上做了很多工作
  • S端可以配置连接自己喜欢的cache、database、message bus等,不需要写代码
  • deepstream.io中术语

records 自动实时同步的文档。可以编辑、可监视(通知变化)的JSON数据文档。文档变化会在各个C端间同步,S端会完成文档数据的持久化并保存在cache或storage(database)中。

events 发布-订阅模式的消息。多个C端可订阅(subscribe )同一个主题(topic),当其他C端发布(publish)该主题的数据时,主题订阅者会收到(receive)消息通知。

rpcs 请求-响应模式的工作流。C端可以注册(register)对外服务,其他C端可以请求(request)这些服务,deepstream.io会智能路由请求并返回服务响应(response)。

presence 监视上线情况。S端提供上线用户(C端)的查询接口,还可以在S端订阅login/logout消息。

listening 监听。当有新的主题(topic)注册(或订阅)时你的服务会立刻收到通知,可让你按需处理实时数据。

security 一切皆须授权。从用户登陆到消息传输,每个环节都支持身份验证和权限设置。

概念

deepstream是什么

概述:嗨,很高兴看到你偶然发现了此页。 deepstream是一个非常强大的概念,但它也有很大的不同,一开始可能会让您大吃一惊。因此,让我们深入了解它的确切含义和工作原理。

它是什么?

deepstream是一个可独立运行的实时通信服务器,可以在所有主要平台上运行。它也是一个单节点服务器,几乎在所有情况下都可以使用自定义逻辑轻松扩展。 deepstream客户端使用轻量级SDK与deepstream服务器建立持久的双向WebSocket连接。客户端的SDK有各种版本,包括javascript/Node,Java/Android,C++,Swift。 deepstream服务器本身是可配置的,并使用权限文件来验证传入的消息,权限文件只做验证不包含其他逻辑。所有逻辑均由“客户端”提供,“客户端”可以是后端进程,也可以是最终用户。deepstream提供了许多功能,例如侦听和活动订阅,以挂钩用户所请求的内容并相应地提供/转换数据,以及集成和检索来自第三方组件或API的数据。这使得deepstream既可以用作移动/浏览器和桌面客户端的实时服务器,又可以用作微服务架构的骨干。

它能做什么?

deepstream可用作大多数应用程序的后端,但通常用于

  • 诸如Google Docs或Trello之类的协作应用程序
  • 快速交易,赌博或拍卖平台
  • 信使和社交互动
  • 财务报告和风险控制
  • 休闲和移动多人游戏
  • 实时仪表盘和分析系统
  • 物联网指标收集和控制
  • 库存/库存控制
  • 流程管理系统

它如何做的?

deepstream提供了三个核心概念:

  • 数据同步(records):有状态和持久性的JSON对象,可以全部或部分操作它们,并在所有连接的客户端之间进行同步
  • 发布订阅(events):基于主题订阅的多对多消息传递
  • 请求-响应(rpcs):request/response工作流
  • 出席(presence):上线统计,上下线通知

它不能做什么?

deepstream是一种实时数据服务器,可以处理应用程序逻辑的所有方面。但它不是HTTP服务器,因此无法提供图片,HTML或CSS文件。在构建Web应用程序时,我们建议您使用CDN以及Github页面或AWS S3之类的东西来为您的静态资源提供服务,而动态数据则交由deepstream处理。

类似的产品?

最常见的比较对象是“自搭建的Firebase”(实际上你无法本地化Firebase)。虽deepstream还提供events和rpcs,并将其数据分解为具有独立生命周期的records,而非Firebase的整体式单树结构,但是这两者确实非常接近。为了便于理解,可以把deepstream在概念当作一个“自搭建的Firebase”,想想在金融交易或多人游戏服务器构建中Firebase的角色,而且是量级更大的那种。可以在我们的框架概述中找到更多有关deepstream如果处理实时通信的描述。

它可以与什么整合?

Deepstream可以选择与三种类型的系统集成:

  • 数据库 可用于长期数据存储和查询
  • 高速缓存 可用于快速短期数据访问
  • 连接器 可用于整合许多流行的系统,例如RethinkDB,MongoDB,Redis或ElasticSearch,也可以自己编写连接器

如果未指定外部系统,则Deepstream将作为单个节点运行并将数据存储在内部存储器中,但不会将其持久化到磁盘上

如何处理安全性?

deepstream支持加密连接和多种接入验证策略。它还使用一种称为Valve的细化权限语言,可让您准确配置哪些用户可以操作哪个记录、事件或和哪些数据进行rpc。

可扩展性如何?

deepstream 节点构建为具有异步I/O的小型单线程进程,可在群集中扩展,适合在云环境中正常运行。 单个节点每秒可以轻松完成160,000~200,000次更新流传输。最新的基准测试是在AWS EC2 t2.medium实例上运行了6个节点的集群,一小时传递了40亿条消息(AWS总成本为36美分)。

它用什么实现的?

主要是nodeJS,还有诸如uws Websocket服务器之类的本地nodeJS插件,可用于提高内存和CPU效率。

它身后是谁?

它由Yasser Fadl和Wolfram Hempel创立,这两个交易技术领域的极客曾经为伦敦的投资银行和对冲基金建立类似的系统,直到他们对这个世界感到有点恼火并决定转向开源。

连接

本段描述连接状态以及如何配置重新连接行为

所有deepstream客户端SDK均与平台建立了持久的双向连接。由于网络中断,缺少移动网络覆盖或类似问题,该连接可能会丢失:如果发生这种情况,所有SDK都会把更新输出到队列中缓存,并尝试重新建立连接。

重连

如果连接丢失,客户端将立即尝试重新连接。如果失败,它将等待一段时间并重试。每次尝试失败均会增加重试间隔,增加的毫秒数由reconnectIntervalIncrement指定。例如,如果将其设置为2000,第一次重连尝试将立即执行,第二次重连尝试在两秒钟后进行,第三次重连尝试在此后四秒钟,依此类推。您可以将上限指定为maxReconnectInterval。在多次重连失败后,如果间隔时间增大到maxReconnectAttempts,客户端将放弃并将连接状态更改为ERROR。

心跳

即使建立了连接,消息也可能无法到达。为了对此进行检查,客户端会不断向该平台发送小的ping消息,以确保它仍然可以访问。如果客户端错过了两个连续的响应,不管连接性如何它都将连接状态更改为ERROR。您可以配置发送这些心跳消息的频率heartbeatInterval(默认情况下每30秒发送一次)。

连接状态

每个SDK都提供对当前连接状态的查询和侦听方式。连接状态包括:

  • 已连接

    • OPEN 每个SDK都提供当前的连接状态以及侦听更改的方式。
  • 未连接

    • RECONNECTING 与服务器的连接已丢失。客户端尝试重新连接。
    • CLOSED 用户通过client.close()故意关闭了连接,不会进行任何重新连接尝试。客户端也以这种状态启动,但是几乎立即切换到AWAITING_CONNECTION。
    • ERROR 由于过多的重连尝试失败或心跳丢失,最终导致该连接被认为不可恢复。不会进行进一步的重新连接尝试。
  • 中间态

    • AWAITING_CONNECTION 客户端已建立物理连接,并等待服务器的初始响应。
    • CHALLENGING 客户端当前正在经历一个协商序列,这可能导致重定向或交换配置。
    • AWAITING_AUTHENTICATION 客户端初始化完毕且.login()被调用之前的状态。
    • AUTHENTICATING .login()被调用,但收到来自平台的响应之前的状态。

列子

const options = { // Reconnect after 10, 20 and 30 seconds reconnectIntervalIncrement: 10000, // Try reconnecting every thirty seconds maxReconnectInterval: 30000, // We never want to stop trying to reconnect maxReconnectAttempts: Infinity, // Send heartbeats only once a minute heartbeatInterval: 60000 };

const client = new DeepstreamClient('', options) client.login()

// Assume we're updating a green/yellow/red indicator for connectionState with jQuery const connectionStateIndicator = $('#connection-state-indicator'); client.on('connectionStateChanged', connectionState => { connectionStateIndicator.removeClass('good neutral bad') switch (connectionState) { case 'OPEN': connectionStateIndicator.addClass('good') break case 'CLOSED': case 'ERROR': connectionStateIndicator.addClass('bad') break default: connectionStateIndicator.addClass('neutral') } })

安全概述

加密,身份验证和用户权限三位一体组成deepstream的安全系统

deepstream的安全性基于三个相互关联的概念

  • 加密连接
  • 身份验证
  • 细粒度权限

下面简述这三者如何配合工作:

加密连接

deepstream 支持使用HTTPS和WSS进行面向Web的连接的传输层安全性。要在Deepstream上设置SSL,您需要提供以下配置密钥:

ssl: key: fileLoad(./my-key.key) cert: fileLoad(./my-key.key)

强烈建议始终使用独立过程作为SSL终端。通常通过负载均衡器(例如Nginx或HA Proxy)。要了解更多信息,请转到Nginx教程。

身份验证

每个传入的连接都需要通过身份验证步骤。客户端调用.login(data, callback)是进行验证。deepstream具有三种内置的身份验证机制:

  • none 允许每个连接。对于不需要访问控制的公共站点,请选择此选项。
  • file 从静态文件读取身份验证数据。对于公共读/私有写用例,例如运动结果页面,每个用户都可以访问,但是只有少数后端进程会更新结果,这是一个不错的选择。

http 与可配置的HTTP网络挂钩联系,询问是否允许用户连接。这是最灵活的选项,因为它允许您编写小型HTTP服务器并连接数据库、活动目录、oAuth供应商或其他您喜欢的东西。 除了仅接受/拒绝传入连接外,身份验证步骤还可以提供另外两个信息:

clientData 在客户端登陆时返回。这对于在登录时提供用户特定的设置很有用(例如博客中的{ "view": "author" }) serverData 将保存在服务器上,当客户端执行某项操作时传递给权限处理程序。这使得某些安全概念成为可能,例如基于角色的身份验证。

"system-settings": { //publicly readable "read": true, //writable only by admin "write": "user.data.role === 'admin'" }

身份验证常见问题

  • 身份验证何时发生? 创建deepstream客户端后它会立即建立连接,但连接在.login()被调用前会保持隔离状态。这样可以确保通过加密连接发送身份验证数据,并有利于用户输入密码再建立连接的场景。
  • 我可以从deepstream记录中读取用于身份验证的用户名吗? 目前尚无内置支持,但配置使用http auth-type并编写从同一数据库或高速缓存读取的服务器很容易。
  • 用户可以同时连接多个吗? 是。同一用户可以从单独的浏览器窗口或设备多次连接。

细粒度权限

权限决定了是否允许特定操作(例如写入记录或订阅事件)。为了细化权限,deepstream使用了一种表达性的,基于JSON的权限描述语言,称为Valve。关于Valve有很多话要说。为了给您第一印象,这里有个很小的Valve文件,在权限教程中可以了解更多

record:

"*": create: true delete: true write: true read: true listen: true

public-read-private-write/$userid: read: true create: "user.id === $userid" write: "user.id === $userid"

only-increment: write: "!oldData.value || data.value > oldData.value" create: true read: true

only-delete-egon-miller/$firstname/$lastname: delete: "$firstname.toLowerCase() === 'egon' && $lastname.toLowerCase() === 'miller'"

only-allows-purchase-of-products-in-stock/$purchaseId: create: true write: "_('item/' + data.itemId ).stock > 0"

event:

"*": listen: true publish: true subscribe: true

open/"*": listen: true publish: true subscribe: true

forbidden/"*": publish: false subscribe: false

a-to-b/"*": publish: "user.id === 'client-a'" subscribe: "user.id === 'client-b'"

news/$topic: publish: "$topic === 'tea-cup-pigs'"

number: publish: "typeof data === 'number' && data > 10"

place/$city: publish: "$city.toLowerCase() === data.address.city.toLowerCase()"

rpc:

"*": provide: true request: true

a-provide-b-request: provide: "user.id === 'client-a'" request: "user.id === 'client-b'"

only-full-user-data: request: "typeof data.firstname === 'string' && typeof data.lastname === 'string'"

关系数据建模

基于records的关系数据概念概述

deepstream的关系数据

关系数据库在我们的行业中很常见,开发人员经常学习使用关系技术对数据进行建模。学习如何在NoSQL解决方案中表示这些常见模式,对于新建项目和对遗留软件的增强都非常有益。

我们将深入研究使用deepstream的records来表示一些常见的关系。records是小型的JSON二进制数据,我们可以订阅、更新和设置权限。它们被持久化到缓存和数据库中,写操作很快,读操作更快并可以实时更新。

  • 1-1关系

在应用程序和传统的SQL数据库中,通常不需要对1-1数据建模。在大多数情况下,数据可以合并到单个表或文档/集合中。但有时我们会有一个类似customer的表:

id firstname lastname info 23 alex harley Like’s pizza

以及一个customer_details这样的表: user_id address card_number dob 23 Berlin xxx-xxx-xxx-xxx 1901 这时,一个customer行和一个customer_details行的关系肯定是1-1。使用deepstream时,如果我们不希望每次获取用户时都加载大量数据,或者我们需要给帐单数据单独设置record避免它们跟着用户信息一起加载时,1-1建模都是有意义的。对这种数据建模的方法是在原record中增加一项指向另一个record。它可能看起来像这样:

users/abc-123 { firstname: 'Alex', lastname: 'Smith', detailsRecord: 'details/abc-123' } details/abc-123 { address: 'Berlin', cardNumber: 'xxx-xxx-xxx-xxx', dob: 1901 }

现在我们可以随时获取有关用户的一些信息:

const record = client.record.getRecord('users/abc-123') record.whenReady((record) => { const { firstname, lastname, detailsRecord } = record.get() })

当用户自己想要查看其详细信息或对其进行更新时,简单:

const detailsRec = client.record.getRecord(detailsRecord) detailsRec.whenReady((record) => { const { address, cardNumber, dob } = record.get() })

这样做的另一个好处是,它容易对数据集的不同部分分别授权。假设当我们将鼠标悬停在用户上方时,希望显示其名称和一些简单信息。这样,我们只需要获取users/abc-123这一个Record即可。 但用户本人还应能够查看自己的用户卡详细信息。我们可以用Valve对details Records加以限制,对应规则可能看起来如下所示,我们要做的就是用用户id来限制details Records的读写权限。

record: "details/$userId": read: "user.id === userId" write: "user.id === userId"

  • 1-n关系

让我们继续看个涉及1-n关系的更复杂的示例。

在长周期应用程序中,经常出现用户出于某种原因更新地址的情况。通常这不是问题,但在某些情况下(通常涉及付款),我们需要保留这些地址的历史记录。在这种情况下,我们具有1-n关系,其中客户有多个地址。使用关系模型,我们创建如下customer表:

id firstname lastname 23 alex harley

以及一张address表:

user_id street_address city post_code country 23 123 Marienstrasse Berlin 88763 Germany

这里的user_id列遵循customer表id列的外键约束。

使用deepstream对此建模也简单,增加项不再指向record,改为指向一个列表。

users/abc-123 { firstname: 'Alex', lastname: 'Smith', addresses: [ 'addresses/789', 'addresses/894 ] }

该列表包含实际地址records的指针。例如:

addresses/789 { streetAddress: '123 Marienstrasse', city: 'Berlin', postCode: '88763', country: 'Germany' }

在获取这些地址时(假设我们已经有了user Record),我们可以按照以下步骤进行操作并呈现它们:

const addressList = client.record.getList(addresses) addressList.whenReady((list) => { list.forEach(printAddress) })

const printAddress= (recordName) => { const record = client.record.getRecord(recordName) record.whenReady(record => console.log(record.get())) } // { streetAddress: '123 Marienstrasse', city: 'Berlin', postCode: '88763', country: 'Germany' } // { streetAddress: '64 Engeldamm', city: 'Berlin', postCode: '12345', country: 'Germany' }

  • m-n关系

让我们再进一步,研究多对多关系。有多种方法能使用Records对此建模,每种方法都有自己的取舍。有些更适合查询,有些则不太适合,怎么取舍要看实际的用例。

假如您要创建一个“小组”和“人”的社交网络,小组可能包含许多人,每个人可能属于多个小组。使用关系模型,它可能看起来如下所示: 我们有一张user表

id firstname lastname 23 alex harley

还有张group表:

id name about city 78 hiking A place for hikers Berlin 96 gaming Gaming all day Auckland

然后是一张联接表groups_users,如下:

user_id group_id membership_type 23 78 admin 23 96 member

现在让我们来看看如何使用Records建模。

  • 每个record中包含一个列表

也许表达多对多关系的最基础方法是在数据集中每个Record中都添加一个列表,列表包含更多数据的指针。

在这里,我们创建了一个小组1234,该小组的成员对徒步旅行感兴趣,成立于12345435

groups/1234 { name: "hiking", created: 12345435, memberList: [ "users/123", "users/124" ] }

其中一个成员的数据结构如下所示,其中也包含该成员所在小组的列表:

users/123 { firstname: "Alex", lastname: "Smith", groupList: [ "groups/1234" ] }

这种对数据建模的方式非常直白,并且使从两侧(即用户所属的所有组以及组中的所有用户)查询都很简单。

但是不利的是,将用户添加到组或从组中删除用户可能会更为复杂。我们必须执行两次写入/删除操作,一次在用户端,一次在组端。

  • 使用中介Record

上述方法非常适合仅表示记录之间的关系,但如果我们还想给这种关系添加任何额外数据,我们都需要一个中介Record来存储额外数据。例如,我们要在会员关系中增加成员类型信息。

可以用下面的Records结构:

memberships/q6756i9 { type: "admin", joined: 12787434, referrals: 8 }

有了memberships,组和用户Records就可以包含一个memberships列表。

users/123 { firstname: "Alex", lastname: "Smith", memberships: [ memberships/q6756i9 ] } groups/1234 { name: "hiking", created: 12345435, memberships: [ memberships/q6756i9 ] }

按这种建模方式取数据时,我们需要加载一些不同的Lists并Records才能获得我们所需的一切。 从这种类型的设置中获取数据的基本思想是,我们需要加载一些不同的东西Lists并Records获得我们需要的一切。示例:

将用户添加到我们的远足组 我们要做的第一件事是获取组和用户的引用Records,依此创建新的会员record。顺带说一句,.getRecord函数实际上会进行CREATE或READ调用,因此获取和创建的语法Records是相同的。

const groupRecord = client.record.getRecord('groups/1234') const userRecord = client.record.getRecord('users/123') const mId = `memberships/${client.getUid()}` const membershipRecord = client.record.getRecord(mId)

接下来,我们需要设置会员Record的数据,包括type,joined和referrals属性。

membershipRecord.set({ userId: 'users/123', groupId: 'groups/1234', type: "user", // could be admin, organiser, etc joined: Date.now(), referrals: 0 })

最后,我们只需要将此成员record添加到用户和组的List中。为简便起见,我只显示将成员添加到组中,但是添加到用户列表的代码完全相同。

const membershipListName = groupRecord.get('memberships') const groupList = client.record.getList(membershipListName) groupList.addEntry(mId)

现在,我们在应用程序中有两个实体之间的关系。用于从组中删除用户的代码也非常相似。

获取我们远足小组的所有成员以及他们何时加入 假设我们要列出远足小组的所有成员。这非常简单,可以很快完成。首先,我们只需要获取对group的引用,然后enumerateMembers使用成员资格列表调用函数。

const record = client.record.getRecord('groups/1234') record.whenReady((record) => { enumerateMembers(record.get('memberships')) })

enumerateMembers函数用于获取每个memberships Record。它将以用户ID和加入时间调用displayUser函数。 function simply gets the Record for each of our memberships. It calls both the displayUser function with the users Id and when they joined.

function enumerateMembers(memberList) { memberList.forEach((membershipId) => { const memberRecord = client.record.getRecord(membership) memberRecord.whenReady(record => { displayUser(record.get('userId'), record.get('joined')) }) } }

最后,我们的displayUser函数实现加载用户Record并打印他们的姓名以及他们加入远足组的时间。

function displayUser(userId, joined) { const userRecord = client.record.getRecord(userId) userRecord.whenReady((record) => { console.log(`${record.get('firstname')} has been in the hiking group since ${formatDate(joined)}`) }) }

如上所示,在此建模方式下查询各种不同的数据非常容易。查找用户的所有组与我们刚刚进行的操作类似,留下来给你做。

主动数据提供者

如何通过按需提供数据来提高应用程序性能

什么是数据提供者?

数据提供者是给deepstream提供数据的进程。从技术上讲,它们只是常规的deepstream客户端,通常运行在后端并写入record,发送event或提供rpc服务。

例子 假设您正在构建一个应用程序,该应用程序显示来自世界各地的各个交易所的股票价格。对于每个交易所,您会构建一个进程来接收数据并将其转发到deepstream。

问题 仅纳斯达克每天就发出数千万次价格更新,而对于其他证券交易所来说也差不多。这会给您的基础架构带来不可持续的负担,并可能导致高带宽成本。更糟糕的是,大多数更新可能是由那些无人订阅且不会转发的股票产生的。

解决方案:主动数据提供者

仅对客户端感兴趣的record执行写入或只发送客户端感兴趣的event。deepstream支持一种称为listening的功能,该功能使客户端可以侦听其他客户端的event或record订阅。首先,监听者注册一个模式,例如nasdaq/.*,有其他客户端订阅匹配该模式的event或record时监听者会收到回调,当其他客户端取消订阅后还会收到.onStop回调,例如:

client.record.listen('nasdaq/.*', (match, response) => { // Start providing data response.accept()

response.onStop(() => { // stop providing data })

// write record , send event etc. })

这使您可以创建更高效的数据提供者,它们仅发送当前所需的数据。

© 2024 lanser.fun修订时间: 2023-05-02 02:19:15

results matching ""

    No results matching ""