Désastre du mode asynchrone WinINet

Désolé pour une si longue question. C’est juste que j’ai passé plusieurs jours à essayer de résoudre mon problème, et je suis épuisé.

J’essaie d’utiliser WinINet en mode asynchrone. Et je dois dire que c’est tout simplement fou . Je ne peux vraiment pas comprendre cela. Il fait tellement de choses, mais malheureusement, son API asynchrone est tellement mal conçue qu’elle ne peut tout simplement pas être utilisée dans une application sérieuse avec des exigences de stabilité élevées.

Mon problème est le suivant: je dois effectuer beaucoup de transactions HTTP / HTTPS en série, alors que je dois également pouvoir les interrompre immédiatement sur demande.

J’allais utiliser WinINet de la manière suivante:

  1. Initialiser l’utilisation de WININet via InternetOpen fonction InternetOpen avec l’indicateur INTERNET_FLAG_ASYNC .
  2. Installez une fonction de rappel global (via InternetSetStatusCallback ).

Maintenant, pour effectuer une transaction qui correspond à ce que je pensais faire:

  1. Allouer une structure par transaction avec différents membres décrivant l’état de la transaction.
  2. Appelez InternetOpenUrl pour lancer la transaction. En mode asynchrone, il renvoie généralement immédiatement une erreur, à savoir ERROR_IO_PENDING . Un de ses parameters est le «contexte», la valeur qui sera transmise à la fonction de rappel. Nous le définissons sur le pointeur vers la structure d’état par transaction.
  3. Très peu de temps après, la fonction de rappel global est appelée (à partir d’un autre thread) avec le statut INTERNET_STATUS_HANDLE_CREATED . A ce moment, nous sauvegardons le handle de session WinINet.
  4. Eventuellement, la fonction de rappel est appelée avec INTERNET_STATUS_REQUEST_COMPLETE lorsque la transaction est terminée. Cela nous permet d’utiliser un mécanisme de notification (tel que la définition d’un événement) pour notifier le thread d’origine que la transaction est terminée.
  5. Le thread qui a émis la transaction réalise qu’il est complet. Ensuite, il effectue le nettoyage: ferme le handle de session WinINet (par InternetCloseHandle ) et supprime la structure d’état.

Jusqu’à présent, il semble y avoir aucun problème.

Comment annuler une transaction en cours d’exécution? Une façon consiste à fermer le handle WinINet approprié. Et comme WinINet n’a pas de fonctions telles que InternetAbortXXXX – la fermeture du handle semble être le seul moyen d’abandonner.

En effet cela a fonctionné. Une telle transaction se termine immédiatement avec le code d’erreur ERROR_INTERNET_OPERATION_CANCELLED . Mais ici tous les problèmes commencent …

La première surprise désagréable que j’ai rencontrée est que WinINet a parfois tendance à invoquer la fonction de rappel pour la transaction même après l’avoir déjà annulée. Selon le MSDN, INTERNET_STATUS_HANDLE_CLOSING est la dernière invocation de la fonction de rappel. Mais c’est un mensonge . Ce que je vois, c’est qu’il ya parfois une notification INTERNET_STATUS_REQUEST_COMPLETE pour le même handle.

J’ai également essayé de désactiver la fonction de rappel pour le handle de transaction juste avant de le fermer, mais cela n’a pas aidé. Il semble que le mécanisme d’appel de rappel de WinINet soit asynchrone. Par conséquent, il peut appeler la fonction de rappel même après la fermeture du descripteur de transaction.

Cela pose un problème: tant que WinINet peut appeler la fonction de rappel, il est évident que je ne peux pas libérer la structure d’état de la transaction. Mais comment diable puis-je savoir si WinINet sera si gentil de l’appeler? De ce que j’ai vu – il n’y a pas de cohérence.

Néanmoins, j’ai travaillé autour de ça. Au lieu de cela, je garde maintenant une carte globale (protégée par la section critique bien sûr) des structures de transaction allouées. Ensuite, à l’intérieur de la fonction de rappel, je m’assure que la transaction existe bel et bien et la verrouille pendant toute la durée de l’appel de rappel.

Mais alors j’ai découvert un autre problème, que je ne pouvais pas résoudre jusqu’à présent. Il se produit lorsque j’annule une transaction très peu de temps après son démarrage.

Qu’est-ce qui se passe est que j’appelle InternetOpenUrl , qui renvoie le code d’erreur ERROR_IO_PENDING . Ensuite, j’attends juste (très court habituellement) jusqu’à ce que la fonction de rappel soit appelée avec la notification INTERNET_STATUS_HANDLE_CREATED . Ensuite, le descripteur de transaction est enregistré, de sorte que nous avons maintenant la possibilité d’abandonner sans fuites de gestion / ressources, et nous pouvons continuer.

J’ai essayé de faire avorter exactement après ce moment. C’est-à-dire fermez cette poignée immédiatement après l’avoir reçue. Devinez ce qui se passe? WinINet se bloque , access mémoire invalide! Et ce n’est pas lié à ce que je fais dans la fonction de rappel. La fonction de rappel n’est même pas appelée, le crash se situe quelque part au cœur de WinINet.

D’un autre côté, si j’attends la notification suivante (telle que “résoudre le nom”), cela fonctionne généralement. Mais parfois aussi se bloque! Le problème semble disparaître si je mets un peu de Sleep entre l’obtention de la poignée et sa fermeture. Mais évidemment, cela ne peut pas être accepté comme une solution sérieuse.

Tout cela me fait conclure: le WinINet est mal conçu.

  • Il n’y a pas de définition ssortingcte de la scope de l’appel de la fonction de rappel pour la session spécifique (transaction).
  • Il n’y a pas de définition ssortingcte du moment à partir duquel je suis autorisé à fermer le handle WinINet.
  • Qui sait quoi d’autre?

Ai-je tort? Est-ce quelque chose que je ne comprends pas? Ou WinINet ne peut tout simplement pas être utilisé en toute sécurité?

MODIFIER:

C’est le bloc de code minimal qui illustre le 2ème problème: crash. J’ai enlevé toute la gestion des erreurs et etc.

 HINTERNET g_hINetGlobal; struct Context { HINTERNET m_hSession; HANDLE m_hEvent; }; void CALLBACK INetCallback(HINTERNET hInternet, DWORD_PTR dwCtx, DWORD dwStatus, PVOID pInfo, DWORD dwInfo) { if (INTERNET_STATUS_HANDLE_CREATED == dwStatus) { Context* pCtx = (Context*) dwCtx; ASSERT(pCtx && !pCtx->m_hSession); INTERNET_ASYNC_RESULT* pRes = (INTERNET_ASYNC_RESULT*) pInfo; ASSERT(pRes); pCtx->m_hSession = (HINTERNET) pRes->dwResult; VERIFY(SetEvent(pCtx->m_hEvent)); } } void FlirtWInet() { g_hINetGlobal = InternetOpen(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC); ASSERT(g_hINetGlobal); InternetSetStatusCallback(g_hINetGlobal, INetCallback); for (int i = 0; i < 100; i++) { Context ctx; ctx.m_hSession = NULL; VERIFY(ctx.m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL)); HINTERNET hSession = InternetOpenUrl( g_hINetGlobal, _T("http://ww.google.com"), NULL, 0, INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD, DWORD_PTR(&ctx)); if (hSession) ctx.m_hSession = hSession; else { ASSERT(ERROR_IO_PENDING == GetLastError()); WaitForSingleObject(ctx.m_hEvent, INFINITE); ASSERT(ctx.m_hSession); } VERIFY(InternetCloseHandle(ctx.m_hSession)); VERIFY(CloseHandle(ctx.m_hEvent)); } VERIFY(InternetCloseHandle(g_hINetGlobal)); } 

Généralement, lors de la première / seconde itération, l’application se bloque. L’un des threads créés par WinINet génère une violation d’access:

 Access violation reading location 0xfeeefeee. 

Il convient de noter que l’adresse ci-dessus a une signification particulière pour le code écrit en C ++ (au moins MSVC). AFAIK lorsque vous supprimez un object qui a une vtable (c’est-à-dire – a des fonctions virtuelles) – il est défini à l’adresse ci-dessus. Pour que ce soit une tentative d’appeler une fonction virtuelle d’un object déjà supprimé.

déclaration de contexte ctx est la source du problème, elle est déclarée dans une boucle for (;;), c’est donc une variable locale créée pour chaque boucle, elle sera détruite et ne sera plus accessible à la fin de chaque boucle.

par conséquent, lorsqu’un rappel est appelé, ctx a déjà été détruit, le pointeur étant passé à des points de rappel sur un fichier ctx détruit, un pointeur de mémoire non valide provoque le blocage.

Un merci spécial à Luke.

Tous les problèmes disparaissent lorsque j’utilise explicitement InternetConnect + HttpOpenRequest + HttpSendRequest au lieu de tout-en-un InternetOpenUrl .

Je ne reçois aucune notification sur le handle de demande (à ne pas confondre avec le handle ‘connection’). Plus de crash.