如何组合websockets和http来创建一个使数据保持最新的REST API?

时间:2015-10-29 21:08:49

标签: rest websocket jax-rs restful-architecture

我正在考虑使用websockets和http构建REST API,我使用websockets告诉客户端新数据可用或直接向客户端提供新数据。

以下是一些有关如何运作的不同想法:
ws = websocket

想法A:

  1. David向所有用户GET /users
  2. Jacob使用POST /users
  3. 添加用户
  4. 向所有客户发送一条ws消息,其中包含新用户存在的信息
  5. David通过ws收到一条消息并致电GET /users
  6. 创意B:

    1. David向所有用户GET /users
    2. David在/users
    3. 进行更改后注册以获取ws更新
    4. Jacob使用POST /users
    5. 添加用户
    6. 新用户通过ws
    7. 发送给David

      想法C:

      1. David向所有用户GET /users
      2. David在/users
      3. 进行更改后注册以获取ws更新
      4. Jacob添加POST /users的用户,并获得ID 4
      5. David通过ws
      6. 收到新用户的ID 4
      7. David使用GET /users/4
      8. 获取新用户

        想法D:

        1. David向所有用户GET /users
        2. David对/users进行更改时注册以获取ws更新。
        3. Jacob使用POST /users
        4. 添加用户
        5. David收到ws消息,指出已对/users
        6. 进行了更改
        7. David只通过调用GET /users?lastcall='time of step one'
        8. 获取delta

          哪种替代方案最好,有什么优点和缺点? 这是另一个更好的想法E'? 我们甚至需要使用REST还是需要所有数据?

          修改
          为了解决数据不同步的问题,我们可以提供标题
          " If-Unmodified-Since"
          https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since
          或" E-标记"
          https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
          或两者都有PUT请求。

9 个答案:

答案 0 :(得分:9)

创意B对我来说是最好的,因为客户专门订阅资源中的更改,并从那一刻开始获取增量更新。

  

我们是否需要使用REST或者是否需要使用所有数据?

请检查:WebSocket/REST: Client connections?

答案 1 :(得分:5)

我不懂Java,但我在Ruby和C上都使用过这些设计......

有趣的是,我认为最简单的解决方案是使用JSON,其中REST API只是将method数据(即method: "POST")添加到JSON,并将请求转发给Websocket的相同处理程序使用

底层API的响应(处理JSON请求的API的响应)可以转换为您需要的任何格式,例如HTML呈现......虽然我会考虑在大多数用例中返回JSON。 / p>

这有助于封装代码并在使用REST和Websockets访问同一API时保持干燥。

正如您可能推断的那样,这种设计使测试更容易,因为处理JSON的底层API可以在本地进行测试,而无需模拟服务器。

祝你好运!

P.S。(发布/订阅)

对于Pub / Sub,我发现最好有一个" hook"对于任何更新API调用(回调)和处理这些事情的单独的Pub / Sub模块。

我还发现将整个数据写入Pub / Sub服务(选项B)而不仅仅是参考号(选项C)或者"可用更新"更加资源友好。消息(选项A和D)。

总的来说,我也相信发送整个用户列表对大型系统来说并不有效。除非你有10-15个用户,否则数据库调用可能会破灭。考虑亚马逊管理员要求所有用户的列表... Brrr ....

相反,我会考虑将其划分为页面,例如每页10-50个用户。这些表可以使用多个请求(Websocket / REST,无关紧要)填充,并可以使用实时发布/订阅消息轻松更新,或者在连接丢失和重新建立时重新加载。

编辑(REST与Websockets)

至于REST与Websockets ......我发现 need 的问题主要是问题的一部分"谁是客户?" ..

然而,一旦逻辑与传输层分离,支持两者就非常容易,并且通常支持两者都更有意义。

我应该注意到,Websockets在身份验证方面通常会略有优势(每个连接交换一次凭据,而不是每个请求交换一次)。我不知道这是否是一个问题。

出于同样的原因(以及其他原因),Websockets通常在性能方面具有优势...... REST的边缘有多大依赖于REST传输层(HTTP / 1.1,HTTP / 2等等) ;)

通常,当提供公共API访问点时,这些事情可以忽略不计,我相信实现这两者可能是现在的方法。

答案 2 :(得分:4)

总结您的想法:

答:当用户在服务器上编辑数据时,向所有客户端发送消息。然后所有用户都要求更新所有数据 - 该系统可能代表未使用数据的客户端进行大量不必要的服务器调用。我不建议产生所有额外的流量,因为处理和发送这些更新可能会变得很昂贵。

B:用户从服务器提取数据后,他们会从服务器订阅更新,并向他们发送有关更改内容的信息。
- 这样可以节省大量的服务器流量,但如果您不同步,则会向用户发布错误的数据。

C:订阅数据更新的用户会收到有关哪些数据已更新的信息,然后自行重新获取。
- 这是A和B中最差的,因为你的用户和服务器之间会有额外的往返行程,只是为了通知他们需要请求可能不同步的信息。

D:订阅更新的用户会在做出任何更改时收到通知,然后请求对服务器进行最后一次更改。
- 这提出了C的所有问题,但包括一旦不同步,您可能会向您的用户发送无意义的数据,这可能会让我们所知道的客户端应用程序崩溃。

我认为这个选项E最好:
每次数据在服务器上发生变化时,都会将所有数据的内容发送给订阅它的客户端。这限制了用户与服务器之间的流量,同时也使他们获得不同步数据的机会最少。如果他们的连接断开,他们可能会得到陈旧的数据,但是当你不确定他们是否收到条目5刚刚移入插槽4的消息时,至少你不会发送类似Delete entry 4的信息。 / p>

一些注意事项:

  • 数据更新的频率是多少?
  • 每次更新时需要更新多少用户?
  • 你的传输是什么? 成本?如果您的移动设备上的用户连接速度较慢,则会影响您发送给他们的频率和数量。
  • 在给定的更新中更新了多少数据?
  • 如果用户看到陈旧数据会怎样?
  • 如果用户的数据不同步会怎样?

你最糟糕的情况是这样的:很多用户,连接速度慢的人经常更新大量的数据,这些数据永远不会过时,如果不同步,会变得误导。

答案 3 :(得分:3)

另一种选择是使用Firebase Cloud Messaging

  

使用FCM,您可以通知客户端应用程序新电子邮件或其他数据   可以同步。

     

它是如何运作的?

     

FCM实现包括两个用于发送和发送的主要组件   接收:

     
      
  • 受信任的环境,例如Cloud Functions for Firebase或构建,定位和发送邮件的应用服务器。
  •   
  • 接收消息的iOS,Android或Web(JavaScript)客户端应用程序。
  •   

客户端将其Firebase密钥注册到服务器。当更新可用时,服务器会将推送通知发送到与客户端关联的Firebase密钥。客户端可能会收到通知结构中的数据,或在收到通知后与服务器同步。

答案 4 :(得分:3)

通常,您可能会查看当前的“实时”Web框架,例如MeteorJS,它可以解决这个问题。

特定的Meteor工作或多或少类似于您的示例D,对特定数据的订阅和仅在更改后发送到受影响的客户端的增量。他们使用的协议称为DDP,它还发送增量不是开销的HTML而是原始数据。

如果无法使用websockets,则可以使用long polling or server sent events之类的后退。

如果您打算自己实施它,我希望这些来源是如何解决这个问题的某种灵感。如前所述,具体用例很重要

答案 5 :(得分:3)

答案取决于您的使用案例。在大多数情况下,虽然我发现你可以用套接字实现你需要的一切。只要您尝试使用可以支持套接字的客户端访问您的服务器。此外,当您仅使用套接字时,缩放可能是一个问题。以下是一些如何使用套接字的示例。

服务器端:

socket.on('getUsers', () => {
    // Get users from db or data model (save as user_list).
    socket.emit('users', user_list );
})
socket.on('createUser', (user_info) => {
    // Create user in db or data model (save created user as user_data).
    io.sockets.emit('newUser', user_data);
})

客户方:

socket.on('newUser', () => {
    // Get users from db or data model (save as user_list).
    socket.emit('getUsers');
})
socket.on('users', (users) => {       
    // Do something with users
})

这使用socket.io作为节点。我不确定你的具体情况是什么,但这适用于那种情况。如果您需要包含可能没问题的REST端点。

答案 6 :(得分:2)

所有伟大的人都在我面前添加了所有重要信息。

我发现最终没有对错,它只是适合你的需要:

让我们在这种情况下采用CRUD:

WS Only Approach:

subversion

WS发送事件信息+休息以自己消耗数据

Create/Read/Update/Deleted information goes all through the websocket.    
--> e.g If you have critical performance considerations ,that is not 
acceptable that the web client will do successive REST request to fetch 
information,or if you know that you want the whole data to be seen in 
the client no matter what was the event ,  so just send the CRUD events 
AND DATA inside the websocket.

e.g。 WS发送UsersListChangedEvent {" ListChangedTrigger:" ItemModified" ," IdOfItem":" XXXX#3232" ," UserExtrainformation":"足够的信息让客户决定是否与其相关以获取已更改的数据"}

我发现使用WS [仅用于使用事件数据]和REST [使用数据]更好,因为:

[1]读写模型之间的分离,想象一下,当你从REST读取数据时,你想要在检索数据时添加一些运行时信息,这是因为你没有混合Write和amp;阅读1中的模型。

[2]让我们说其他平台,不一定是Web客户端会消耗这些数据。     所以你只需将事件触发器从WS更改为新方式,并使用REST     消费数据。

[3]客户端不需要编写两种方法来读取新的/修改过的数据。     通常还有代码在页面加载时读取数据,而不是     通过websocket,这段代码现在可以使用两次,一次是在页面上     在WS触发特定事件时加载,第二次。

[4]也许客户端不想获取新用户,因为它当前只显示旧数据[例如。用户]和新数据更改是不是有兴趣获取?

答案 7 :(得分:1)

我个人在制作中使用了Idea B,对结果非常满意。我们使用http://www.axonframework.org/,因此实体的每次更改或创建都将作为整个应用程序中的事件发布。然后使用这些事件更新几个读取模型,这些模型基本上是支持一个或多个查询的简单Mysql表。我在更新这些读取模型的事件处理器中添加了一些拦截器,以便在将数据提交到数据库之后发布它们刚刚处理的事件。

通过Web套接字通过STOMP完成事件的发布。使用Spring的Web Socket支持(https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html)非常简单。这就是我写它的方式:

@Override
protected void dispatch(Object serializedEvent, String topic, Class eventClass) {
    Map<String, Object> headers = new HashMap<>();
    headers.put("eventType", eventClass.getName());
    messagingTemplate.convertAndSend("/topic" + topic, serializedEvent, headers);
}

我编写了一个使用Springs bean factory API的小型配置器,以便我可以像这样注释我的Axon事件处理程序:

@PublishToTopics({
    @PublishToTopic(value = "/salary-table/{agreementId}/{salaryTableId}", eventClass = SalaryTableChanged.class),
    @PublishToTopic(
            value = "/salary-table-replacement/{agreementId}/{activatedTable}/{deactivatedTable}",
            eventClass = ActiveSalaryTableReplaced.class
    )
})

当然,这只是一种方法。客户端的连接可能如下所示:

var connectedClient = $.Deferred();

function initialize() {
    var basePath = ApplicationContext.cataDirectBaseUrl().replace(/^https/, 'wss');
    var accessToken = ApplicationContext.accessToken();
    var socket = new WebSocket(basePath + '/wss/query-events?access_token=' + accessToken);
    var stompClient = Stomp.over(socket);

    stompClient.connect({}, function () {
        connectedClient.resolve(stompClient);
    });
}


this.subscribe = function (topic, callBack) {
    connectedClient.then(function (stompClient) {
        stompClient.subscribe('/topic' + topic, function (frame) {
            callBack(frame.headers.eventType, JSON.parse(frame.body));
        });
    });
};

initialize();

答案 8 :(得分:-1)

我更喜欢A,它允许客户灵活性是否更新现有数据。

同样使用这种方法,实现和访问控制变得更加容易。

例如,您可以简单地向所有用户广播userUpdated事件,这可以节省用于执行特定广播的客户端列表,并且应用于您的REST路由的Access Controls and Authentications不必更改为重新应用,因为客户端将再次发出GET请求。

很多事情取决于你正在做什么样的应用程序。