Deepstream.io

2020-03-18  本文已影响0人  清晨起床敲代码

deepstream.io 介绍

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是一个可独立运行的实时通信服务器,可以在所有主要平台上运行。它也是一个单节点服务器,几乎在所有情况下都可以使用自定义逻辑轻松扩展。<br />
deepstream客户端使用轻量级SDK与deepstream服务器建立持久的双向WebSocket连接。客户端的SDK有各种版本,包括javascript/Node,Java/Android,C++,Swift。<br />
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()被调用,但收到来自平台的响应之前的状态。
列子

 <nowiki>
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('<url>', 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')
    }
})
</nowiki>

安全概述

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

deepstream的安全性基于三个相互关联的概念
*加密连接
*身份验证
*细粒度权限
下面简述这三者如何配合工作:

加密连接

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

 <nowiki>
ssl:
  key: fileLoad(./my-key.key)
  cert: fileLoad(./my-key.key)
</nowiki>

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

身份验证

每个传入的连接都需要通过身份验证步骤。客户端调用.login(data, callback)是进行验证。deepstream具有三种内置的身份验证机制:
*none 允许每个连接。对于不需要访问控制的公共站点,请选择此选项。
*file 从静态文件读取身份验证数据。对于公共读/私有写用例,例如运动结果页面,每个用户都可以访问,但是只有少数后端进程会更新结果,这是一个不错的选择。
http 与可配置的HTTP网络挂钩联系,询问是否允许用户连接。这是最灵活的选项,因为它允许您编写小型HTTP服务器并连接数据库、活动目录、oAuth供应商或其他您喜欢的东西。
除了仅接受/拒绝传入连接外,身份验证步骤还可以提供另外两个信息:

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

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

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

细粒度权限

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

 <nowiki>
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'"
</nowiki>

关系数据建模

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

deepstream的关系数据

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

我们将深入研究使用deepstream的records来表示一些常见的关系。records是小型的JSON二进制数据,我们可以订阅、更新和设置权限。它们被持久化到缓存和数据库中,写操作很快,读操作更快并可以实时更新。
*1-1关系
在应用程序和传统的SQL数据库中,通常不需要对1-1数据建模。在大多数情况下,数据可以合并到单个表或文档/集合中。但有时我们会有一个类似customer的表:

 <nowiki>
id  firstname   lastname    info
23  alex            harley          Like’s pizza</nowiki>
以及一个customer_details这样的表:
<nowiki>
user_id address card_number dob
23  Berlin  xxx-xxx-xxx-xxx 1901
</nowiki>

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

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

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

 <nowiki>
const record = client.record.getRecord('users/abc-123')
record.whenReady((record) => {
    const { firstname, lastname, detailsRecord } = record.get()
})
</nowiki>
当用户自己想要查看其详细信息或对其进行更新时,简单:
 <nowiki>
const detailsRec = client.record.getRecord(detailsRecord)
detailsRec.whenReady((record) => {
    const { address, cardNumber, dob } = record.get()
})
</nowiki>

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

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

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

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

 <nowiki>
id  firstname   lastname
23  alex            harley</nowiki>
以及一张address表:
 <nowiki>
user_id street_address          city    post_code   country 
23  123 Marienstrasse   Berlin  88763           Germany 
</nowiki>

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

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

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

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

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

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

 <nowiki>
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' }
</nowiki>

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

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

 <nowiki>
id  firstname   lastname
23  alex            harley
 </nowiki>

还有张group表:

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

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

 <nowiki>
user_id group_id    membership_type 
23  78          admin   
23  96          member  
</nowiki>

现在让我们来看看如何使用Records建模。
**每个record中包含一个列表
也许表达多对多关系的最基础方法是在数据集中每个Record中都添加一个列表,列表包含更多数据的指针。

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

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

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

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

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

但是不利的是,将用户添加到组或从组中删除用户可能会更为复杂。我们必须执行两次写入/删除操作,一次在用户端,一次在组端。
**使用中介Record
上述方法非常适合仅表示记录之间的关系,但如果我们还想给这种关系添加任何额外数据,我们都需要一个中介Record来存储额外数据。例如,我们要在会员关系中增加成员类型信息。

可以用下面的Records结构:

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

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

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

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

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

 <nowiki>
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)</nowiki>
接下来,我们需要设置会员Record的数据,包括type,joined和referrals属性。
 <nowiki>
membershipRecord.set({
    userId: 'users/123',
    groupId: 'groups/1234',
    type: "user", // could be admin, organiser, etc
    joined: Date.now(),
    referrals: 0
})
</nowiki>

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

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

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

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

 <nowiki>
const record = client.record.getRecord('groups/1234')
record.whenReady((record) => {
    enumerateMembers(record.get('memberships'))
})</nowiki>
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.
 <nowiki>
function enumerateMembers(memberList) {
    memberList.forEach((membershipId) => {
        const memberRecord = client.record.getRecord(membership)
        memberRecord.whenReady(record => {
            displayUser(record.get('userId'), record.get('joined'))
        })
    }
}
</nowiki>

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

 <nowiki>
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)}`)
    })
}
</nowiki>

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

主动数据提供者

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

什么是数据提供者?

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

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

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

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

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

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

  // write record , send event etc.
})
</nowiki>

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

安装

在Linux上安装

了解如何在Linux上安装deepstream

下载最新的服务器安装包deepstream.io-linux-VERSION.tar.gz并将其解压缩。

启动deepstream

您可以在控制台上简单的键入如下命令启动deepstream
./deepstream
了解deepstream 命令行界面及其配置文件的更多信息。

一些提示

*deepstream的配置文件可以用YAML或JSON编写。deepstream将根据文件扩展名自动选择正确的解析器。
*一些核心配置选项可以被命令行参数覆盖,例如--host,--port或--disable-auth。完整列表,请运行
deepstream start --help
*配置文件中有相对路径,例如 ./permissions.yml或users.yml,这些路径是相对于主配置文件所在目录的。如果您从其他位置运行,请确保对其进行更新。

安装为服务

从2.4开始,deepstream在支持init.d或systemd的计算机上可作为服务自动运行。

安装服务非常简单
sudo deepstream service add
然后,您可以使用常规服务命令启动它
sudo service deepstream start
或直接通过deepstream启动
sudo deepstream service start
对于那些希望注册多个服务(在一台机器上运行多个deepstream)的用户,可用指定名称并提供唯一的配置文件来实现。
sudo deepstream service add --service-name deepstream-6020 -c ~/path1/to/config
sudo deepstream service add --service-name deepstream-6030 -c ~/path2/to/config

避免sudo

如果要验证服务器配置,或者不想用sudo操作,可以加--dry-run选项打印出服务脚本以进行配置检查或手动安装。
deepstream service add --dry-run

在OSX上安装

了解如何在OS X上安装deepstream

下载最新的服务器安装包deepstream.io-mac-VERSION.zip并将其解压缩。

启动deepstream

您可以在控制台上简单的键入如下命令启动deepstream
./deepstream
了解deepstream 命令行界面及其配置文件的更多信息。

一些提示

*deepstream的配置文件可以用YAML或JSON编写。deepstream将根据文件扩展名自动选择正确的解析器。
*一些核心配置选项可以被命令行参数覆盖,例如--host,--port或--disable-auth。完整列表,请运行
deepstream start --help
*配置文件中有相对路径,例如 ./permissions.yml或users.yml,这些路径是相对于主配置文件所在目录的。如果您从其他位置运行,请确保对其进行更新。

Homebrew

Homebrew尚未与V5版本保持同步。如果您能够帮助进行更新,请与我们联系!

在Windows上安装

了解如何在Windows上安装deepstream

下载最新的服务器安装包deepstream.io-windows-VERSION.zip并将其解压缩。

启动deepstream

您可以通过双击可执行文件或通过CMD启动服务器
deepstream.exe start
了解deepstream 命令行界面及其配置文件的更多信息。

一些提示

*deepstream的配置文件可以用YAML或JSON编写。deepstream将根据文件扩展名自动选择正确的解析器。
*一些核心配置选项可以被命令行参数覆盖,例如--host,--port或--disable-auth。完整列表,请运行
deepstream start --help
*配置文件中有相对路径,例如 ./permissions.yml或users.yml,这些路径是相对于主配置文件所在目录的。如果您从其他位置运行,请确保对其进行更新。

Node / NPM / Yarn

通过NPM和Node.js安装deepstream

deepstream也可以作为NPM软件包安装,并提供Node.js API进行编程交互。这对自定义身份验证或许可逻辑很有用。您可以查看完整的Node.js API 。

通过npm安装服务器

npm install @deepstream/server
创建一个js文件,例如具有以下内容的start.js

 <nowiki>
const { Deepstream } = require('@deepstream/server')

/*
The server can take
1) a configuration file path
2) null to explicitly use defaults to be overriden by server.set()
3) left empty to load the base configuration from the config file located within the conf directory.
4) pass some options, missing options will be merged from the base configuration.
*/
const server = new Deepstream()

// start the server
server.start()
</nowiki>

用node运行文件
node start.js

在Node.js中使用Deepstream客户端

可以通过NPM安装deepstream javascript客户端并在Node.js中使用。

 npm install @deepstream/client
 <nowiki>
const { DeepstreamClient } = require('@deepstream/client')
const client = new DeepstreamClient('localhost:6020')
client.login()
</nowiki>

Docker Image

了解如何从docker镜像运行deepstream的独立容器。本教程还将说明如何手动构建映像。

什么是Docker?

Docker是一个开源项目,可自动在软件容器内部署应用程序。Docker在Linux上提供了OS虚拟化的抽象层和自动化层。

在Docker基础映像上执行操作时,将以如下方式创建并记录联合文件系统层,即每个层都完全描述了如何重新创建操作。该策略使Docker支持轻量级映像,因为仅更新的层需要传播(而不是整个VM)。

为什么要在Deepstream上使用Docker?

Docker提供了一个轻量级的解决方案来启动deepstream服务器。它封装了所有必要的设置,并降低了大规模微服务部署的复杂性。

如何在Docker中启动deepstream?

要使deepstream服务器快速运行,您可以执行以下操作:
docker run -p 6020:6020 deepstreamio/deepstream.io
仅此而已!

如果要覆盖某些配置或在后台运行deepstream,请继续阅读。

首先,通过运行以下命令从DockerHub注册表中安装镜像:
docker pull deepstreamio/deepstream.io
然后,用该镜像创建一个容器,并将容器命名为deepstream.io:

 <nowiki>
docker create -t -p 6020:6020 \
  --name deepstream.io \
  -v $(pwd)/conf:/usr/local/deepstream/conf \
  -v $(pwd)/var:/usr/local/deepstream/var \
  deepstreamio/deepstream.io
</nowiki>

现在您可以通过以下方式启动容器
docker start -ia deepstream.io
这将在前台启动容器。您可以按Ctrl+C但是该容器仍然有效。要停止容器,您需要运行
docker stop deepstream.io
如果您想在后台启动它,只需忽略-ia选项。

您可以使用以下命令显示日志:
docker logs -f deepstream.io
该-f选项将使保持进程活性并跟踪输出。

如果您想要的不仅仅是一个deepstream节点,请转至Docker Compose教程,该教程介绍了deepstream该如何连接数据库,高速缓存和搜索容器,这些容器充当deepstream的高速缓存和存储层。

入门

从HTTP入门

了解如何启动服务器并用HTTP API交互

本指南将介绍deepstream的HTTP接口,并说明如何使用它来访问records,events,RPCs和presence。

使用HTTP API的第一件事是明确应用程序的HTTP URL。

启动服务器

让我们从安装服务器开始。只需为您的操作系统选择正确的版本,然后按部就班即可。安装服务器后,可以使用以下命令启动它:
deepstream start
想体验实际工作效果,我们可以设置一个javascript WebSocket客户端。详细设置请查看javascript 教程入门。

Events (订阅-发布)

我们将使用JS客户端订阅事件test-event:

 <nowiki>
ds.event.subscribe( 'test-event', function( eventData ){ 
  console.log( eventData );
});
… 
</nowiki>

现在我们可以使用HTTP客户端(例如jQuery.ajax)发布事件:

<nowiki>
const requestBody = {
  body: [{
    topic: 'event',
    action: 'emit',
    eventName: 'test-event',
    data: { some: 'data' }
  }]
};

const url = '<YOUR HTTP URL>';

$.ajax({
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  url: url,
  data: JSON.stringify(requestBody)
}).done(function (response) {
  console.log('The request was a', response.result);
});
…
 </nowiki>

也可以用CURL:

<nowiki>
curl -X POST -H "Content-Type: application/json" -d '{
 "body": [{
   "topic": "event",
   "action": "emit",
   "eventName": "test-event",
   "data": "some test data"
 }]
}' "<YOUR HTTP URL>"
</nowiki>

更多信息请参见Deepstream HTTP文档。

从JavaScript入门

了解如何启动服务器并连接一个简单的客户端

是时候开始使用deepstream了。本教程将引导您启动服务器并使用一个简单的JS 网页客户端连接服务器。

启动服务器

让我们从安装服务器开始。只需为您的操作系统选择正确的版本,然后按部就班即可。安装服务器后,可以使用以下命令启动它:
deepstream start

安装客户端

在本教程中,我们从deepstream.io上获取JS 客户端,但您也可以用NPM从@deepstream/client获取:
npm install @deepstream/client
创建一个index.html文件并向其中添加以下内容,确保使用了您的客户端库:

 <nowiki>
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.deepstream.io/js/client/latest/ds.min.js"></script>
  </head>
  <body>
    <input type="text" />
    <script type="text/javascript">
      //js goes here
    </script>
  </body>
</html>
</nowiki>

该页面包含一个文本框供用户输入。在script标记内,添加以下JavaScript登录您的deepstream服务器:

<nowiki>
const client = new DeepstreamClient('localhost:6020')
client.login()
</nowiki>

接下来,我们请求record。Records用于在所有已连接客户端之间同步的、少量数据。
const record = client.record.getRecord('some-name')
最后,我们将其连接到文本框。在多个浏览器窗口中打开同一页面,我们会使输入保持同步

 <nowiki>
const input = document.querySelector('input')

input.onkeyup = (function() {
  record.set('firstname', input.value)
})

record.subscribe('firstname', function(value) {
  input.value = value
})
</nowiki>

在两个浏览器窗口中打开网页,然后在任一文本框中键入文本,另一个浏览器窗口将立即反映出更改。

就是这样。当然还有更多事可做。如果您想了解有关record及其用途的更多信息请转到record教程。或开始阅读有关deepstream的Request/Response或Pub/Sub功能的文档。

从Java入门

了解如何用Java构建deepstream服务器端的实时数据提供者

本指南将向您展示如何使用deepstream的三个核心概念(Records,Events和RPCs)在Java中构建后端流程。

启动您服务器

让我们从安装服务器开始。只需为您的操作系统选择正确的版本,然后按部就班即可。安装服务器后,可以使用以下命令启动它:
deepstream start

连接deepstream并登陆

您需要做的第一件事是使用gradle设置Java项目,并将以下行添加到build.gradle文件中。
compile 'io.deepstream:deepstream.io-client-java:2.0.4'
然后我们就可以实例化一个客户端,如下所示:
DeepstreamClient client = new DeepstreamClient("<Your app url here>");
并登录(我们未配置任何身份验证,因此不需要凭据)
client.login()

Records (实时数据存储)

Records是deepstream实时数据存储文档。record由唯一ID标识,并且可以包含任何种类的JSON数据。客户端和后端进程可以创建、读取、写入、更新、订阅整个record或其中指定部分。任何更改都会立即在所有连接的订阅者之间同步。Records可以用列表和集合组织,还可以包含对其他记录的引用,以便对关系数据结构进行建模。您可以在“ 记录指南”中了解有关记录的更多信息。

创建新记录或检索现有记录可以使用 getRecord()
Record record = client.record.getRecord("test-record");
可以使用以下.set()方法存储值

 <nowiki>
// you can set the whole record
JsonObject data = new JsonObject();
data.addProperty("name", "Alex");
data.addProperty("favouriteDrink", "coffee");
record.set(data);

// or just a path
record.set( "hobbies", new String[]{ "sailing", "reading" });
</nowiki>

并使用.get()获取数据

<nowiki>
record.get(); // returns all record data as a JsonElement
record.get( "hobbies[1]" ); // returns the JsonElement 'reading'
</nowiki>

用.subscribe()订阅您或其他客户端的更改

 <nowiki>
//subscribe to changes made by you or other clients using .subscribe()
record.subscribe(new RecordChangedCallback() {
    public void onRecordChanged(String recordName, JsonElement data) {
        // some value in the record has changed
    }
});

record.subscribe( "firstname", new RecordPathChangedCallback() {
    public void onRecordPathChanged(String recordName, String path, JsonElement data) {
        // the field "firstname" changed
    }
});
</nowiki>

您可以使用.unsubscribe()删除订阅,使用.discard()告诉服务器您不再对记录感兴趣,或使用.delete()删除记录。

Events (发布-订阅)

*event 是deepstream的发布-订阅机制。客户端和后端进程可以订阅事件名称(有时也称为“topic”或“channel”),并接收其他端点发布的消息。
*event 是非持久的一次性消息。对于永久性数据请使用records。
*event(又称Pub/Sub)允许使用发布-订阅模式进行通信。客户端/服务器emit 一个事件称为发布事件,所有连接的(订阅的)客户端/服务器都将触发收到事件的有效负载(如果有的话)。这是常见的模式,不仅在实时系统中,而且在软件工程中也是如此。

客户端和后端进程可以使用.subscribe()接收event

 <nowiki>
client.event.subscribe("test-event", new EventListener() {
    public void onEvent(String eventName, Object data) {
        // do something with data
    }
});
…
</nowiki>

可以用.emit() 发布event
client.event.emit( "test-event", "some data");

RPCs (请求-响应)

*deepstream用远程过程调用实现请求-响应机制。客户端和后端进程可以注册为给定RPC的“提供者”,由唯一名称标识。其他端点可以请求该RPC。
*deepstream会将请求路由给合适的提供者,针对同一RPC在多个提供者之间进行负载平衡,并处理数据序列化和传输。

您可以使用 .make()创建一个请求

 <nowiki>
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("a", 7);
jsonObject.addProperty("b", 8);
RpcResult rpcResult = client.rpc.make( "multiply-numbers", jsonObject);
int result = (Integer) rpcResult.getData(); // 56
</nowiki>

用.provide()响应请求

<nowiki>
client.rpc.provide("multiply-numbers", new RpcRequestedListener() {
    public void onRPCRequested(String name, Object data, RpcResponse response) {
        Gson gson = new Gson();
        JsonObject jsonData = (JsonObject) gson.toJsonTree(data);
        int a = jsonData.get("a").getAsInt();
        int b = jsonData.get("b").getAsInt();
        response.send(a * b);
    }
});
</nowiki>

从Android入门

Android平台上的deepstream入门指南

本指南将带您深入了解Deepstream的三个核心概念,即Records,Events和RPCs。

启动您服务器

让我们从安装服务器开始。只需为您的操作系统选择正确的版本,然后按部就班即可。安装服务器后,可以使用以下命令启动它:
deepstream start

连接deepstream并登陆

您需要做的第一件事是在Android项目中的build.gradle 文件里添加以下行:
compile 'io.deepstream:deepstream.io-client-java:2.0.4'
因为需要在应用程序的Activity 之间传递相同的客户端,所以我们可以使用内置DeepstreamFactory来创建客户端并对其进行引用。MainActivity您需要执行以下操作:

<nowiki>
DeepstreamFactory factory = DeepstreamFactory.getInstance();
DeepstreamClient client = factory.getClient("<Your app url">);
client.login();
</nowiki>

然后,只要我们能引用到工厂类,我们就可以用factory.getClient()得到相同的客户端。

MainActivity仅仅是一个基本的Activity,上面有三个页面按钮EventActivity,RpcActivity和RecordActivity。因此,我们暂时将其忽略,直接进入event。

Events (发布-订阅)

*event 是deepstream的发布-订阅机制。客户端和后端进程可以订阅事件名称(有时也称为“topic”或“channel”),并接收其他端点发布的消息。
*event 是非持久的一次性消息。对于永久性数据请使用records。
*event(又称Pub/Sub)允许使用发布-订阅模式进行通信。客户端/服务器emit 一个事件称为发布事件,所有连接的(订阅的)客户端/服务器都将触发收到事件的有效负载(如果有的话)。这是常见的模式,不仅在实时系统中,而且在软件工程中也是如此

Event API非常简单,我们将使用它在两个设备之间传输数据。我们需要的Android特定组件EditText做输入,Button用于发送数据,TextView用于显示此数据。
要发送EditText中的数据,我们可以执行以下操作:

 <nowiki>
submitButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String eventPayload = inputField.getText().toString();
        client.event.emit("test-event", eventPayload);
        inputField.setText("");
    }
});
</nowiki>

要在TextView显示收到的数据,我们可以执行以下操作:

 <nowiki>
client.event.subscribe("test-event", new EventListener() {
    @Override
    public void onEvent(String s, final Object o) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                outputField.setText((String) o);
            }
        });
    }
});
</nowiki>

Records (实时数据存储)

Records是deepstream实时数据存储文档。record由唯一ID标识,并且可以包含任何种类的JSON数据。客户端和后端进程可以创建、读取、写入、更新、订阅整个record或其中指定部分。任何更改都会立即在所有连接的订阅者之间同步。Records可以用列表和集合组织,还可以包含对其他记录的引用,以便对关系数据结构进行建模。您可以在“ 记录指南”中了解有关记录的更多信息。

我们将使用record(带有两个字段的firstname和lastname)在设备之间同步数据。我们的Activity还需要两个EditText。
首先要获取对的record的引用,使用Java SDK:

 <nowiki>
Record record = client.record.getRecord("test-record");
record.setMergeStrategy(MergeStrategy.REMOTE_WINS);
</nowiki>

接下来我们要在输入框上添加TextWatcher,以便每当输入新数据时record都会更新。我们将使用record.set(String path, Object data)来更新record数据。

 <nowiki>
firstnameInputField.addTextChangedListener(new CustomTextChangedWatcher("firstname"));
lastnameInputField.addTextChangedListener(new CustomTextChangedWatcher("lastname"));</nowiki>
CustomTextChangedWatcher实现如下:
 <nowiki>
 private class CustomTextChangedWatcher implements TextWatcher {

    private String field;

    CustomTextChangedWatcher(String recordField) {
        this.field = recordField;
    }
    @Override
    public void afterTextChanged(Editable s) {
        if (s.toString().length() == 0) {
            return;
        }
        record.set(field, s.toString());
    }
}
</nowiki>

然后我们需要订阅firstname和lastname字段,并在EditText中更新。这类似于上面的代码片段,我们只需将RecordPathChangedCallback包装在一个类中,并保留对某些控件的引用(在本例中为EditText)。

 <nowiki>
record.subscribe("firstname", new CustomRecordPathChangedCallback(firstnameInputField), true);
record.subscribe("lastname", new CustomRecordPathChangedCallback(lastnameInputField), true);</nowiki>

其中CustomRecordPathChangedCallback实现如下:

 <nowiki>
private class CustomRecordPathChangedCallback implements RecordPathChangedCallback {
    private EditText field;
    CustomRecordPathChangedCallback(EditText editTextField) {
        this.field = editTextField;
    }
    @Override
    public void onRecordPathChanged(String recordName, String path, final JsonElement data) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (data.isJsonNull()) {
                    return;
                }
                field.setText(data.getAsString());
                // this line just moves the cursor to the end of the text
                field.setSelection(field.getText().length());
            }
        });
    }
}
</nowiki>

RPCs (请求-响应)

*deepstream用远程过程调用实现请求-响应机制。客户端和后端进程可以注册为给定RPC的“提供者”,由唯一名称标识。其他端点可以请求该RPC。
*deepstream会将请求路由给合适的提供者,针对同一RPC在多个提供者之间进行负载平衡,并处理数据序列化和传输。

我们的演示应用程序只有一个功能,那就是使一串字符变为大写。为此,RPC提供了一个to-uppercase方法,然后使用一些字符串调用该方法。

我们还需要一些控件:
*Button submitButton;按钮,单击发起RPC 请求
*CheckBox provideCheckBox;复选框,说明是否提供RPC
*EditText inputField;输入框,用于输入数据
*TextView outputField;文本框,显示结果的文本字段

注册“提供者”的方法很简单,如果选中复选框则提供RPC,否则我们将不提供。

 <nowiki>
public void toggleProvide(View view) {
    if (provideCheckBox.isChecked()) {
        client.rpc.provide("to-uppercase", new RpcRequestedListener() {
            @Override
            public void onRPCRequested(String name, Object data, RpcResponse response) {
                String uppercaseResult = data.toString().toUpperCase();
                response.send(uppercaseResult);
            }
        });
    } else {
        client.rpc.unprovide("to-uppercase");
    }
}
</nowiki>

点击按钮时,我们将获取EditText的内容并尝试将其大写显示。

 <nowiki>
public void makeToUppercase(View view) {
    String data = inputField.getText().toString();
    final RpcResult result = client.rpc.make("to-uppercase", data);
    if (result.success()) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                outputField.setText(result.getData().toString());
            }
        });
    } else {
        Toast.makeText(this, "Error making RPC", Toast.LENGTH_LONG).show();
    }
}
</nowiki>

请记住,如果没有RPC 提供者RPC将无法完成,并将返回NO_RPC_PROVIDER错误。

上一篇下一篇

猜你喜欢

热点阅读