【问题标题】:How to combine websockets and http to create a REST API that keeps data up to date? [closed]如何结合 websockets 和 http 来创建一个保持数据最新的 REST API? [关闭]
【发布时间】:2016-01-30 05:00:51
【问题描述】:

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

以下是关于它如何工作的一些不同想法:
ws = websocket

想法一:

  1. David 使用GET /users 获取所有用户
  2. Jacob 添加一个用户POST /users
  3. 向所有客户端发送一条 ws 消息,其中包含新用户存在的信息
  4. David 收到 ws 的消息并致电 GET /users

想法 B:

  1. David 使用GET /users 获取所有用户
  2. /users 发生更改时,David 注册以获取 ws 更新
  3. Jacob 添加一个用户POST /users
  4. 新用户由ws发送给David

想法 C:

  1. David 使用GET /users 获取所有用户
  2. 当对/users 进行更改时,David 注册以获取 ws 更新
  3. Jacob 用POST /users 添加一个用户,它的 id 为 4
  4. David 通过 ws 接收到新用户的 id 4
  5. David 通过GET /users/4 获取新用户

想法 D:

  1. David 使用GET /users 获取所有用户
  2. David 注册以在对 /users 进行更改时获取 ws 更新。
  3. Jacob 添加一个用户POST /users
  4. David 收到一条 ws 消息,指出对 /users 的更改已完成
  5. David 通过调用GET /users?lastcall='time of step one' 仅获取增量

哪种选择是最好的,优缺点是什么?
它是另一个更好的“创意 E”吗?
我们是否甚至需要使用 REST 或者 ws 是否足以处理所有数据?

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

【问题讨论】:

  • @Robocide,你能再解释一下你的赏金吗?我不确定你的意思。

标签: rest websocket jax-rs restful-architecture


【解决方案1】:

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

我们是否甚至需要使用 REST 或者 ws 是否足以处理所有数据?

请查看:WebSocket/REST: Client connections?

【讨论】:

  • 感谢您的回答!我也认为我更喜欢想法 B,但对这篇文章有了新的想法。是否也使用 ws 来保存实体是个好主意?
  • 这就是我试图在我包含的链接中回答的问题。 WS 是双向的,所以从技术上讲,这是可能的。但是,当您需要扩展您的解决方案时,您需要将写入与读取分开,如果您通过同一个 websocket 做所有事情,那将是不可能的。
【解决方案2】:

总结你的想法:

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

B:用户从服务器提取数据后,他们会订阅来自服务器的更新,服务器会向他们发送有关已更改内容的信息。
-这可以节省大量服务器流量,但如果你曾经不同步,您将向用户发布不正确的数据。

C:订阅数据更新的用户会收到有关哪些数据已更新的信息,然后自己再次获取。
-这是 A 和 B 中最糟糕的一个,因为您将获得额外的回合您的用户和服务器之间的行程只是为了通知他们他们需要请求可能不同步的信息。

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

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

一些注意事项:

  • 数据多久更新一次?
  • 每次更新需要更新多少用户?
  • 你的传输是什么 费用?如果您的用户使用连接速度较慢的移动设备,这将影响您向他们发送数据的频率和金额。
  • 在给定的更新中更新了多少数据?
  • 如果用户看到过时的数据会怎样?
  • 如果用户的数据不同步会怎样?

您最糟糕的情况是这样的:很多用户连接速度很慢,他们经常更新大量不应过时的数据,如果数据不同步,就会产生误导。

【讨论】:

  • 感谢您的回复。我认为你对选项 E 的建议是我对选项 B 的想法。如果这个例子不够好,我很抱歉。另外,关于您担心数据会不同步,这可以通过在 post 调用中提供标题“If-Not-Modified-Since”来解决。 developer.mozilla.org/en-US/docs/Web/HTTP/Headers/…
  • 如何确定您的服务器时间和用户时间相同?如果您开始使用微服务,您将如何确保服务器的时间同步?在记录时间戳不同步的分布式系统上对错误情况进行故障排除并不有趣。
  • 您可以将上次编辑的日期存储在服务器上您在获取请求时提供给客户端的资源上,然后在发送“If-Unmodified-Since”标头时使用此日期。跨度>
  • 然而这并不能确保服务器时间同步,但我认为你应该在分布式系统中有工具来确保这一点。
  • 我阅读了您的编辑。如果最坏的情况是很多用户的连接速度很慢,那么最好尽可能少地发送,只发送增量,也许是整个对象的校验和。这样,客户端可以查看数据是否不同步,并仅在发生时才请求完整集。我越是想到这一点,我就越倾向于使用“D”替代方案的某个版本,甚至更喜欢完整的 ws 实现。
【解决方案3】:

另一种选择是使用Firebase Cloud Messaging

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

它是如何工作的?

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

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

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

【讨论】:

  • 我认为 Firebase 使用 websockets 进行通信,至少对于网页而言。我测试过的另一个类似产品是 Deepstream,如果您想使用完整的 websocket 解决方案,它是一个不错的选择。 (deepstream.io)
【解决方案4】:

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

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

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

很多事情取决于您正在制作什么样的应用程序。

【讨论】:

  • 是什么让备选方案 A 的访问控制比使用 GET 获取数据的 C 或 D 更容易?
【解决方案5】:

一般来说,您可能会看看当前的“实时”网络框架,例如 MeteorJS,它们正好解决了这个问题。

特定作品中的 Meteor 或多或少类似于您的示例 D,其中订阅某些数据和 delta 版仅在更改后发送给受影响的客户端。他们使用的协议称为DDP,它另外发送的增量不是易开销的 HTML,而是原始数据。

如果 websocket 不可用,则可以使用 long polling or server sent events 之类的后备。

如果您打算自己实现它,我希望这些资源能够启发您如何解决这个问题。如前所述,具体的用例很重要

【讨论】:

    【解决方案6】:

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

    服务器端:

    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 端点也可以。

    【讨论】:

      【解决方案7】:

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

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

      可以将底层 API 的响应(来自处理 JSON 请求的 API 的响应)转换为您需要的任何格式,例如 HTML 渲染...尽管我会考虑在大多数用例中简单地返回 JSON。

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

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

      祝你好运!

      附言(发布/订阅)

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

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

      一般来说,我还认为发送整个用户列表对大型系统无效。除非您有 10-15 个用户,否则数据库调用可能会失败。考虑一下亚马逊管理员调用所有用户的列表...... Brrr......

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

      编辑(REST 与 Websockets)

      至于 REST 与 Websockets...我发现 need 的问题主要是“谁是客户?”这个问题的一个子集...

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

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

      出于同样的原因(以及其他原因),Websockets 通常在性能方面具有优势......相对于 REST 的优势有多大取决于 REST 传输层(HTTP/1.1、HTTP/2 等) .

      当需要提供公共 API 访问点时,这些东西通常可以忽略不计,我相信实现这两者可能是目前要走的路。

      【讨论】:

        【解决方案8】:

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

        事件的发布是通过网络套接字上的 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();
        

        【讨论】:

          【解决方案9】:

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

          我发现最终没有对错,它只是归结为适合您的需求:

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

          仅 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.
          

          WS 发送事件信息 + REST 使用数据本身

          Create/Read/Update/Deleted , Event information is sent in the Websocket,
          giving the web client information that is necessary to send the proper 
          REST request to fetch exactly the thing the CRUD that happend in server.
          

          例如WS 发送 UsersListChangedEvent {"ListChangedTrigger: "ItemModified" , "IdOfItem":"XXXX#3232" , "UserExtrainformation":"足够的信息让客户端决定它是否与获取更改的数据相关"}

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

          [1] 读取和写入模型之间的分离,假设您想在从 REST 读取数据时检索数据时添加一些运行时信息,现在实现了,因为您没有像 1 中那样混合写入和读取模型。

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

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

          [4] 客户端可能不想获取新用户,因为它当前只显示旧数据的视图[例如。 users] ,并且新的数据更改不符合其获取的利益?

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-07-25
            • 2014-02-22
            • 1970-01-01
            • 2018-08-20
            • 1970-01-01
            • 2019-10-17
            • 2013-03-12
            相关资源
            最近更新 更多