as-tu déjà vu un '\n' ou un traitement ne tenant pas compte de realloc dans le code de HTTP ????
ou pensant que tout est arrrivé sur un seul recv ??
as-tu déjà vu un '\n' ou un traitement ne tenant pas compte de realloc dans le code de HTTP ????
ou pensant que tout est arrrivé sur un seul recv ??
sans doute, mais si c'est un champ au milieu de la structure qui est modifié, en binaire ça crashe.. quelle que soit la "version" que tu aies pu mettre..
Maintenant, il est certain que si le programmeur n'avait pas prévu le "unrecognized keyword" ou au contraire la transformation avec le bon keyword en passant d'une version supérieure à une version inférieure, on n'y peut rien..
Mais une bonne conception ASCII sera plus souple qu'une bonne conception binaire par rapport à l'évolution / maintenance..
Je n'ai regardé le code d'aucune implémentation de HTTP. Dans tous les cas, je n'ai pas dit qu'il n'y en avait pas, seulement qu'il était ch***t d'avoir à en mettre.
Et encore, je pense personnellement que pas mal de vieilles implémentations doivent avoir un buffer de taille fixe pour les lignes d'en-tête: L'erreur 414 existe pour une raison, après tout.
C'est justement là le problème : avoir un realloc... Et pour HTTP, effectivement, ça ne sera pas "\n" qui sera vérifié, mais "\r\n".
Et pour avoir écrit de mes p'tites mains musclées un serveur HTTP, un serveur FTP, un client SMTP et moult clients/serveurs Telnet, je te garantis que tu as "un peu" intérêt à tenir compte du facteur "taille maximale des lignes", ou à prier Sainte-Fragmentation de foutre la paix à ton heap...
C'est pour ça que j'ai mis "version" ET longueur... Pas de crash dans un tel cas, pour plusieurs raisons :
- La taille empêche d'émettre / recevoir trop de données et de provoquer donc un buffer overrun.
- La version interdit de décoder une trame que l'on ne sait PAS décoder (version plus récente que celle du code).
Plus souple, c'est pas gagné non plus. Plus lisible / parlante, éventuellement. Plus facile à examiner par un humain "on-wire", c'est une évidence.
Mais la version ASCII peut être plus difficile à implémenter que la version binaire, et poser bien plus de soucis... Entre d'une part le "réglage" d'une déclaration de structure (alignement + endianness) et ensuite un code qui tape bourrinement dedans, et d'autre part un décodage syntaxique ET sémantique d'un flux texte, il y a plus qu'une nuance, et côté temps de développement, y'a pas photo non plus... Pour l'ASCII, par exemple, si tu ne parles pas à une machine ayant la même taille de mot-machine, tu commences déjà assez mal pour le décodage des entiers / flottants, sans même aborder la problématique de l'encodage des caractères eux-même...
je ne me prendrais pas la tête avec toi là dessus..
Nous avons des expériences différentes, et ma conclusion diffère..
Néanmoins juste un problème par rapport au binaire :
inclure la taille ne change rien à l'affaire..
Si la structure comprend : entier + char + char*4 + double + entier
Et que tu la transfomes en : entier + char + double + entier + char*4
la taille restera identique mais la lecture binaire crashera...
C'est juste ce que je voulais dire.. : il te faut OBLIGATOIREMENT recompilation de l'ensemble des exécutables concernés..
Et comme, dans ce cas, il y aura toujours, dans chaque exécutable, une routine ReadStructure et un autre WriteStructure, que tu le lises en ASCII ou en binaire sera (si c'est fait proprement, avec des htonl etc) de toutes façons non immédiat.. ce qui faciliterait grandement la "compatibilité" si c'était transmis en ASCII par mot-clé, puisque le "découpage" élément par élément de la structure est déjà fait...
M'enfin, tout ça était pour dire pr rapport à la remarque initiale de Médinoc..
NON la taille n'est pas un problème...
Oui, il faut les deux, version ET taille :Bref, pas de problème...
- Premier cas : version(1) + taille(22) + entier + char + char*4 + double + entier
- Deuxième cas : version(2) + taille(22) + entier + char + double + entier + char*4
- La taille protège l'overrun => pas de crash à ce niveau.
- La version interdit d'exploiter le contenu de la structure (qui sera bien entendu totalement incohérent sans l'union adéquate permettant de décoder la 2ème variante).
Certes. Mais entre quelques correction d'endianness et la sérialisation sous forme de texte de N valeurs flottantes (par exemple), il y a une grosse différence dans le temps d'exécution ET dans le temps de réalisation du code, de façon à gérer tous les cas possibles... Et si ton endianness host est identique à celle attendue par le protocole, ça se finit avec un send bovin.
Ben si, pourtant... Soit par épuisement de ta RAM à coup de realloc (ce qui correspond à une attaque du serveur / client en règle, d'ailleurs), soit via un refus type 414, soit encore par une usine à gaz gérant le flux en tant que tel (et non plus ligne à ligne comme le protocole l'impose dans le cas des précités).
C'est très exactement la même problématique que la lecture d'un fichier texte ligne à ligne, d'ailleurs... Sujet hautement récurrent sur le forum, comme tu le sais très bien.
Simplement parce que tu supposes que dans le code il est prévu "si VERSION > MaVERSION" alors ne fait rien...
En général les vérifications se passent par négociations vers la version la plus basse, si un système de version a été prévu.
Mais de toutes façons, cela suppose la gestion de version, ce qui n'était pas le problème exposé ci-dessus..
Je continue à affirmer : dans un code "normal", qui (surtout pour des passages en binaire de structure) ne prévoit pas de versions, se passer des structures en binaires est très largement plus pénalisant en souplesse et maintenance que se les passer en ASCII...
Je ne comprend pas ce point...
Les routines de lecture et d'écriture doivent passer à travers tous les champs de la structure un par un pour faire les appels aux hton ou ntoh..
Quel est le poids différent de l'écrire dans un buffer ??
Encore une fois, je ne vois pas le point..
On ne parle pas ici d'envoyer un fichier ligne à ligne, on parle de transmettre une structure....
Et encore une fois, un protocole établi empêche tout simplement ton "attaque"... Mais de toutes façons le problème n'est pas dans une attaque.. Le sujet ici n'est pas le piratage, mais comment passer des données d'un programme à un autre via socket...
Et je continue à penser que la réponse donnée est hors de propos par rapport à la question posée...
Évidemment, bien sûr que passer des structures non-sérialisées est dangereux. On n'est pas idiots quand même.
Mais je préfère toujours sérialiser en binaire plutôt qu'en texte. Même en supposant un système de valeurs nommées, j'utiliserais un format binaire, du genre:
taille totale(u32, valeur 0xFFFFFFFF réservée) + nombre de variables(u32) + ( taille nom(u16) + nom ( + autres infos) taille données(u32) + données ) * (nombre de variables)
eh bien moi non..
J'utiliserais un protocole ASCII de token...
Comme ça, les structures et les binaires sont indépendants..
Code : Sélectionner tout - Visualiser dans une fenêtre à part Nom_VAR1 val1 Nom_VAR2 Val2 ...
Un peut stocker Var1 en réel l'autre en entier si il en a plus l'utilité..
Mais aussi on peut par exemple supprimer l'envoi d'un token sans rien changer aux autres binaires..
Ou en rajouter sans pour autant modifier TOUS les programmes, mais juste ceux qui doivent l'utiliser (dans un premier temps évidemment, avant de tous les remettre à jour)..
C'est juste une pratique de développement/maintenance souple...
Cela n'empêche pas de tout recompiler quand on souhaite la version défintiive à la même version..
ça implique simplement que l'on peut procéder (tester, debugger, etc) par étapes.. (et en particulier pour les très gros projets où la compilation totale est très longue)...
ça implique aussi que l'on peut se permettre d'être multi-versions, avec plusieurs clients par exemple...
ça m'est arrivé.. Tu fais une amélioration, mais 2 clients sur 3 ne la veulent pas, ou pas maintenant (par exemple ils sont en cours de produire quelque chose, et veulent comparer des choses comparables)..
Tout ce qui se passe, c'est que simplement le "nouveau" token n'est pas reconnu par eux, mais est reconnu par le client qui a bien voulu avoir l'update..
PS: et on peut retomber sur la notion de compatibilité et la discussion à propos de IE6
Le format binaire que j'ai ici esquissé n'empêche en rien l'ajout ou la suppression de valeurs nommées, et je me suis bien gardé de mentionner le format des valeurs elles-mêmes: Ce qui importe pour moi, c'est qu'elles soient précédées de leur taille, et que cette taille ait elle-même une taille (plus ou moins) fixe.
Oh, et avec ça, on n'a pas de problèmes de séparateurs.
Non, mais il est plus long (puisque par exemple tu passes le nombre de variables)
Mais de toutes façons, je ne crois pas que c'est ce qui était entendu par "passage en binaire de structures", ou alors on se comprend très mal....
Ce qui est pratiqué en général dans ce cas, c'est soit le "dump" total en binaire (fwrite), soit le "dump" modulé avec les hton ou ntoh par champ...
Et cette pratique est, je le répète, ce qui s'est passé au début des années 90, mais est très lourde de conséquences pour la maintenance et la souplesse...
Maintenant, si tu fais un protocole en passant un nom de variable en ASCII, que la valeur elle-même tu la passes en binaire ou non c'est identique de mon point de ve, à la conversion près, qui, sur une structure, ne va pas consommer du temps...
Ce dont je parlais est d'une communication de structures SANS protocole ASCII...
Dans ce cas, je suis d'accord avec toi: Le passage de structures non-sérialisées, c'est le mal.
Généralement, j'ai tendance à utiliser une sérialisation "rigide", mais je vais suivre mon propre conseil et utiliser une sérialisation "souple" à partir de maintenant, pour mes prochains protocoles.
Désolé d'interrompre ce débat très intéressant
Je voudrais donc définir une structure et je vais passer par une union mais mon schéma de requête est du type :
plutôt qu'un choix réel entre les différentes données, il s'agit d'un ajout. Avec une union ça me donnerait un truc du style :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 requete 0 : data_a requete 1 : data_a + data_b + data_c requete 2 : data_a + data_b + data_c + data_d + data_e
Je me retrouve donc avec des définitions assez lourdes et dupliquées.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 union donnees { struct donnees_1 { int data_a; }; struct donnees_2 { int data_a; int data_b; int data_c; }; struct donnees_3 { int data_a; int data_b; int data_c; int data_d; int data_e; }; };
Auriez-vous une idée sur la question pour mutualiser tout ça ?
Je pensais à quelque chose comme ça, mais qui ne me semble pas très propre :
Car ensuite, on a une hiérarchie de structures qui complexifie la réception.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 union donnees { typedef struct donnees_1 { int data_a; } d1; typedef struct donnees_2 { donnees_1 d1; int data_b; int data_c; } d2; struct donnees_3 { donnees_2 d2; int data_d; int data_e; }; };
D'autre part, je savais que HTTP, SMTP, etc. fonctionnaient via ASCII mais quelqu'un pourrait-il me dire pourquoi on a alors plus de problème d'alignement lors de l'envoi de données sur différentes machines/OS ?
Merci d'avance.
[edit] Je précise quand même que je n'envoie pas que des int
euh...
Pourquoi pas simplement :
ou toute autre structure équivalente ???
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 struct donnees { int Present[5] ; int data_a; int data_b; int data_c; int data_d; int data_e; }
J'aimerais que tu clarifies ton problème..
Là tu sembles avoir des requêtes différentes, qui chacune utilise des données différentes..
Mais tes données, tu les as de part et d'autre, c'est bien ça ?
Elles ne sont pas imbriquées, mais successives , indépendantes ?
En quoi faire des strutures de données imbriquées résoudra-t-il le rpoblème ?
Si tu peux avoir les 4 ou N) données simultanément, faire une union et un peu absurde..
Soit tu fais une strcuture comme j'ai mentionné plus haut, soit tu peux aussi faire :
et manipuler un tableau de données, par exemple...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 struct donnees { int type ; int data ; }
Mais alors c'est une union de types de valeur, qu'il te faut, pas une union d'ensembles de données..
Avec ou non inclusion du type suivant que tu veux le faire explicitement ou non, et/ou que tu veux le passer via le protocole ou non...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 typedef union pValue ({ int i ; double d ; char c ; char *s ; } Value ;
Tout comme tu supposes que ton client ne va planter s'il ne trouve pas un champ requis, ou qu'au contraire il en trouve un non-prévu... Si tu supposes que c'est implémenté par un goret, quoi que tu conçoives, ça va planter de toutes façons !!
La question étant plutôt "pourquoi ne PAS mettre d'informations de taille / version" de façon générale... Prévoir l'évolutivité n'est pas spécialement une option, à mon sens, que ce soit en binaire ou en ASCII.
Si tu le sérialises en ASCII, il est pourtant clair et net qu'un htonl est nettement plus rapide et moins pénalisant à faire qu'un sprintf, et c'est encore pire lors de la réception : scanf n'est pas un modèle de légèreté à gérer, tu le sais très bien.
Tu peux appeler "ligne" ce que tu veux : une chaîne AZT, une chaîne terminée par CR/LF, ça n'a aucune importance. Dans tous les cas, tu DOIS pouvoir déterminer la fin de ligne (= d'entité) pour savoir quand la réception est terminée.
Certes. Mais pour envoyer une structure, il est quand même bien de savoir quand elle est terminée. En binaire, c'est la taille des données qui te le dit. En ASCII, c'est comment ? D'où la problématique des tailles de buffer en réception que l'on évoquait précédemment.
... Ce qui est extrêmement pénalisant côté bande passante, quand même, il faut en être conscient, et encore plus pénalisant côté (dé)sérialisation si l'on se contente des fonctions C classiques. Même en C++, ce n'est pas une sinécure, il n'y a guère que dans les langages de script que c'est une chose (relativement) simple à traiter.
Comme tu le sais certainement, c'est justement mon cas quotidien. Or, je ne fais au contraire QUE des envois binaires, notamment pour permettre à des éléments plus "faibles" en puissance de calcul de pouvoir malgré tout traiter un volume de données suffisant.
C'est pourtant toujours d'actualité, cf. TCP/IP et Ethernet... Et IPv6 n'est pas mieux à ce sujet.
Tu peux faire un truc dans ce genre, aussi :Toutefois, cela reste une solution d'envoi binaire "brut", bien entendu.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 union donnees { struct donnees_1 { int data_a; }; struct donnees_2 { struct donnees_1 d1; int data_b; int data_c; }; struct donnees_3 { struct donnees_2 d2; int data_d; int data_e; }; };
Parce qu'à part si elle est en Unicode, ce qui n'est pas le cas de ces protocoles, une chaîne de caractères est composée d'éléments d'un seul octet, les caractères, et n'est donc pas sensible à l'endianness, tout simplement.
Ok, merci bien, je vais rester sur définitions dupliquées
Pour ce qui est de l'ASCII, c'est ce que je pensais merci.
@souviron34 : en fait ce que je voulais dire c'est que ma requête de type 1 par exemple utilise les mêmes variables que la requête de type 0 plus d'autres variables. La requête de type 2 les mêmes que la requête 1 plus d'autres, etc.
La duplication, c'est le mal. Soit tu réutilises des structures déjà définies, soit tu utilises une structure "universelle" (définissant TOUT), avec des #define ou des constantes pour définir la taille à envoyer (et donc n'envoyer qu'une partie de la structure).
Mais définir deux fois la même chose à deux endroits différents, c'est s'exposer à un risque non négligeable de régression en cas d'évolution.
Hmm, donc tu me conseillerais de faire comme ça :
plutôt que comme ça ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 union donnees { struct donnees_1 { int data_a; }; struct donnees_2 { struct donnees_1 d1; int data_b; int data_c; }; struct donnees_3 { struct donnees_2 d2; int data_d; int data_e; }; };
quittes à complexifier pas mal le code de réception ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 union donnees { struct donnees_1 { int data_a; }; struct donnees_2 { int data_a; int data_b; int data_c; }; struct donnees_3 { int data_a; int data_b; int data_c; int data_d; int data_e; }; };
Oui, même s'il n'est pas si complexifié que ça : le code de traitement du type 3 de requête reste celui du type 3, non-impacté par le code des autres requêtes. Par rapport à ta méthode, ça ne fait que rajouter un ".d1" ou un ".d2" en plus dans l'identifiant de ton champ, au final, et mettre dans une fonction le codage/décodage des sous-structures si nécessaire de façon à capitaliser le code.
Mais au moins, à condition qu'il soit normal que les premiers champs du 3ème type de requête soit les mêmes que les champs des requêtes "précédentes", la modification des données de la requête 1 modifiera automatiquement les requêtes 2 et 3, alors que dans ta méthode, tu devras le modifier trois fois.
Si par contre ce n'est qu'un heureux hasard, alors ta solution par duplication est préférable, car la requête 1 pourrait évoluer sans impacter la requête 3 par exemple.
Oui effectivement mais là je me retrouve avec :
Ce qui me donne des manipulations de ce style pour assigner des variables :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 struct d3 { struct d1 d_1; // Lecture struct d2 d_2; // Ecriture } d_3;
Et j'aurai le même problème à la réception par le serveur.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 scanf("%s", buffer); strcpy(req.d.d_3.d_1.d_0.data_a, atoi(buffer));
Je garde donc ta solution dans un coin mais sur ce coup-là je vais m'en tenir à la duplication, c'est loin d'être un projet important (merci encore)
Partager