OVH Cloud OVH Cloud

sockets asynchrone, FD_READ et nbre d'octets

27 réponses
Avatar
Nicolas Aunai
bonjour

je me demande s'il existe un nombre d'octets limités sur un socket,
annoncé par un FD_READ, pour allouer ue taille fixe et suffisante a un
buffer de reception... ?

autre petite question sur un point pas clair :
je reçoit un FD_READ, et j'ai disons 3ko sur la socket qui attendent
d'etre reçus. 3ko sur un total de 10ko que mon serveur a envoyé.

je reçoit les données annoncées par le message avec ioctlsocket et
FIONREAD, qui m'indique la quté de donnée pouvant etre lues sur un seul
recv, disons que cette quté est inférieure aux 3ko présents...et
qu'elle est égale a...2ko

aurai-je un autre FD_READ pour le 1ko restant ? ou le prochain FD_READ
se produira-t-il lorsqu'un autre paquet de données arrivera sur la
socket ?


--
Nico,
http://astrosurf.com/nicoastro

10 réponses

1 2 3
Avatar
Cyrille \cns\ Szymanski
Voici ce que tu dois faire en cas de FD_READ :

"case FD_READ:"
1_ Remplir recvbuf avec les données qui viennent d'arriver.
C'est là que le buffer est rechargé, quand il y a des
données à recevoir. Typiquement tu fais des recv() tant
que WSAGetLastError()!=WSAE_WOULDBLOCK.
2_ Ensuite appeler traiter() pour queles données qui viennent
d'arriver soient traitées.


bon voila des petites modif :



J'ai reformaté ton code pour plus de lisibilité. Voici les états que tu
as retenu. J'ai enlevé l'état 4 qui ne sert à rien.

0 : pas de client : suivant=1
1 : prêt à traiter une requête (etat achevé) : suivant=2
2 : entête incomplète (etat inachevé) : suivant=3
3 : message incomplet (etat inachevé) : suivant=1 ou 0

Les lignes que j'ai modifiées ne commencent pas par ">".

traiter( ... )
{
do
{
if( contexte.etat == 1 )
{


// prêt à traiter une requête
contexte.etat = 2;
}
if( contexte.etat == 2 )
{


// recevoir (et traiter) l'entête

// recv_entete place l'entête dans userbuf
// ou mieux dans context.header_buf
ret = recv_entete();

if( ret != 0 )
{


// la réception est complète

// lit l'entête context.header_buf, par exemple
// vérifie si elle est valide, découpe la chaine
// et stocke dans la structure HEADER :
// - BodyType
// - BodySize (pour l'allocation du body)
traiter_entete();



/*on peut libérer la mémoire de userbuf*/
free(userbuf)
contexte.etat = 3;
}
else
{


// l'entête n'est pas reçue en entier

// sortie de boucle a contexte.etat==2
// pour remplir le recvbuf
}
}
if( contexte.etat == 3 )
{


// recevoir (et traiter) le message

// on a BodySize normalement, on a qu'à allouer


// BodySize octets pour userbuf
context.message = malloc( contect.BodySize );

// on reçoit tant qu'on a pas copié BodySize
// octets du recvbuf vers userbuf.
// si nbr_octet_copies==BodySize, retour !=0
// on a tout reçu
// si <BodySize ça veut dire que le recvbuf
// contient pas tout le body, on retourne 0
ret = recv_message();

if( ret != 0 )
{


// la réception est complète, on peut traiter le
// message selon le type de BodyType, le message
// est dans recvbuf (ou mieux dans context.message)
traiter_message();

// on en a fini avec le message on peut


// libérer la mémoire.
free(userbuf);
contexte.etat = 1; // ou 0 pour quitter
}


else
{


// body pas entier dans le recvbuf
// sortie à contexte.etat==3
}
}


}
while ( contexte.etat == 1 );
}



Tu vois, si on termine sur un état incomplet (0, 2 ou 3) c'est qu'il n'y
a pas assez de données dans le recvbuf pour faire quelquechose d'utile.
Alors on sort de la fonction de traitement. Celle-ci ne sera appelée
qu'au prochain FD_READ (quand des données auront été reçues).

Le mieux pour la structure context est de créer un membre par élément que
tu veux recevoir (c'est plus clair et facile à déboguer) :

struct context
{
int etat;

// header
// données brutes : (le recvbuf dans le cas réception de header)
char *header_buf;
// données analysées
int BodyType;
int BodyLen;

// body
// données brutes : (le recvbuf dans le cas réception de body)
char *body;
// données analysées
char *param1;
int param2;
};

a+
--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/
Avatar
Nicolas Aunai
"Cyrille "cns" Szymanski" vient de nous annoncer :
Voici ce que tu dois faire en cas de FD_READ :

"case FD_READ:"
1_ Remplir recvbuf avec les données qui viennent d'arriver.
C'est là que le buffer est rechargé, quand il y a des
données à recevoir. Typiquement tu fais des recv() tant
que WSAGetLastError()!=WSAE_WOULDBLOCK.
2_ Ensuite appeler traiter() pour queles données qui viennent
d'arriver soient traitées.




tout a fait.



J'ai reformaté ton code pour plus de lisibilité. Voici les états que tu
as retenu. J'ai enlevé l'état 4 qui ne sert à rien.



ok.

0 : pas de client : suivant=1
1 : prêt à traiter une requête (etat achevé) : suivant=2
2 : entête incomplète (etat inachevé) : suivant=3
3 : message incomplet (etat inachevé) : suivant=1 ou 0




traiter( ... )
{
do
{
if( contexte.etat == 1 )
{ // prêt à traiter une requête
contexte.etat = 2;
}
if( contexte.etat == 2 )
{


// recevoir (et traiter) l'entête

// recv_entete place l'entête dans userbuf
// ou mieux dans context.header_buf
ret = recv_entete();

if( ret != 0 )
{


// la réception est complète

// lit l'entête context.header_buf, par exemple
// vérifie si elle est valide, découpe la chaine
// et stocke dans la structure HEADER :
// - BodyType
// - BodySize (pour l'allocation du body)
traiter_entete();



/*on peut libérer la mémoire de userbuf*/
free(userbuf)
contexte.etat = 3;
}






là il faut ajouter "noter la position dans le recvbuf", en effet, si la
reception de l'entete est complète, ça signifie :

-soit on a "vidé" le recvbuf auquel cas dans le contexte.etat==3 on
aura un return 0 et une sortie de boucle et un rechargement du recvbuf.

-soit il reste des octets dans le recvbuf, qui correspondent forcément
au body, ce qui signifie que recv_message() devra lire dans le recvbuf
a partir de là ou recv_entete() ct arrété... c logique !


else
{


// l'entête n'est pas reçue en entier

// sortie de boucle a contexte.etat==2
// pour remplir le recvbuf
}






là, c'est là position dans le header_buf qu'il faut noter, car on sort
de la boucle, un nouveau FD_READ va remplir le recvbuf, et dans mon
prochain passage dans traiter(), contexte.etat sera égal a 2, et
recv_entete() ne devra pas "écraser" l'entete inachevée, mais reprendre
là ou elle c'était arrêtée. logique également.


}
if( contexte.etat == 3 )
{ // recevoir (et traiter) le message

// on a BodySize normalement, on a qu'à allouer


// BodySize octets pour userbuf
context.message = malloc( contect.BodySize );

// on reçoit tant qu'on a pas copié BodySize
// octets du recvbuf vers userbuf.
// si nbr_octet_copies==BodySize, retour !=0
// on a tout reçu
// si <BodySize ça veut dire que le recvbuf
// contient pas tout le body, on retourne 0
ret = recv_message();

if( ret != 0 )
{


// la réception est complète, on peut traiter le
// message selon le type de BodyType, le message
// est dans recvbuf (ou mieux dans context.message)
traiter_message();

// on en a fini avec le message on peut // libérer la
mémoire. free(userbuf);
contexte.etat = 1; // ou 0 pour quitter
}






idem que pour l'entete, il se peut que le recvbuf contienne un morceau
d'une prochaine entete, il faut donc noter la position courante de
lecture du recvbuf, pour que le prochain appel a recv_entete reprenne
là où on en était.

else
{ // body pas entier dans le recvbuf
// sortie à contexte.etat==3
}





et là encore pareil, noter la position courante d'écriture dans
body_buf, qui permettra lors du prochain passage par recv_body, de
reprendre là où l'on c'était arrêté.

}


}
while ( contexte.etat == 1 );
}



Tu vois, si on termine sur un état incomplet (0, 2 ou 3) c'est qu'il n'y
a pas assez de données dans le recvbuf pour faire quelquechose d'utile.



oui, et penser aux positions dans les buffers appropriés.

Alors on sort de la fonction de traitement. Celle-ci ne sera appelée
qu'au prochain FD_READ (quand des données auront été reçues).



tout a fait, pour reprendres là ou on était, ça peut aussi etre 0.

Le mieux pour la structure context est de créer un membre par élément que
tu veux recevoir (c'est plus clair et facile à déboguer) :



ok.

struct context
{
int etat;

// header
// données brutes : (le recvbuf dans le cas réception de header)



le recvbuf ?? pk tu dis ça ?

char *header_buf;
// données analysées
int BodyType;
int BodyLen;

// body
// données brutes : (le recvbuf dans le cas réception de body)
char *body;
// données analysées
char *param1;
int param2;



il faut rajouter :

int recvpos; //position dans le recvbuf

et dans chaque partie :

int headerbufpos;//position d'écriture dans le header_buf
int bodybufpos;//position d'écriture dans le body_buf.

};

a+




--
Nico,
http://astrosurf.com/nicoastro
Avatar
Nicolas Aunai
"Cyrille "cns" Szymanski" avait soumis l'idée :

Ce que tu peux faire c'est qu'une fois lues, les données sont enlevées du
buffer. Donc le premier octet est toujours le premier non lu.

recvbuf : xxxxxxxxxxxxxxxoooooooooo
userbuf : xxxxxxxxooooooooooo
--> tu lis des octets
recvbuf : lllllxxxxxxxxxxoooooooooo
userbuf : xxxxxxxxllllloooooo
--> puis
recvbuf : xxxxxxxxxxooooooooooooooo
userbuf : xxxxxxxxxxxxxoooooo



c'est bien mignon ça, mais comment tu veux faire pour "enlever" des
données du recvbuf ? :/
en changeant l'adresse du buffer et en la mettant a la position
courante ?

recvbuf=&recvbuf[position_courante];

sinon j'alloue directement la taille reçue dans le recvbuf, elle peut
etre trop grande, trop petite (besoin d'un realloc après) ou pile
poil.



C'est aussi une bonne méthode.



j'pense que c la moins casse cou*lles.

Tu vois, toutes ces questions n'on en fait rien à voir avec le protocole.
La question est : comment faire un buffer (de type pile) efficace ? Ce
qui est le sujet d'un tout autre thread a mon avis.





Oui, dans le cas où n-m<BodySize tu restes à 3. Tous les autres if sont à
false, et la condition while( etat==1 ) (voir ton implémentation
légèrement modifiée) fait que tu sors de la fonction traiter. Cette
fonction ne sera appelée qu'au prochain FD_READ.




En restant à 3 tu sors de la boucle de lecture.



oui oui.. bien sûr je n'avais pas réfléchi.


si je comprends, on reviendra ensuite dans la boucle avec un état 3,
donc on reprendra là où on en était. il faut donc conserver également
la position courante d'écriture dans le userbuf.



Oui !!!



ok.


Franchement ça vaut vraiment le coup d'y passer un peu de temps. Même si
dans le cas présent on pourrait torcher ton programme en quelques lignes,
en faisant comme ça tu ne seras pas capable d'appréhender un projet plus
important.



oui possible, quoi que je doute que je l'aurai torché en qql lignes...



--
Nico,
http://astrosurf.com/nicoastro
Avatar
Cyrille \cns\ Szymanski
>> Ce que tu peux faire c'est qu'une fois lues, les données sont
enlevées du buffer. Donc le premier octet est toujours le premier non
lu.

recvbuf : xxxxxxxxxxxxxxxoooooooooo
userbuf : xxxxxxxxooooooooooo
--> tu lis des octets
recvbuf : lllllxxxxxxxxxxoooooooooo
userbuf : xxxxxxxxllllloooooo
--> puis
recvbuf : xxxxxxxxxxooooooooooooooo
userbuf : xxxxxxxxxxxxxoooooo



c'est bien mignon ça, mais comment tu veux faire pour "enlever" des
données du recvbuf ? :/
en changeant l'adresse du buffer et en la mettant a la position
courante ?

recvbuf=&recvbuf[position_courante];



Un memmove() et realloc() feront l'affaire :

struct buffer {
char *ptr;
size_t len;
};

struct buffer buf;

Enlever n octets au buffer buf :
memmove( buf.ptr, buf.ptr+n, n );
buf.len -= n;
buf.ptr = realloc( buf.len );

a+
--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/
Avatar
Nicolas Aunai
je viens de penser, en plus, qu'on ne peut pas allouer (ni réallouer
d'ailleurs) la mémoire comme ça aussi facilement, dans les début de
if(contexte.etat==2) ou ==3, en effet, ça irait si c'était la 1ere fois
qu'on passait ici, mais si on y repasser pour continuer a écrire dans
le buffer approprié (header_buf ou body_buf) après un rechargement de
recvbuf, on va pas faire body_buf=malloc(BodySize) par exemple, car il
aura déjà été alloué au 1er passage...

s'il est nécessaire de faire 10 passages, ça fait 10 malloc, c'est
n'importe quoi :)

de même, on ne peux pas mettre realloc, car si c'est le 1er passage, il
n'y aura pas eu d'allocation...


il semblerai donc qu'il faille encore une variable suplémentaire,
charger d'indiquer le nombre de boucle de reception d'un bloc.

si compteur_body ==1 alors malloc(BodySize), si compteur_body>1 alors
que dale...

idem pour le header,

si je décide d'allouer la taille du recvbuf lors de la 1ere reception,
ok, mais il se peut comme on l'a dit, que cela ne suffise pas, et là on
n'est pas dans le cas du body ou l'ont connait la taille a allouer pile
poil, alors je prospose de faire :

si header_compteur==1 malloc(lenrecvbuf)

si header_compteur>1 realloc(lenrecvbuf*header_compteur)


m'enfin c qu'une idée...


--
Nico,
http://astrosurf.com/nicoastro
Avatar
Cyrille \cns\ Szymanski
>>> if( contexte.etat == 2 )
{


// recevoir (et traiter) l'entête

// recv_entete place l'entête dans userbuf
// ou mieux dans context.header_buf
ret = recv_entete();

if( ret != 0 )
{


// la réception est complète

// lit l'entête context.header_buf, par exemple
// vérifie si elle est valide, découpe la chaine
// et stocke dans la structure HEADER :
// - BodyType
// - BodySize (pour l'allocation du body)
traiter_entete();



/*on peut libérer la mémoire de userbuf*/
free(userbuf)
contexte.etat = 3;
}






là il faut ajouter "noter la position dans le recvbuf", en effet, si
la reception de l'entete est complète, ça signifie :

-soit on a "vidé" le recvbuf auquel cas dans le contexte.etat==3 on
aura un return 0 et une sortie de boucle et un rechargement du
recvbuf.

-soit il reste des octets dans le recvbuf, qui correspondent forcément
au body, ce qui signifie que recv_message() devra lire dans le recvbuf
a partir de là ou recv_entete() ct arrété... c logique !



Si tu copies ce que tu viens de lire dans un autre buffer du même type
que recvbuf (voir l'autre post) disons headerbuf alors il n'y a pas
besoin de faire cela, c'est les fonctions de gestion de buffer qui se
chargent de savoir où on en est et combien d'octets il reste.

De toute façon ce genre de code n'a rien à faire à ce niveau. Il doit se
trouver dans la fonction recv_entete() ou une de ses sous fonctions. Ici
tout ce qui t'intéresse c'est une fonction qui lit tout ce qu'elle peut
de l'entête et qui t'indique si elle a été reçue ou non.


else
{


// l'entête n'est pas reçue en entier

// sortie de boucle a contexte.etat==2
// pour remplir le recvbuf
}






là, c'est là position dans le header_buf qu'il faut noter, car on sort
de la boucle, un nouveau FD_READ va remplir le recvbuf, et dans mon
prochain passage dans traiter(), contexte.etat sera égal a 2, et
recv_entete() ne devra pas "écraser" l'entete inachevée, mais
reprendre là ou elle c'était arrêtée. logique également.



Mêmes remarques. Typiquement tu utilises une fonction du genre int
buffer_add_data( struct buffer buf, char *data, size_t len ) à laquelle
tu passes les données que tu as lues depuis le recvbuffer.


oui, et penser aux positions dans les buffers appropriés.



Ce qui est géré par le sous-système buffer.



--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/
Avatar
Cyrille \cns\ Szymanski
> Enlever n octets au buffer buf :
memmove( buf.ptr, buf.ptr+n, n );



C'est plutôt :
memmove( buf.ptr, buf.ptr+n, buf.len-n );

buf.len -= n;
buf.ptr = realloc( buf.len );




--
_|_|_| CnS
_|_| for(n=0;b;n++)
_| b&=b-1; /*pp.47 K&R*/
Avatar
Nicolas Aunai
"Cyrille "cns" Szymanski" a exprimé avec précision :
Je crois que le mieux est que je te donne une implémentation complète pour
que tu voies comment je fais ça. Encore une fois on peut faire autrement,
ce n'est pas la seule méthode. Mais crois moi, j'ai passé du temps à
planifier tout ça pour que ce soit ple plus propre, flexible et logique
possible.



soit.


Aha !

Mais tout ça se passe au niveau de traitement du buffer (c'est à dire dans
un appel à une fonction buffer_* qui a lieu dans une des fonctions
recv_entete().

En gros tu as deux solutions qui se valent :

****************
* Solution 1 : * Les données restent dans le recvbuf tant que l'objet
**************** entier n'est pas reçu. Dans ce cas c'est simple, tu
reprends le code de traiter précédent (sans en changer une ligne ou
presque) et la fonction recv_bidule ressemble à :

Tes buffers sont des objets de type buffer avec l'interface que j'ai donnée
avant :
buffer_read : lit et supprime des octets du buffer
buffer_len : renvoie la taille du buffer
buffer_peek : lit mais ne supprime pas
buffer_get : renvoie un pointeur vers le premier octet




struct buffer recvbuffer; // ce n'est pas un simple char*, il contient
// d'autres infos telles que sa taille

int recv_bidule( ... )
{
// la condition qui va bien pour déterminer que l'entête est complète
char *pos;
pos = strnstr( buffer_get(recvbuffer), ".END", buffer_len(recvbuffer) );
if( pos!=0 ) {
// l'entête est reçue en entier
int size = pos-buffer_get(recvbuffer);
context.header = malloc( size );
buffer_read( recvbuffer, context.header, size );
return 1;
}
else
{
// non, continuer à chercher
return 0;
}
}



ok pour cette solution, juste deux points flous :

buffer_get, retourne le 1er octet, je suppose que c de l'octet qui nous
intéresse dont tu aprles, c'est à dire le 1er octet non encore lu. si
oui, comment elle le trouve cet octet ?

ensuite, tu retourne 0 comme prévu en cas d'entete incomplete, ok, il
va cependant réallouer le recvbuf ensuite... de sa taille actuelle+la
taille reçue de la socket.

j'aime pas trop ça.


****************
* Solution 2 : * Les données lues sont copiées octet par octet dans un
**************** buffer de travail, c'est à dire directement en place.
C'est à peine plus long mais ça a l'avantage de vider le recvbuf au
maximum. On sait exactement ce qui a été lu en regardant recvbuf et cela
facilite le déboguage. Dans le cas précédent il faut deviner.

Là je rajoute deux fonctions à l'interface buffer :
buffer_get_char : qui renvoie le caractère suivant et l'enlève du buffer



comprend tjrs pas comment tu fais pour "enlever" qqch.. ton truc de
memmove+realloc là... moué moué...

buffer_append_char : qui ajoute un caractère à la fin d'un buffer

int recv_bidule( ... )
{
do
{
int err;
char c;

// lire un caractère
err = buffer_get_char( recvbuf, &c );
if( err!=BUFFER_EOF )
{
// ajouter le caractère
buffer_append_char( context.bidule, c );
}
else
{
// recvbuf est vide -> incomplet
return 0;
}
}
// les quatre derniers caractères du buffer sonr ".END"
while( strncmp(
buffer_get(recvbuffer)+buffer_len(recvbuffer)-4,
".END",
4)
);

return 1;
}

Dans le dernier cas il faut évidemment initialiser le buffer (ce que tu
appelles le malloc initial). Tu seras d'accord avec moi pour dire que ça a
a voir avec la réception de données. Donc c'est dans la fonction
recv_bidule que ça se place. Comment le mettre exactement dépend de
l'implémentation de buffer.



ça je suis bien d'accord !
quoi que je trouve que tes fonctions compliquent un brin... mais bon.
c juste le coup d'enlever les données du recvbuf que je pige pas.


Un realloc de 0 normalement fait un malloc donc une solution (assez moche
je l'admets) est d'utiliser realloc tout le temps.



hum non, le malloc c'est ptr=realloc(NULL,taille); le realloc normal c
ptr=realloc(ptr,taille);

il semblerai donc qu'il faille encore une variable suplémentaire,
charger d'indiquer le nombre de boucle de reception d'un bloc.



Tu peux faire comme-ça mais ce n'est pas obligatoire et ça complique
(inutilement si tu veux mon avis) le code.



bah j'vois pas autrement. bon bhe j'ai encore du boulot niveau
comprehension


--
Nico,
http://astrosurf.com/nicoastro
Avatar
Nicolas Aunai
Il se trouve que "Cyrille "cns" Szymanski" a formulé :


int recv_bidule( ... )
{
do
{
int err;
char c;

// lire un caractère
err = buffer_get_char( recvbuf, &c );
if( err!=BUFFER_EOF )
{
// ajouter le caractère
buffer_append_char( context.bidule, c );
}
else
{
// recvbuf est vide -> incomplet
return 0;
}
}
// les quatre derniers caractères du buffer sonr ".END"
while( strncmp(
buffer_get(recvbuffer)+buffer_len(recvbuffer)-4,
".END",
4)
);




j'ai l'impresion que ta condition de boucle est fausse, en effet, pk
regardes-tu les 4 derniers octets de RECVBUF ? 8-o ce sont les 4
derniers octets du buffer bidule dans lequel tu déplace les caract.
qui'lf aut vérifier.




--
Nico,
http://astrosurf.com/nicoastro
Avatar
Nicolas Aunai
bon, suivant tes conseils j'ai mis en place la base des communications
réseaux de mon programme.

normalement l'organisation est a peu près logique, enfin j'espère, j'ai
fait un chti organigramme de l'intéraction des fonctions entre elles,
avec une couleur par module.

http://nicolas.aunai.free.fr/async/orgaasync.png

le code est a : http://nicolas.aunai.free.fr/async/


--
Nico,
http://astrosurf.com/nicoastro
messenger :
1 2 3