Synchronisation Multi-Clients <=> Serveur
muqaddar
Administrateur
Bonsoir à tous,
Dans mon application iPhone, j'utilise déjà la synchronisation serveur => client pour une série de modifications ou suppressions possibles sur quelques tables de données SQL.
En clair, j'ai développé un WebServices en Ruby sur mon site Internet où j'ai une administration qui enregistre mes actions dans une table. Ensuite, je génère des enregistrements SQL et une version associée pour ces changements.
Depuis l'iPhone, lorsque je lance une synchronisation dans mon application, j'appelle une URL avec contrôle de version BDD qui me renvoie un fichier XML que je parse, et qui génère mes requêtes SQL en local pour modifier ma base locale.
Jusque là tout va bien.
En revanche, je vais essayer de faire la pirouette inverse, à savoir envoyer les données de la base iPhone vers le serveur. A première vue, le mieux est aussi de générer un fichier XML en local et de l'envoyer en requête HTTP POST ? A l'arrivée, je peux faire un parser en Ruby qui fait le boulot inverse ? Quelle est la meilleure méthode pour expédier cet XML sur le serveur ? Quelles sont les règles de base pour ne pas envoyer toute la base du client à chaque fois ? Faut-il également enregistrer toutes ses actions ou se servir de champs created_at, updated_at dans chaque table par exemple ? Je suppose qu'il y a des choses à faire et ne pas faire en synchronisation.
Pour l'instant, le client ne peut modifier ses données que sur l'iPhone mais pourquoi pas un jour sur le serveur... En attendant, ça lui servira de backup s'il a un soucis avec son app client.
Bref, si vous avez des pistes, ou bien un bon lien (que j'ai du mal à trouver) pour faire de la synchronisation avec Cocoa, n'hésitez pas.
Dans mon application iPhone, j'utilise déjà la synchronisation serveur => client pour une série de modifications ou suppressions possibles sur quelques tables de données SQL.
En clair, j'ai développé un WebServices en Ruby sur mon site Internet où j'ai une administration qui enregistre mes actions dans une table. Ensuite, je génère des enregistrements SQL et une version associée pour ces changements.
Depuis l'iPhone, lorsque je lance une synchronisation dans mon application, j'appelle une URL avec contrôle de version BDD qui me renvoie un fichier XML que je parse, et qui génère mes requêtes SQL en local pour modifier ma base locale.
Jusque là tout va bien.
En revanche, je vais essayer de faire la pirouette inverse, à savoir envoyer les données de la base iPhone vers le serveur. A première vue, le mieux est aussi de générer un fichier XML en local et de l'envoyer en requête HTTP POST ? A l'arrivée, je peux faire un parser en Ruby qui fait le boulot inverse ? Quelle est la meilleure méthode pour expédier cet XML sur le serveur ? Quelles sont les règles de base pour ne pas envoyer toute la base du client à chaque fois ? Faut-il également enregistrer toutes ses actions ou se servir de champs created_at, updated_at dans chaque table par exemple ? Je suppose qu'il y a des choses à faire et ne pas faire en synchronisation.
Pour l'instant, le client ne peut modifier ses données que sur l'iPhone mais pourquoi pas un jour sur le serveur... En attendant, ça lui servira de backup s'il a un soucis avec son app client.
Bref, si vous avez des pistes, ou bien un bon lien (que j'ai du mal à trouver) pour faire de la synchronisation avec Cocoa, n'hésitez pas.
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Tu peux regarder du coté de JSON aussi. Ca te permet de dialoguer avec ton serveur et l'avantage de JSON c'est que tu peut lui envoyer des objets genre Array. Ca sera peut-être moins lourd à gérer que du XML à voir.
Tu peux jeter un oeil par là :
http://www.mobileorchard.com/tutorial-json-over-http-on-the-iphone/
ou là
http://iphonedevelopertips.com/networking/iphone-json-flickr-tutorial-part-1.html
Tu trouveras pas mal d'infos sur le web.
Moi j'utilise JSON pour discuter entre mon javascript et mon PHP et c'est top.
++
D'autres avis sur la syncho sont les bienvenus.
En gros le JSON permet d'échanger des données simples (tableaux, dictionnaires, ou valeurs comme des chaines, entiers, flottants, booléens). Voir le site json.org qui est super simpliste... mais largement suffisant, puisqu'il explique uniquement avec 3-4 images toutes les possibilités de JSON. Simple mais efficace.
Il existe une "lib" pas mal répandue en plus pour l'iPhone : SBJSON.
Elle consiste juste en quelques fichiers et son utilisation est super simple grace au fait que l'auteur a fait des catégories sur NSString, NSDictionary, et NSArray.
Du coup ça te permet, quand tu veux envoyer un NSArray à ton serveur, de faire [tt][monArray JSONString][/tt] pour récupérer la NSString JSON à envoyer au serveur. Pareil avec NSDictionary, NSArray, etc.
De même dans l'autre sens quand tu reçois une réponse JSON de la part de ton serveur, tu fais juste [tt][chaineRetourneeParServeur JSONValue][/tt] et ça te retourne directement le NSArray ou NSDictionary correspondant à la représentation JSON récupérée.
Côté serveur, il y a plein de librairies PHP pour générer du JSON à la volée. Côté Javascript (par exemple requêtes AJAX), évidemment c'est natif, puisque c'est de là que vient le JSON (JavaScript Object Notation). Côté Ruby, je sais pas trop mais à mon avis soit c'est intégré soit y'a plein de libs aussi.
Tu as deux fonctions très utiles : json_encode et json_decode
C'est très simple à utiliser.
En revanche, comme Ali, pour Ruby je ne sais pas mais je crois avec déjà vu que ct supporté.
May the force be with you
J'ai trouvé des librairies Ruby JSON. Je vais étudier tout cela et surtout bien rélféchir à la logique de la synchronisation...
Pour info, j'ai 40 tables dans l'application.
- 6 à synchroniser serveur => client
- 9 à synchroniser serveur <=> client
Je sens que je vais m'y mettre pour l'instant j'ai toujours fonctionné en XML.
J'ai téléchargé et installé JSON dans mon app.
Mais je suis sur 2 tutorials qui ne procèdent pas de la même manière.
Ici : http://iphonedevelopertips.com/networking/iphone-json-flickr-tutorial-part-1.html
Point de SBJSON object, on utilise JSONValue sur la string récupérée.
Ici : http://blog.zachwaugh.com/post/309924609/how-to-use-json-in-cocoaobjective-c
Un objet SBJSON qui sert pour le parsing.
Bref, y-t-il 2 façons de faire pour le décodage d'une string JSON ?
1) Méthode 1, utiliser les objets de parsing et génération
- Tu as une classe SBJsonWriter, qui permet de générer du JSON. Il possède un protocole associé ([tt]@interface SBJsonWriter : SBJsonBase <SBJsonWriter> ... @end[/tt]) qui déclare ses méthodes. Ainsi tu as d'un côté un protocole décrivant les méthodes qu'un objet implémente quand il se conforme à SBJsonWriter, et un objet qui implémente effectivement ces méthodes.
- Sur le même principe, il existe une classe SBJsonParser, qui permet de parser une chaà®ne JSON pour recréer un objet, et utilise le même principe comme tu le vois ([tt]@interface SBJsonParser : SBJsonBase <SBJsonParser> ... @end[/tt])
- Et tu as une classe SBJSON (celle utilisé dans le cas du lien 2 que tu as mis) qui est une sorte de wrapper, qui implémente les deux protocoles SBJsonParser et SBJsonWriter. C'est juste pour une facilité si tu veux : si tu regardes le .h et .m tu verras en effet que cette classe contient une variable d'instance SBJsonWriter et une v.i. SBJsonParser, et que les méthodes des protocoles éponymes se contentent de forwarder les appels à ces objets internes. Ca te permet d'avoir dans ton code de ton côté qu'un seul objet pour faire les deux, parsing et génération.
Donc déjà là tu as le choix, entre utiliser les objets de premier niveau SBJsonWriter/SBJsonParser quand tu veux générer ou parser du JSON, ou utiliser un objet unique SBJSON qui encapsule les deux possibilités à la fois.
2) Méthode 2 : utiliser les catégories
Mais là où l'auteur de la lib a eu une bonne idée, c'est qu'en plus de ces classes permettant de parser une chaà®ne JSON que tu lui passes, ou générer du JSON d'après un objet, il a rajouté une catégorie sur NSString (ajout d'une méthode [tt]-(id)JSONValue[/tt]) ainsi qu'une catégorie sur NSObject (ajout d'une méthode [tt]-(NSString*)JSONRepresentation[/tt]).
Et que font ces catégories ? Simple !
- La méthode JSONValue de NSString, en interne elle crée un SBJsonParser, lui passe la NSString (self) en paramètre, et retourne l'objet en résultat.
- La méthode JSONRepresentation de NSObject, en interne elle crée un SBJsonWriter, lui passe l'objet (self) en paramètre, et retourne la chaà®ne générée.
Du coup grâce à ces catégories, ça fait beaucoup plus "objet" et beaucoup plus propre comme code, et ça t'évite de créer ton SBJsonParser et SBJsonWriter ou ton objet SBJSON.
Après, tu as tous les choix, utiliser la classe/wrapper SBJSON, utiliser plus directement un SBJsonWriter et/ou un SBJsonParser, ne pas t'embêter et passer directement par les catégories... moi perso je m'embête pas, les deux méthodes des catégories font tout ce qu'il faut pour moi (et donnent l'impression que JSON est intégré à Cocoa et aux classes existantes) donc pourquoi s'en priver ?
(de plus JSON commence à être très bien vu )
En fait, j'étais sur la doc Ruby-Json pendant ce temps.
Je regarde tout ça demain et vous tiens au courant.
Oui, tu as raison. Si on a le gâteau et la cerise, autant commencer à manger la cerise !
Elle est dessus en plus.
Petit test :
nous donne :
Pour info, @countries est un array de hashes obtenu par requête MySQL.
Est-ce que isync ou encore ce framework (http://www.zarrastudios.com/ZSync/ZSync.html) ne peuvent pas vous aider pour synchro?
A voir ce que donne ce Framework, mais les syncservices pour ma part j'ai toujours abandonné au bout de 2 pages de doc et 3 aspirines :-p
D'autan qu'ici la synchro est entre un serveur sur le net et les iPhone, les syncservices permette de synchro avec un BDD particulière qui est au sein de Mac. C'est pas adapté à la situation.
Intéressant... Mais comme l'iPhone ne supporte pas nativement les Sync Services, la solution proposée par ce framework implique l'installation d'un daemon sur le Mac, qui fera l'interface avec iSync. L'autre problème avec les Sync Services, c'est qu'ils n'existent pas sur PC, et donc les utiliser risque de mécontenter les utilisateurs d'iPhone et de PC qui ne pourront pas synchroniser leurs données...
Comment feriez-vous pour associer des images aux chaà®nes JSON ?
Pour envoyer l'image, je suppose qu'il est obligatoire de passer par mutableRequest et un postData en plus de la chaà®ne JSON qui ne contient que les données texte ?
L'idéal aurait peut-être été d'encoder l'image en NSData et pouvoir la passer dans la chaà®ne JSON directement mais je n'aurais pas moyen de la décoder côté serveur.
Après, c'est tout à fait possible de les envoyer en NSData (ou plutôt tu envoies la représentation base64 du NSData retourné par UIImagePNGRepresentation) et de les récupérer côté serveur (décoder le base64 puis enregistrer sur disque directement les octets résultants, dans un fichier avec la bonne extension, et tu as ton image) mais bon.
Personellement :
1) Quand mon serveur doit retourner une image à mon client :
- soit le client n'attend qu'une image et rien d'autre donc je lui envoie directement (avec le Content-Type qui va bien, genre "image/png") bref comme s'il demandait le fichier image.png au serveur,
- soit il attend cette image dans la réponse en plus d'autres choses, et dans ce cas je ne lui envoie pas l'image dans la réponse JSON, mais juste l'URL (potentiellement temporaire, plus généralement autogénérée genre "image.php?id=...&w=...&h=..." par ex) à laquelle il pourra aller récupérer cette image, et je récupère cette image dans un 2e temps via une 2e requête
2) Quand j'ai besoin que mon client envoie une (ou plusieurs) image au serveur, j'envoie l'image en question en "multipart/form-data", avec une "part" par fichier (bien souvent qu'une seule, mais rien n'empêche de faire transiter plusieurs fichiers d'un coup si tu as besoin).
Si tu ne connais pas, ce principe de requêtes POST "multipart/form-data" n'est pas bien compliqué. Il suffit d'envoyer une requête POST (comme tu ferais n'importe quelle requête NSURLHTTPRequest) avec un body correctement formatté, ça suit une norme mais qui n'a rien de compliqué et tient en qques lignes.
C'est d'ailleurs le type de requête et de transfert de contenu typiquement utilisé à la fois en HTML (quand tu as des formulaires avec un <input type="file" ...>) et pour les mails avec pièce jointe.
ça confirme ce que je pensais.
Dans un premier temps, ça sera client => serveur.
J'ai trouvé cette page pleine d'exemples de formatages de POST data:
http://www.cocoadev.com/index.pl?HTTPFileUpload
- Comme tu le vois, quand tu composes ta NSURLRequest tu as juste à spécifier "multipart/form-data; boundary=MACHIN" (tu mets ce que tu veux comme boundary, MACHIN ou autre chose) comme Content-Type de ta requête, et dans le corps de la requête, tu mets chacune de tes "parts" séparés par "--MACHIN\r\n" (sauf pour après la dernière part où tu mets "--MACHIN--\r\n").
- Ensuite, chaque "part" consiste ensuite en une ou plusieurs lignes qui correspondent aux headers de cette "part" (au moins le header Content-Disposition, et pour des données qui ne sont pas juste du texte mais par exemple une image ou des données binaires, rajouter le header "Content-Type"), suivi d'une ligne vierge (d'où le "\r\n\r\n" après le dernier header de chaque "part", passer à la ligne + rajouter une ligne vide) avant d'y mettre le contenu de ladite "part".
Ce qui donne par exemple un corps de ce genre : Légende :
- boundary qui sépare les différentes "parts"
- headers HTTP de chaque "part"
- contenu de chaque "part"
Si tu envoies une requête avec ce corps, de l'autre côté si par exemple tu as du PHP, tu pourras récupérer tes variables avec [tt]$_POST['realname'][/tt], [tt]$_POST['email'][/tt] et [tt]$_FILES['image'][/tt]
A toi d'écrire le code Objective-C pour générer ce genre de trucs en fonction de ce que tu veux envoyer
PS : Je me demande si le "boundary", même s'il est arbitraire, ne doit pas commencer par un certain nombre de "----", vu que c'est assez courant de voir des boundaries de ce type dans les exemples. Mais je crois que c'est juste une habitude / pratique répandue (d'autant que visuellement si tu affiches le body de la requête que tu envoies tu identifies ainsi facilement les diverses "parts") plus qu'un truc imposé par la norme.
J'attaque cela très prochainement. Je te tiendrais au courant, j'ai un autre dev à finir auparavant.
Je déterre ce sujet car avec le temps, les choses ont avancé mais se sont complexifiées.
J'ai longuement réfléchi à la logique d'une synchronisation client <=> server et je pense pouvoir y arriver. En revanche, là où ça c'est compliqué, c'est que maintenant cette synchronisation doit être multi-clients, et croyez-moi, je m'arrache les cheveux.
Pourquoi ? La raison en est simple.
- J'ai une table SQL sur mon client 1 (iPhone par exemple) avec X vins.
- J'ai une table SQL sur mon client 2 (iPad par exemple) avec Y vins.
Le problème c'est que sur ces 2 bases, des vins différents ont le même ID en clé primaire...
Sur le serveur où je n'ai qu'une base SQL qui doit tout centraliser (dont les les ID sont bien sûr uniques), j'avais réfléchi à une parade :
- je pensais ajouter un champ remote_id dans les tables locales des clients pour avoir l'id du vin en question sur le serveur (ainsi un vin déjà synchronisé aurait un remote_id dans sa table en local). Cette solution pourrait marcher MAIS les 10 autres tables dépendantes de la première auraient leurs clés étrangères perdues puisqu'elles sont en rapport avec l'id local et non l'id remote...
Evidemment l'idéal serait d'avoir un ID local unique pour 1 vin donné mais c'est impossible avec plusieurs clients...
Donc sur ces points, je prends vraiment toute suggestion (en en plus je n'ai pas parlé encore des conflits de version d'un vin...)
Euh... je vois pas en quoi ça résout mon problème. Si tu peux développer...
L'idée semble très bonne. Toutefois je ne vois toujours pas comment éviter les collisions pour les tables secondaires.
Le problème c'est que mes clés étrangères sont actuellement calquées sur les les ID des clés primaires autoincrement des autres tables... Si je dois les calquer sur le nouveau GUID, je dois changer pas mal de code dans mon client, y compris dans mes modèles... ou alors j'ai loupé un truc ?
Je veux dire, à la limite renommer ton champ "id" actuel en "autoincrement_id" et créer un nouveau champ "id" pour y mettre ton GUID à la place.
Comme ça tu changes pas tes requêtes SQL et ton modèle roby, si ?
ça implique de mettre à jour toutes les tables dépendantes de la principale avec le GUID en clé secondaire il me semble ?
En fait, le fond du problème c'est le fait que id soit autoincrément dans 2 bases différentes. C'est clair que le GUID s'il est dans un espace indéfini et unique sur X clients peut le faire. Effectivement, la solution la plus simple est peut-être de tout simplement passer l'id en mode GUID (en virant l'autoincrement) et forcément donc mettre à jour les clés secondaires des autres tables...
ça peut intéresser du monde.
Ce qui donne par exemple : 1011BCA7-7C19-446B-9F42-165103E11AB1
L'idée est d'avoir des catégories qui rajoutent :
- La génération d'un UUID dans NSString (= ton code)
- Le calcul d'un MD5 d'un buffer/NSData (via CommonCrypto/CommonDigest.h et CC_MD5). Et tant qu'à faire faire aussi SHA-1 & co qui sont aussi dispos dans CommonCrypto
- Une méthode de commodité qui fait la mm chose sur une NSString (récupère la représentation data UTF8 de la string, demande le MD5)
- Une méthode de NSData qui affiche la représentation hexa d'un buffer, avec caractère séparateur optionnel tous les N octets