web-dev-qa-db-ja.com

websocketとhttpを組み合わせて、データを最新の状態に保つREST APIを作成するにはどうすればよいですか?

REST APIをwebsocketとhttpの両方で構築し、websocketを使用して、新しいデータが利用可能であることをクライアントに伝えるか、クライアントに新しいデータを直接提供することを考えています。

以下に、それがどのように機能するかについてのいくつかの異なるアイデアを示します。
ws = websocket

アイデアA:

  1. DavidはGET /usersですべてのユーザーを取得します
  2. ジェイコブはPOST /usersでユーザーを追加します
  3. 新しいユーザーが存在するという情報とともに、WSメッセージがすべてのクライアントに送信されます
  4. Davidはwsでメッセージを受け取り、GET /usersを呼び出します

アイデアB:

  1. DavidはGET /usersですべてのユーザーを取得します
  2. Davidは、/usersに変更が加えられたときにwsの更新を取得するために登録します
  3. ジェイコブはPOST /usersでユーザーを追加します
  4. 新しいユーザーはwsによってDavidに送信されます

アイデアC:

  1. DavidはGET /usersですべてのユーザーを取得します
  2. Davidは、/usersに変更が加えられたときにwsの更新を取得するために登録します
  3. JacobはPOST /usersでユーザーを追加し、ID 4を取得します
  4. Davidは、wsによって新しいユーザーのID 4を受け取ります
  5. DavidはGET /users/4で新しいユーザーを取得します

死んだ:

  1. DavidはGET /usersですべてのユーザーを取得します
  2. Davidは、/usersに変更が加えられたときにws更新を取得するために登録します。
  3. ジェイコブはPOST /usersでユーザーを追加します
  4. デビッドは、/usersに変更が加えられたというWSメッセージを受け取ります
  5. DavidはGET /users?lastcall='time of step one'を呼び出してデルタのみを取得します

どの選択肢が最良であり、長所と短所は何ですか?
別の「Idea 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リクエストの両方。

44
David Berg

私はJavaを知りませんが、これらの設計でRubyとCの両方で作業しました...

面白いことに、REST APIがmethodデータ(つまりmethod: "POST")をJSONに追加し、リクエストを同じハンドラーに転送するJSONを使用するのが最も簡単なソリューションだと思いますWebsocketが使用します。

基礎となるAPIの応答(JSON要求を処理するAPIからの応答)は、HTMLレンダリングなど、必要な任意の形式に変換できますが、ほとんどのユースケースでJSONを返すことを検討します。

これにより、DRYとWebsocketの両方を使用して同じAPIにアクセスしている間、コードをカプセル化してRESTを保持できます。

推測されるかもしれませんが、JSONを処理する基盤となるAPIはサーバーをエミュレートすることなくローカルでテストできるため、この設計によりテストが容易になります。

幸運を!

P.S。(Pub/Sub)

Pub/Subに関しては、更新APIコール(コールバック)の「フック」と、これらのことを処理する個別のPub/Subモジュールを用意するのが最適です。

また、参照番号(オプションC)または "update available"メッセージ(オプションAおよびD)だけでなく、Pub/Subサービス(オプションB)にデータ全体を書き込む方がリソースに優しいと思います。

一般に、ユーザーリスト全体を送信することは、大規模なシステムには効果的ではないと考えています。 10〜15人のユーザーがいない限り、データベース呼び出しは失敗する可能性があります。すべてのユーザーのリストを要求するAmazon管理者を考えてみてください... Brrr ....

代わりに、これをページに分割することを検討します。たとえば、1ページあたり10〜50ユーザーです。これらのテーブルは、複数のリクエスト(Websocket/REST、重要ではない)を使用して入力でき、ライブPub/Subメッセージを使用して簡単に更新するか、接続が失われて再確立された場合にリロードできます。

[〜#〜] edit [〜#〜](REST vs. Websockets)

REST対Websocketsについて...needの質問は、ほとんどの場合、「クライアントは誰ですか?」という質問のサブセットです。 ...

ただし、ロジックがトランスポート層から分離されると、両方をサポートすることは非常に簡単であり、多くの場合、両方をサポートする方が理にかなっています。

認証に関しては、Websocketのエッジはわずかであることが多いことに注意してください(資格情報は、要求ごとではなく、接続ごとに1回交換されます)。これが懸念事項かどうかはわかりません。

同じ理由(および他の理由)で、Websocketには通常、パフォーマンスに関してEdgeがあります。REST上のEdgeの大きさは、RESTトランスポート層(HTTP/1.1、HTTP/2など)。

通常、パブリックAPIアクセスポイントを提供するときが来ると、これらのことは無視できます。両方を実装することは、おそらく今のところ進むべき方法だと思います。

6
Myst

あなたのアイデアを要約するには:

A:ユーザーがサーバー上のデータを編集すると、すべてのクライアントにメッセージを送信します。その後、すべてのユーザーがすべてのデータの更新を要求します。
-このシステムは、データを使用していないクライアントに代わって多くの不要なサーバー呼び出しを行う場合があります。これらの更新を処理して送信するとコストがかかる可能性があるため、余分なトラフィックをすべて生成することはお勧めしません。

B:ユーザーがサーバーからデータを取得した後、サーバーからの更新をサブスクライブし、サーバーは変更内容に関する情報を送信します。
-これにより、サーバーのトラフィックが大幅に節約されますが、同期が取れなくなった場合は、ユーザーに誤ったデータを投稿することになります。

C:データ更新をサブスクライブするユーザーには、どのデータが更新されたかに関する情報が送信され、その後、自分でデータを再度取得します。
-これはAとBの最悪です。ユーザーとサーバー間で余分な往復が発生し、同期していない可能性のある情報を要求する必要があることを通知します。

D:更新をサブスクライブするユーザーは、変更が行われたときに通知され、サーバーに対して行われた最後の変更を要求します。
-これはCのすべての問題を提示しますが、同期が外れると、ユーザーに無意味なデータを送信して、クライアント側アプリをクラッシュさせる可能性があります。

このオプションEが最適だと思います。
サーバー上のデータが変更されるたびに、すべてのデータの内容を、サブスクライブしているクライアントに送信します。これにより、ユーザーとサーバー間のトラフィックが制限され、同期データが失われる可能性が最小限に抑えられます。接続が切断された場合、それらは古いデータを取得する可能性がありますが、少なくともエントリ5がスロット4に移動したというメッセージを受け取ったかどうかわからない場合、Delete entry 4のようなものを送信しません。

いくつかの考慮事項:

  • データはどのくらいの頻度で更新されますか?
  • 更新が発生するたびに何人のユーザーを更新する必要がありますか?
  • 送信コストはいくらですか?接続速度の遅いモバイルデバイスにユーザーがいる場合、ユーザーに送信できる頻度と量に影響します。
  • 特定の更新でどのくらいのデータが更新されますか?
  • ユーザーが古いデータを見るとどうなりますか?
  • ユーザーが同期されていないデータを取得するとどうなりますか?

最悪の場合のシナリオは次のようになります。多くのユーザーは、接続が遅く、古くなってはならない大量のデータを頻繁に更新します。

5
Glen Pierce

答えはユースケースによって異なります。ほとんどの場合、必要なものはすべてソケットで実装できることがわかりました。ソケットをサポートできるクライアントでのみサーバーにアクセスしようとしている限り。また、ソケットのみを使用している場合、スケールが問題になる可能性があります。ソケットを使用する方法の例を次に示します。

サーバ側:

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エンドポイントも含める必要があります。

3
Brian Baker

すべての素晴らしい情報で、すべての素晴らしい人々が私の前に追加しました。

最終的には正しいか間違っているかはわかりませんが、単にニーズに合ったものになります:

このシナリオで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を介して、このコードを2回使用できます。1回はページの読み込み時に、2回目はWSが特定のイベントをトリガーしたときに使用できます。

[4]現在、古いデータのビューのみを表示しているため、クライアントは新しいユーザーを取得したくないかもしれません。ユーザー]、および新しいデータの変更は取得することに関心がありませんか?

3
Robocide

別のオプションは Firebase Cloud Messaging を使用することです:

FCMを使用すると、新しい電子メールまたはその他のデータを同期できることをクライアントアプリに通知できます。

どのように機能しますか?

FCM実装には、送信および受信用の2つの主要コンポーネントが含まれます。

  • Cloud Functions for Firebaseなどの信頼できる環境、またはメッセージを作成、ターゲット設定、送信するアプリサーバー。
  • メッセージを受信するiOS、Android、またはWeb(JavaScript)クライアントアプリ。

クライアントは、Firebaseキーをサーバーに登録します。更新が利用可能になると、サーバーはクライアントに関連付けられたFirebaseキーにプッシュ通知を送信します。クライアントは、通知構造でデータを受信するか、通知を受信した後にサーバーと同期します。

3
Justas

一般的に、この問題に正確に取り組む MeteorJS のような現在の「リアルタイム」Webフレームワークを見ることができます。

特定のデータのMeteorは、影響を受けるクライアントにのみ変更が加えられた後に特定のデータとデルタが送信される例Dとほぼ同じように機能します。使用されるプロトコルは [〜#〜] ddp [〜#〜] と呼ばれ、これはオーバーヘッドが発生しやすいHTMLではなく生データとしてデルタを追加的に送信します。

WebSocketが利用できない場合、 long pollingまたはserver sent events のようなフォールバックを使用できます。

自分で実装する予定がある場合は、これらのソースがこの問題にどのようにアプローチしたかのインスピレーションの一種であることを願っています。すでに述べたように、特定のユースケースは重要です

3

私は個人的にプロダクションでIdea Bを使用しましたが、結果に非常に満足しています。 http://www.axonframework.org/ を使用しているため、エンティティのすべての変更または作成は、アプリケーション全体でイベントとして公開されます。これらのイベントは、いくつかの読み取りモデルを更新するために使用されます。これらのモデルは、基本的に1つ以上のクエリをサポートする単純なMysqlテーブルです。これらの読み取りモデルを更新するイベントプロセッサにインターセプターをいくつか追加し、データがDBにコミットされた後に処理したばかりのイベントを発行するようにしました。

イベントの発行は、Webソケットを介したSTOMPを介して行われます。 SpringのWebソケットサポート( 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);
}

私は次のようにAxonイベントハンドラーに注釈を付けることができるように、Spring BeanファクトリAPIを使用する小さなコンフィギュレーターを作成しました。

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

もちろん、それはそれを行う1つの方法にすぎません。クライアント側での接続は次のようになります。

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();
1
CrimsonCricket