OVH Cloud OVH Cloud

Lecture fragmentee de binaires

29 réponses
Avatar
NaeiKinDus
Bonjour tout le monde !

Je planche actuellement sur un serveur web, et je dois "fragmenter"
l'envoi de mes donnees pour ne pas bloquer X clients lorsqu'un
telecharger un fichier... Disons en blocs de 5Ko. Neanmois, je n'y
arrive absolument pas, ma lecture plante avec un tellg qui renvoit -1
comme taille, et une concat qui foire, donc :(

Code:

ifstream fileStream;
unsigned int size;
char tmp[4096;]

fileStream.open(file.c_str(), ios_base::in | ios_base::binary); //file
est un string recuperee dans un autre endroit du code
cerr << "opened ? " << fileStream.is_open() << endl; // affiche 1
cerr << "good ? " << fileStream.good() << endl; // affiche 1
request.append("\r\nContent-Length: ");
fileStream.seekg(0, ios_base::end);
size = fileStream.tellg();
oss << size;
request.append(oss.str());
request += "\r\n";
request.append("Accept-Ranges: bytes\r\n");
request.append("Connection: keep-alive\r\n\r\n"); // Separation du
header et du body par une paire de "\r\n"
if (*idx > 0){ //*idx est un pointeur sur un int qui permet de
reprendre la lecture la ou on l'avait laissee...
cerr << "clearing request\n";
request.clear(); // si on passe ici, on a pas besoin de header (en
theorie)
fileStream.seekg(*idx);
}
else
fileStream.seekg(0, ios_base::beg);
long pos = (long)fileStream.tellg() - *idx; // Je souhaitais pouvoir
me placer dans mon tmp pour mettre le '\0', mais tellg me renvoit
toujours -1 :(
cerr << "opened ? " << fileStream.is_open() << endl; // renvoi 1
cerr << "good ? " << fileStream.good() << endl; // renvoi 1
fileStream.read(tmp, 4095); // char tmp[4096]... 1 octet pour mettre
le '\0' (habitude unix... bonne ou pas ?)
cerr << "tellg: " << (long)fileStream.tellg() << endl; // renvoi -1
cerr << "idx: " << *idx << endl; // renvoi 0
cerr << "pos: " << pos << endl; // renvoi -1
//tmp[pos] = '\0'; // donc logiquement ca pete ici
*idx = (long)fileStream.tellg(); // je garde en memoire la valeur de
lecture pour reprendre plus tard
if (*idx == size){ // si le fichier est complet, on met idx a -1 pour
que la fonction appellante close la socket.
cerr << "Reseting *idx to -1";
*idx = -1;
request.append(tmp);
request += "\r\n";
}
else
request.append(tmp);



Merci a tous :)

10 réponses

1 2 3
Avatar
James Kanze
On Apr 2, 6:42 pm, "NaeiKinDus" wrote:
Très bien, veuillez m'excuser pour la confusion des genres, mais un
"corresponding to a long" veut dire que type == long... A tord semble
t il !


Je ne sais pas. « Corresponding », en anglais, c'est assez
vague. Ça peut signifie que c'est un typedef, comme ça peut
signifie que c'est un type qui a les même valeurs. Si c'était
réelement un long, je me serais attendu à « is a long »,
carrément.

Mais il faut avouer qu'une documentation qui décrit des choses
d'une façon aussi vague, ce n'est pas ce qui va t'aider beaucoup
à apprendre.

Je m'en vais corriger ça de suite.

Pour le petit programme isolant le soucis, j'en serais bien en peine
de réaliser ça car le soucis peut se trouver à pas mal d'endroits, et
prendree en compte les threads, les sockets, etc... dur.

Idx : long qui contient la position dans la lecture de fichier... A
changer donc par un streampos.


Ou à éliminer carrément. Le flux lui-même garde bien sa postion.
Dans la mesure où tu le lis séquentiellement, il n'y a pas
besoin que tu gères la position. Tu fais un source.read( buf,
4096 ), et le coup suivant, il va lire les 4096 octets suivants.
Et ainsi jusqu'à la fin. Tu n'as même pas besoin de savoir la
taille d'avance. (Sauf que tu dois la mettre dans l'en-tête,
évidemment. Mais si tout le fichier tient en mémoire, je
réserverais simplement la place pour mettre la taille, et je le
mettrait par la suite, une fois la lecture finie.)

Et puisque tu te sers de istream::read, je te previens tout de
suite d'un petit piège. Quand tu fais source.read(), le flux se
met en état d'erreur s'il n'a pas pû lire tous les octets que tu
lui a démandés. Si donc tu lis par bloc de 4096, et le fichier
n'a pas une taille multiple de 4096, tu auras une erreur sur la
dernière lecture, alors qu'il y a bien des octets lus qu'il faut
que tu prennes en compte. Après l'erreur, il faut que tu
appelles aussi source.gcount(), fonction membre de istream, qui
t'indique le nombre d'octets réelement lus dans le dernier read.

Sinon (mais je ne suis pas sûr que tu dois t'en servir dans le
cadre d'un projet d'étude) : moi, je me servirais directement
du filebuf, sans passer par le ifstream. filebuf a une fonction,
sgetn, qui renvoie le nombre d'octets lus -- tu lis jusqu'à ce
qu'elle renvoie 0.

Pourquoi ne pas faire un thread / client ?
1°/ Ce programme est un projet d'études... et cette clause est
précisée dans le sujet !


D'accord. C'est peut-être aussi pourquoi ils insistent sur ce
que tu saucissonnes les lectures, bien que ça ne sert à rien
dans la pratique.

2°/ Il semblerait que cela ne soit pas bon à fort taux de charge, avec
un ralentissement considérable du serveur...


Ça dépend. C'est vrai que créer un thread n'est pas toujours bon
marché. Mais si c'est un problème de performance (ce qui est en
fait souvent le cas dans les serveurs HTML, parce que
typiquement, il y a une connection par URL), on peut toujours se
servir d'un pool de threads ; quand un thread finit avec une
connection, il ne se termine pas, mais se remet dans le pool
pour reservir.

GetFileAttributesEx : merci pour la fonction, je ne l'avais pas
trouvée :( (je m'étais borné à l'utilisation du tellg...)


Attention : c'est une fonction de l'API de Windows. Tu ne la
trouveras pas, par exemple, sous Linux. En revanche, elle donne
exactement l'information que tu veux (y compris la taille sur 64
bits). (En fait, sous Unix, je ferais un stat, pour en obtenir
la taille, puis mmap, pour simplement mapper le fichier dans mon
espace mémoire. Pas besoin de le « lire » du tout. La même
possibilité existe sous Windows, mais je ne connais pas les
détails.)

Comme j'ai dit, si tu le crois acceptable d'exiger que le
fichier tient en mémoire, la solution la plus sûre, c'est de le
lire d'abord, et puis générer l'en-tête.

Finalement, concernant les prototypes de fonctions, les mans & co,
auriez vous un site que vous considéreriez de "bien" ? (càd, mieux que
la msdn qui semble avoir quelques lacunes....)


Pour les fonctions standard, je conseille :
http://www.dinkumware.com/manuals/?manual=compleat&page=index.html.
Mais c'est une référence aussi, non un texte d'apprentissage.

--
James Kanze (Gabi Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Avatar
NaeiKinDus
On 2 avr, 20:39, "James Kanze" wrote:
On Apr 2, 6:42 pm, "NaeiKinDus" wrote:

Très bien, veuillez m'excuser pour la confusion des genres, mais un
"corresponding to a long" veut dire que type == long... A tord semb le
t il !


Je ne sais pas. « Corresponding », en anglais, c'est assez
vague. Ça peut signifie que c'est un typedef, comme ça peut
signifie que c'est un type qui a les même valeurs. Si c'était
réelement un long, je me serais attendu à « is a long »,
carrément.



Effectivement... mais la confusion était possible :)

Je m'en vais corriger ça de suite.
Pour le petit programme isolant le soucis, j'en serais bien en peine
de réaliser ça car le soucis peut se trouver à pas mal d'endroits , et
prendree en compte les threads, les sockets, etc... dur.
Idx : long qui contient la position dans la lecture de fichier... A
changer donc par un streampos.


Ou à éliminer carrément. Le flux lui-même garde bien sa postion.
Dans la mesure où tu le lis séquentiellement, il n'y a pas
besoin que tu gères la position. Tu fais un source.read( buf,
4096 ), et le coup suivant, il va lire les 4096 octets suivants.
Et ainsi jusqu'à la fin. Tu n'as même pas besoin de savoir la
taille d'avance. (Sauf que tu dois la mettre dans l'en-tête,
évidemment. Mais si tout le fichier tient en mémoire, je
réserverais simplement la place pour mettre la taille, et je le
mettrait par la suite, une fois la lecture finie.)


Le flux garde la position même en cas de sortie de la fonction,
rebouclage, entrée dans la fonction et ouverture d'un autre fichier ?
Je penserais plus que l'objet est perdu en mémoire...
Enfin, non, le fichier ne doit absolument pas tenir en mémoire : si le
serveur propose, par exemple, de downloader un OS complet de 2Go... Je
doute que le serveur accepte de loader ça en mémoire !

Et puisque tu te sers de istream::read, je te previens tout de
suite d'un petit piège. Quand tu fais source.read(), le flux se
met en état d'erreur s'il n'a pas pû lire tous les octets que tu
lui a démandés. Si donc tu lis par bloc de 4096, et le fichier
n'a pas une taille multiple de 4096, tu auras une erreur sur la
dernière lecture, alors qu'il y a bien des octets lus qu'il faut
que tu prennes en compte. Après l'erreur, il faut que tu
appelles aussi source.gcount(), fonction membre de istream, qui
t'indique le nombre d'octets réelement lus dans le dernier read.


Merci beaucoup ! J'avais effectivement été confronté à ce soucis et il
me semblait insolvable.


Sinon (mais je ne suis pas sûr que tu dois t'en servir dans le
cadre d'un projet d'étude) : moi, je me servirais directement
du filebuf, sans passer par le ifstream. filebuf a une fonction,
sgetn, qui renvoie le nombre d'octets lus -- tu lis jusqu'à ce
qu'elle renvoie 0.


Je pourrais le faire, effectivement... Mais y a t'il une réelle
différence ? Le sgetn ne serait il pas comme un read simplement couplé
avec un gcount ?


2°/ Il semblerait que cela ne soit pas bon à fort taux de charge, a vec
un ralentissement considérable du serveur...


Ça dépend. C'est vrai que créer un thread n'est pas toujours bon
marché. Mais si c'est un problème de performance (ce qui est en
fait souvent le cas dans les serveurs HTML, parce que
typiquement, il y a une connection par URL), on peut toujours se
servir d'un pool de threads ; quand un thread finit avec une
connection, il ne se termine pas, mais se remet dans le pool
pour reservir.



J'avais pensé à mettre en pratique la solution des IOCP mais... nous
n'avons que peu de temps pour boucler le projet, aussi je ne me
sentais pas assez fort pour me lancer là dedans...

Pour les fonctions standard, je conseille :http://www.dinkumware.com/manu als/?manual=compleat&page=index.html.
Mais c'est une référence aussi, non un texte d'apprentissage.


J'ai la connaissance acquise du C, mais nullement l'approche objet
induite par C++... Et ne sachant pas vraiment non plus les ajouts /
modifications entre C et C++ (ou même la programmation sous
Windows...)
Merci, donc, j'utiliserais cette source tant que nécessaire :D


Avatar
Jean-Marc Bourguet
"NaeiKinDus" writes:

Très bien, veuillez m'excuser pour la confusion des genres, mais un
"corresponding to a long" veut dire que type == long... A tord semble t
il !


Ca m'étonne quand même fort de trouver ca... est-ce que tu ne regarderais
pas dans la doc d'une vieille version (VC 6.0 au hasard :-))

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
NaeiKinDus
Ca m'étonne quand même fort de trouver ca... est-ce que tu ne regarde rais
pas dans la doc d'une vieille version (VC 6.0 au hasard :-))



Non non :)
Je l'ai copier/coller directement depuis la msdn !

lien :: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ vclang98/html/_iostream_istream.3a3a.tellg.asp

Avatar
NaeiKinDus
Mince alors !
Les docs des différents VC++ semblent mixées !
Pour Tellg, il y a le lien que j'ai donné, avec VC98 dedans, et un
autre, qui ne répète pas l'erreur du long et explique même le fameux
-1 que tellg me retournait !
http://msdn2.microsoft.com/en-us/library/y464378x(VS.80).aspx

C'est vraiment mal fait leur truc :(
Avatar
Jean-Marc Bourguet
"NaeiKinDus" writes:

Ca m'étonne quand même fort de trouver ca... est-ce que tu ne regarderais
pas dans la doc d'une vieille version (VC 6.0 au hasard :-))



Non non :)
Je l'ai copier/coller directement depuis la msdn !

lien :: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang98/html/_iostream_istream.3a3a.tellg.asp



Page en tete de laquelle on lit:

MSDN Home > MSDN Library > Development Tools and Languages > Visual
Studio 6.0 > Visual C and C++ 6.0 >

J'avais écrit quoi?

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Sylvain
NaeiKinDus wrote on 02/04/2007 22:06:

C'est vraiment mal fait leur truc :(


non, c'est exprès et même utile! à moins que tu préfères un mode pensée
unique où toutes références relatives à une version n-1 d'un soft
disparaisse le jour de la parution de la version n !...

la page <http://msdn2.microsoft.com/en-us/library/aa187916.aspx> te
donne l'index des environnements, dont VS2005, '.NET' (l'inutilisable
'2003') et VS6 (aka VS98) même s'il n'est plus supporté il reste documenté.

Sylvain.

Avatar
NaeiKinDus
C'est vraiment mal fait leur truc :(


non, c'est exprès et même utile! à moins que tu préfères un mod e pensée
unique où toutes références relatives à une version n-1 d'un soft
disparaisse le jour de la parution de la version n !...


Non non, c'est juste qu'on ne voit pas de claire séparation entre les
versions... d'où mon erreur ! Mais je le confesse, je ne suis pas un
habitué de la MSDN...


Avatar
James Kanze
On Apr 2, 9:35 pm, "NaeiKinDus" wrote:
On 2 avr, 20:39, "James Kanze" wrote:

On Apr 2, 6:42 pm, "NaeiKinDus" wrote:

Très bien, veuillez m'excuser pour la confusion des genres, mais un
"corresponding to a long" veut dire que type == long... A tord se mble
t il !


Je ne sais pas. « Corresponding », en anglais, c'est assez
vague. Ça peut signifie que c'est un typedef, comme ça peut
signifie que c'est un type qui a les même valeurs. Si c'était
réelement un long, je me serais attendu à « is a long »,
carrément.


Effectivement... mais la confusion était possible :)


Tout à fait. Le choix des mots dans le document n'est pas des
plus heureux, et dans un document de référence, un mot comme
« corresponding » n'a de place que si on explique en quoi il y
a correspondance.

Je m'en vais corriger ça de suite.
Pour le petit programme isolant le soucis, j'en serais bien en peine
de réaliser ça car le soucis peut se trouver à pas mal d'endroi ts, et
prendree en compte les threads, les sockets, etc... dur.
Idx : long qui contient la position dans la lecture de fichier... A
changer donc par un streampos.


Ou à éliminer carrément. Le flux lui-même garde bien sa postion.
Dans la mesure où tu le lis séquentiellement, il n'y a pas
besoin que tu gères la position. Tu fais un source.read( buf,
4096 ), et le coup suivant, il va lire les 4096 octets suivants.
Et ainsi jusqu'à la fin. Tu n'as même pas besoin de savoir la
taille d'avance. (Sauf que tu dois la mettre dans l'en-tête,
évidemment. Mais si tout le fichier tient en mémoire, je
réserverais simplement la place pour mettre la taille, et je le
mettrait par la suite, une fois la lecture finie.)


Le flux garde la position même en cas de sortie de la fonction,
rebouclage, entrée dans la fonction et ouverture d'un autre fichier ?


Le flux garde la position tant qu'il est ouvert. Si tu fermes le
fichier en quittant la fonction (par exemple en appelant le
destructeur du ifstream, si la variable est locale), tu perds la
position. Mais tu ne veux pas faire ça ; tu veux garder le flux
ouvert. Le ifstream doit donc être une variable à durée de vie
supérieur à celle de la fonction.

Je penserais plus que l'objet est perdu en mémoire...


Il obeit les même règles que ton int (qui contenait l'indice).
Si tu sais garder la position entre plusieurs appels de la
fonction, tu sais garder le flux.

Enfin, non, le fichier ne doit absolument pas tenir en mémoire : si le
serveur propose, par exemple, de downloader un OS complet de 2Go... Je
doute que le serveur accepte de loader ça en mémoire !


C'est ce dont je n'étais pas sûr. Je ne sais pas ce que fait ton
« request.append() » ; s'il bâtit la requête en mémoire ou
non. Et on pourrait très bien limiter la taille des fichiers
qu'on accepte de servir, selon l'utilisation.

Et puisque tu te sers de istream::read, je te previens tout de
suite d'un petit piège. Quand tu fais source.read(), le flux se
met en état d'erreur s'il n'a pas pû lire tous les octets que tu
lui a démandés. Si donc tu lis par bloc de 4096, et le fichier
n'a pas une taille multiple de 4096, tu auras une erreur sur la
dernière lecture, alors qu'il y a bien des octets lus qu'il faut
que tu prennes en compte. Après l'erreur, il faut que tu
appelles aussi source.gcount(), fonction membre de istream, qui
t'indique le nombre d'octets réelement lus dans le dernier read.


Merci beaucoup ! J'avais effectivement été confronté à ce soucis et il
me semblait insolvable.


Il faut dire que l'utilisation des flux standard pour des
entrées non formattées textes est un peu tordue:-).

Sinon (mais je ne suis pas sûr que tu dois t'en servir dans le
cadre d'un projet d'étude) : moi, je me servirais directement
du filebuf, sans passer par le ifstream. filebuf a une fonction,
sgetn, qui renvoie le nombre d'octets lus -- tu lis jusqu'à ce
qu'elle renvoie 0.


Je pourrais le faire, effectivement... Mais y a t'il une réelle
différence ? Le sgetn ne serait il pas comme un read simplement coupl é
avec un gcount ?


Je dirais plutôt, est-ce qu'il y un intérêt à ne pas le faire ?
Qu'est-ce que t'apporte l'istream dans ce cas-ci ?
L'abstraction de base d'istream, c'est l'interprétation du texte
formatté ; la conversion d'une chaîne de caractères en un type
interne. Cette abstraction ne t'intéresse nullement. En plus, il
gère des erreurs, en rendant l'erreur « sticky », par exemple.
Mais ça aussi n'est utile que si tu as un nombre fixes de
données à lire -- ici, la façon qu'elle les gère va plutôt à
l'encontre de ce qu'il te faut. Enfin, l'ifstream gère la durée
de vie du filebuf. Mais puisque tu ne peux pas t'en servir comme
variable locale, il faut que tu gère la durée de vie de l'une ou
de l'autre explicitement, de toute façon.

Avec le filebuf, tu fais quelque chose du genre :

std::filebuf* input = new std::filebuf ;
if ( ! input->open( ... ) ) {
// Erreur d'ouverture...
delete input ;
input = NULL ;
}

puis, pour chaque lecture :

int nombreLu = input->sgetn( buffer, sizeof( buffer ) ) ;
if ( nombreLu <= 0 ) {
delete input ;
input = NULL ;
} else {
request.append( buffer, nombreLu ) ;
// ou est-ce qu'il faut :
// request.append( std::string( buffer, nombreLu ) ) ;
}

Quand input devient NULL, la requête est finie.

Si tu veux gérer plusieurs connexions en parallel, il te
faudrait un espèce de map, plutôt qu'un pointeur tout nu.
Quelque chose du genre :

typedef std::map< ConnectionId, std::filebuf* >
CxMap ;
CxMap cxMap ;

Dans ce cas-là, à la place de mettre le pointeur à NULL, tu
l'enlève du map. Dans ce cas-là, j'utiliserais un
std::auto_ptr lors de l'ouverture du fichier, et seulement quand
tout c'est bien passé :
cxMap.insert( CxMap::value_type( cxId, filebufPtr.get()) ) ;
filebufPtr.release() ;
Attention aussi de ne faire la release() qu'après le pointeur
est bien dans le map ; l'insertion peut lever une exception.

2°/ Il semblerait que cela ne soit pas bon à fort taux de charge, avec
un ralentissement considérable du serveur...


Ça dépend. C'est vrai que créer un thread n'est pas toujours bon
marché. Mais si c'est un problème de performance (ce qui est en
fait souvent le cas dans les serveurs HTML, parce que
typiquement, il y a une connection par URL), on peut toujours se
servir d'un pool de threads ; quand un thread finit avec une
connection, il ne se termine pas, mais se remet dans le pool
pour reservir.


J'avais pensé à mettre en pratique la solution des IOCP mais... nous
n'avons que peu de temps pour boucler le projet, aussi je ne me
sentais pas assez fort pour me lancer là dedans...


Là, je ne m'y connais pas du tout. Je travaille sur des
serveurs, mais des serveurs qui tournent sous Solaris, voire
depuis peu sous Linux. La seule fois que j'ai travaillé
professionnellement sous Windows, c'était pour écrire un GUI
avec Java.

Pour les fonctions standard, je conseille :http://www.dinkumware.com/ma nuals/?manual=compleat&page=index.html.
Mais c'est une référence aussi, non un texte d'apprentissage.


J'ai la connaissance acquise du C, mais nullement l'approche objet
induite par C++... Et ne sachant pas vraiment non plus les ajouts /
modifications entre C et C++ (ou même la programmation sous
Windows...)


L'approche C++ est souvent bien différente de l'approche C, même
si une bonne partie du C marche tel quel en C++. Dans ce cas-ci,
en revanche, je ne suis pas sûr que les différences soient
énorme. filebuf se comporte assez comme un FILE* pour la lecteur
du binaire -- et encore plus comme un fd sous Unix ou un HANDLE
sous Windows. Et tu as le map tout fait, tandis qu'en C, il
faudrait que tu le code toi-même. Mais a priori, je ne vois pas
beaucoup dans ce que tu fais où l'approche objet (ou l'approche
programmation générique) t'apporterait quelque chose.

La seule vraie différence, que tu ne peux pas laisser de côté,
c'est que le C++ peut lever les exceptions. C-à-d que tu ne vois
pas tous les flux possibles dans ton code, et qu'il faut que tu
te débrouilles pour que tout le clean-up nécessaire ait lieu
dans des destructeurs. (Mais... est-ce que tu aies besoin de
t'occuper des exceptions. A priori, dans le code que tu
utilises, les seules que tu risques de voir, c'est en cas
d'épuisement de la mémoire, ou en cas d'une erreur physique sur
le disque lors de la lecture d'un fichier. Et dans un projet
d'étude, il est peut-être acceptable d'aborter dans de tels
cas.)

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34



Avatar
James Kanze
On Apr 2, 10:35 pm, Sylvain wrote:
NaeiKinDus wrote on 02/04/2007 22:06:

C'est vraiment mal fait leur truc :(


non, c'est exprès et même utile!


C'est certainement utile d'avoir accès à la documentation des
anciennes versions. En revanche, quand tu fais une recherche, il
n'est pas évident quelle entrée correspond à quelle version dans
la liste des réponses. Si je cherche ParseDateTime, par exemple,
les six premières réponses ont un titre qui commence
« COldDateTime::ParseDateTime ». Trois d'entre elles ont aussi
« (ATL/MFC) » derrière -- je ne sais pas ce que ça signifie,
mais je void « (VS.80) » ou « (VS.71) » dans l'URL. Qui est
autrement illisible, ce qui est normal, parce que normalement,
une personne n'a pas à régarder l'URL -- c'est pour la machine.
Une autre entrée a « (Visual C++ 6.0) » ; d'accord, ce n'est
pas celle-là que je veux, mais des deux dernières ont « (Visual
C++ Libraries) » (ce qui me paraît bonne), et rien de tout.

C'est nettement plus facile à naviguer que les pages de man sous
Unix, mais c'est loin d'être parfait non plus.

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


1 2 3