Je pense à construire une API REST avec les websockets et http où j'utilise les websockets pour dire au client que de nouvelles données sont disponibles ou fournir les nouvelles données directement au client.
Voici quelques idées différentes sur la façon dont cela pourrait fonctionner:
ws = websocket
Idée A:
GET /users
POST /users
GET /users
Idée B:
GET /users
/users
POST /users
Idée C:
GET /users
/users
POST /users
et il obtient l'id 4GET /users/4
Idée D:
GET /users
/users
.POST /users
/users
GET /users?lastcall='time of step one'
Quelle alternative est la meilleure et quels sont les avantages et les inconvénients?
Est-ce une autre meilleure "idée E"?
Avons-nous même besoin d'utiliser REST ou ws est-il suffisant pour toutes les données?
Modifier
Pour résoudre les problèmes de désynchronisation des données, nous pourrions fournir l'en-tête
"Si non modifié depuis"
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since
ou "E-Tag"
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
ou les deux avec des demandes PUT.
L'idée B est pour moi la meilleure, car le client s'abonne spécifiquement aux modifications d'une ressource et obtient les mises à jour incrémentielles à partir de ce moment.
Avons-nous même besoin d'utiliser REST ou ws est-il suffisant pour toutes les données?
Veuillez vérifier: WebSocket/REST: connexions client?
Je ne connais pas Java, mais j'ai travaillé avec les deux Ruby et C sur ces conceptions ...
Assez drôle, je pense que la solution la plus simple consiste à utiliser JSON, où l'API REST ajoute simplement les données method
(c'est-à-dire method: "POST"
) au JSON et transmet la demande au même gestionnaire que le Websocket utilise.
La réponse de l'API sous-jacente (la réponse de l'API gérant les requêtes JSON) peut être traduite dans n'importe quel format dont vous avez besoin, comme le rendu HTML ... bien que je considérerais simplement de renvoyer JSON pour la plupart des cas d'utilisation.
Cela permet d'encapsuler le code et de le conserver DRY tout en accédant à la même API en utilisant à la fois REST et Websockets).
Comme vous pouvez en déduire, cette conception facilite les tests, car l'API sous-jacente qui gère le JSON peut être testée localement sans avoir besoin d'émuler un serveur.
Bonne chance!
P.S. (Pub/Sub)
En ce qui concerne le Pub/Sub, je trouve préférable d'avoir un "hook" pour tous les appels d'API de mise à jour (un rappel) et un module Pub/Sub séparé qui gère ces choses.
Je trouve également qu'il est plus convivial d'écrire toutes les données dans le service Pub/Sub (option B) au lieu d'un simple numéro de référence (option C) ou d'un message "mise à jour disponible" (options A et D).
En général, je pense également que l'envoi de la liste complète des utilisateurs n'est pas efficace pour les grands systèmes. Sauf si vous avez 10 à 15 utilisateurs, l'appel à la base de données peut être un échec. Considérez l'administrateur Amazon appelant à une liste de tous les utilisateurs ... Brrr ....
Au lieu de cela, j'envisagerais de diviser cela en pages, disons 10 à 50 utilisateurs par page. Ces tableaux peuvent être remplis à l'aide de plusieurs demandes (Websocket/REST, peu importe) et facilement mis à jour à l'aide de messages Pub/Sub en direct ou rechargés si une connexion a été perdue et rétablie.
MODIFIER (REST vs Websockets)
Quant à REST vs Websockets ... je trouve que la question de besoin est principalement un sous-ensemble de la question " qui est le client? "...
Cependant, une fois que la logique est séparée de la couche de transport, la prise en charge des deux est très facile et il est souvent plus logique de prendre en charge les deux.
Je dois noter que les Websockets ont souvent un léger avantage en matière d'authentification (les informations d'identification sont échangées une fois par connexion au lieu d'une fois par demande). Je ne sais pas si c'est une préoccupation.
Pour la même raison (ainsi que d'autres), les Websockets ont généralement un Edge en termes de performances ... la taille d'un Edge sur REST dépend de REST = couche de transport (HTTP/1.1, HTTP/2, etc ').
Habituellement, ces choses sont négligeables quand vient le temps d'offrir un point d'accès API public et je pense que la mise en œuvre des deux est probablement la voie à suivre pour le moment.
Pour résumer vos idées:
R: Envoyez un message à tous les clients lorsqu'un utilisateur modifie des données sur le serveur. Tous les utilisateurs demandent alors une mise à jour de toutes les données.
-Ce système peut effectuer de nombreux appels de serveur inutiles au nom de clients qui n'utilisent pas les données. Je ne recommande pas de produire tout ce trafic supplémentaire car le traitement et l'envoi de ces mises à jour pourraient devenir coûteux.
B: Après qu'un utilisateur a extrait des données du serveur, il s'abonne aux mises à jour du serveur qui lui envoie des informations sur ce qui a changé.
-Cela permet d'économiser beaucoup de trafic sur le serveur, mais si jamais vous vous désynchronisez, vous allez publier des données incorrectes pour vos utilisateurs.
C: Les utilisateurs qui s'abonnent aux mises à jour de données reçoivent des informations sur les données qui ont été mises à jour, puis les récupèrent eux-mêmes.
-C'est le pire de A et B dans la mesure où vous aurez des allers-retours supplémentaires entre vos utilisateurs et serveurs juste pour les informer qu'ils doivent faire une demande d'informations qui peuvent être désynchronisées.
D: Les utilisateurs qui s'abonnent aux mises à jour sont informés lorsque des modifications sont apportées, puis demandent la dernière modification apportée au serveur.
-Cela présente tous les problèmes avec C, mais inclut la possibilité que, une fois désynchronisé, vous puissiez envoyer des données qui n'auront aucun sens à vos utilisateurs, ce qui pourrait simplement planter l'application côté client pour tout ce que nous savons.
Je pense que cette option E serait la meilleure:
Chaque fois que des données changent sur le serveur, envoyez le contenu de toutes les données aux clients qui y sont abonnés. Cela limite le trafic entre vos utilisateurs et le serveur tout en leur donnant également le moins de chances d'avoir des données désynchronisées. Ils pourraient obtenir des données périmées si leur connexion tombe, mais au moins vous ne leur enverriez pas quelque chose comme Delete entry 4
lorsque vous ne savez pas s'ils ont reçu ou non le message que l'entrée 5 vient d'être déplacée dans l'emplacement 4.
Quelques considérations:
Votre pire scénario serait quelque chose comme ceci: beaucoup d'utilisateurs, avec des connexions lentes qui mettent fréquemment à jour de grandes quantités de données qui ne devraient jamais être périmées et, si elles ne sont plus synchronisées, deviennent trompeuses.
La réponse dépend de votre cas d'utilisation. Pour la plupart, j'ai trouvé que vous pouvez implémenter tout ce dont vous avez besoin avec des sockets. Tant que vous essayez d'accéder à votre serveur uniquement avec des clients qui peuvent prendre en charge les sockets. En outre, l'échelle peut être un problème lorsque vous utilisez uniquement des sockets. Voici quelques exemples d'utilisation de sockets uniquement.
Du côté serveur:
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);
})
Côté client:
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
})
Cela utilise socket.io pour le nœud. Je ne sais pas quel est votre scénario exact, mais cela fonctionnerait pour ce cas. Si vous devez inclure REST endpoints, ce serait bien aussi.
Avec toutes les bonnes informations, toutes les personnes formidables se sont ajoutées devant moi.
J'ai trouvé que finalement il n'y a pas de bien ou de mal, ça dépend simplement de ce qui convient à vos besoins:
permet de prendre CRUD dans ce scénario:
Approche WS uniquement:
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 POUR ENVOYER DES INFORMATIONS SUR L'ÉVÉNEMENT + REST POUR CONSOMMER LES DONNÉES
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.
par exemple. WS envoie UsersListChangedEvent {"ListChangedTrigger:" ItemModified "," IdOfItem ":" XXXX # 3232 "," UserExtrainformation ":" Assez d'informations pour laisser le client décider s'il est pertinent pour lui de récupérer les données modifiées "}
J'ai trouvé qu'il était préférable d'utiliser WS [uniquement pour utiliser les données d'événement] et REST [Pour consommer les données] parce que:
[1] Séparation entre le modèle de lecture et d'écriture, imaginez que vous souhaitez ajouter des informations d'exécution lorsque vos données sont récupérées lors de leur lecture à partir de REST, cela est maintenant réalisé parce que vous ne mélangez pas Write & Read modèles comme dans 1.
[2] Disons qu'une autre plate-forme, pas nécessairement un client Web, consommera ces données. il vous suffit donc de changer le déclencheur d'événement de WS à la nouvelle façon et d'utiliser REST pour consommer les données.
[3] Le client n'a pas besoin d'écrire 2 façons de lire les données nouvelles/modifiées. généralement, il y a aussi du code qui lit les données lors du chargement de la page, et non
via le Websocket, ce code peut désormais être utilisé deux fois, une fois lors du chargement de la page et deuxièmement lorsque WS a déclenché l'événement spécifique.
[4] Peut-être que le client ne veut pas récupérer le nouvel utilisateur car il ne montre actuellement qu'une vue des anciennes données [ex. utilisateurs], et les nouvelles modifications de données ne sont pas dans son intérêt à récupérer?
Une autre option consiste à utiliser Firebase Cloud Messaging :
À l'aide de FCM, vous pouvez informer une application cliente que de nouveaux e-mails ou d'autres données sont disponibles pour la synchronisation.
Comment ça marche?
Une implémentation FCM comprend deux composants principaux pour l'envoi et la réception:
- Un environnement de confiance tel que Cloud Functions for Firebase ou un serveur d'applications sur lequel créer, cibler et envoyer des messages.
- Une application client iOS, Android ou Web (JavaScript) qui reçoit des messages.
Le client enregistre sa clé Firebase sur un serveur. Lorsque des mises à jour sont disponibles, le serveur envoie une notification Push à la clé Firebase associée au client. Le client peut recevoir des données dans la structure de notification ou les synchroniser avec un serveur après avoir reçu une notification.
En général, vous pouvez jeter un œil aux frameworks Web "en temps réel" comme MeteorJS qui résolvent exactement ce problème.
Meteor fonctionne plus ou moins comme votre exemple D avec des abonnements sur certaines données et deltas envoyés après des modifications uniquement aux clients concernés. Leur protocole utilisé est appelé DDP qui envoie en outre les deltas non pas en HTML sujettes aux frais généraux mais en données brutes.
Si les websockets ne sont pas disponibles, des solutions de rechange comme interrogation longue ou événements envoyés par le serveur peuvent être utilisées.
Si vous prévoyez de le mettre en œuvre vous-même, j'espère que ces sources sont une sorte d'inspiration pour la façon dont ce problème a été abordé. Comme déjà indiqué, le cas d'utilisation spécifique est important
J'ai personnellement utilisé Idea B dans la production et je suis très satisfait des résultats. Nous utilisons http://www.axonframework.org/ , donc chaque modification ou création d'une entité est publiée en tant qu'événement dans l'application. Ces événements sont ensuite utilisés pour mettre à jour plusieurs modèles de lecture, qui sont essentiellement de simples tables Mysql sauvegardant une ou plusieurs requêtes. J'ai ajouté des intercepteurs aux processeurs d'événements qui mettent à jour ces modèles de lecture afin qu'ils publient les événements qu'ils viennent de traiter une fois les données validées dans la base de données.
La publication des événements se fait via STOMP sur des sockets Web. C'est très simple si vous utilisez le support Web Socket de Spring ( https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html ). Voici comment je l'ai écrit:
@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);
}
J'ai écrit un petit configurateur qui utilise l'API Factory de bean Springs pour que je puisse annoter mes gestionnaires d'événements Axon comme ceci:
@PublishToTopics({
@PublishToTopic(value = "/salary-table/{agreementId}/{salaryTableId}", eventClass = SalaryTableChanged.class),
@PublishToTopic(
value = "/salary-table-replacement/{agreementId}/{activatedTable}/{deactivatedTable}",
eventClass = ActiveSalaryTableReplaced.class
)
})
Bien sûr, ce n'est qu'une façon de procéder. La connexion côté client peut ressembler à ceci:
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();