2010-08-05 9 views
5

Ci scusiamo per una domanda così lunga. Ho solo passato diversi giorni a cercare di risolvere il mio problema, e sono esausto.WinINet disastro in modalità asincrona

Sto cercando di utilizzare WinINet in modalità asincrona. E devo dire ... questo è semplicemente insano. Non riesco davvero a capirlo. Fa così tante cose, ma sfortunatamente la sua API asincrona è progettata così male che non può essere utilizzata in un'applicazione seria con richieste di stabilità elevate.

Il mio problema è il seguente: ho bisogno di fare molte transazioni HTTP/HTTPS in serie, mentre devo anche essere in grado di interromperle immediatamente su richiesta.

Stavo per usare WinINet nel modo followig:

  1. utilizzo di inizializzazione WININET via InternetOpen funzione con INTERNET_FLAG_ASYNC bandiera.
  2. Installare una funzione di richiamata globale (tramite InternetSetStatusCallback).

Ora, al fine di eseguire una transazione che è quello che ho pensato di fare:

  1. allocare una struttura di per-transazione con vari membri che descrivono lo stato della transazione.
  2. Chiamare InternetOpenUrl per avviare la transazione. Nella modalità asincrona di solito ritorna immediatamente con un errore, che è ERROR_IO_PENDING. Uno dei suoi parametri è il "contesto", il valore che verrà passato alla funzione di callback. Impostiamo il puntatore sulla struttura dello stato per transazione.
  3. Poco dopo viene richiamata la funzione di callback globale (da un altro thread) con lo stato INTERNET_STATUS_HANDLE_CREATED. Al momento salviamo l'handle della sessione WinINet.
  4. Eventualmente la funzione di richiamata viene invocata con INTERNET_STATUS_REQUEST_COMPLETE quando la transazione è completa. Questo ci consente di utilizzare alcuni meccanismi di notifica (come l'impostazione di un evento) per notificare al thread di origine che la transazione è stata completata.
  5. Il thread che ha emesso la transazione si rende conto che è completo. Quindi esegue la pulizia: chiude l'handle di sessione WinINet (entro il InternetCloseHandle) e cancella la struttura dello stato.

Finora non sembra esserci alcun problema.

Come interrompere una transazione in corso di esecuzione? Un modo è quello di chiudere l'handle di WinINet appropriato. E poiché WinINet non ha funzioni come InternetAbortXXXX - la chiusura della maniglia sembra essere l'unico modo per interrompere.

Effettivamente questo ha funzionato. Tale transazione viene completata immediatamente con il codice di errore ERROR_INTERNET_OPERATION_CANCELLED. ma qui tutti i problemi cominciano ...

La prima sorpresa sgradevole che ho incontrato è che WinINet tende a richiamare a volte la funzione di callback per la transazione, anche dopo è già stata interrotta. In base al MSDN, l'INTERNET_STATUS_HANDLE_CLOSING è l'ultima chiamata della funzione di callback. Ma è una bugia.Quello che vedo è che a volte c'è una conseguente notifica INTERNET_STATUS_REQUEST_COMPLETE per lo stesso handle.

Ho anche provato a disattivare la funzione di callback per l'handle della transazione appena prima di chiuderla, ma questo non ha aiutato. Sembra che il meccanismo di richiamo di callback di WinINet sia asincrono. Quindi, può chiamare la funzione di callback anche dopo che l'handle della transazione è stato chiuso.

Questo impone un problema: finché Wininet può chiamare richiamare la funzione - ovviamente non posso liberare la struttura dello stato della transazione. Ma come diavolo faccio a sapere se WinINet sarà così gentile da chiamarlo o no? Da quello che ho visto - non c'è coerenza.

Tuttavia, ho lavorato in questo senso. Invece ora mantengo una mappa globale (protetta dalla sezione critica ovviamente) delle strutture di transazione allocate. Quindi, all'interno della funzione di callback, mi assicuro che la transazione esista effettivamente e la blocchi per la durata del richiamo di callback.

Ma poi ho scoperto un altro problema, che non ho potuto risolvere finora. Sorge quando interrompo una transazione molto poco dopo che è iniziata.

Quello che succede è che io chiamo InternetOpenUrl, che restituisce il codice di errore ERROR_IO_PENDING. Quindi devo solo aspettare (molto breve di solito) fino a quando la funzione di callback sarà chiamata con la notifica INTERNET_STATUS_HANDLE_CREATED. Quindi, l'handle della transazione viene salvato, in modo che ora abbiamo l'opportunità di interrompere senza handle/perdite di risorse, e possiamo andare avanti.

Ho provato a fare l'annullamento esattamente dopo questo momento. Cioè, chiudere questo handle immediatamente dopo averlo ricevuto. Indovina cosa succede? WinINet si arresta in modo anomalo, accesso alla memoria non valido! E questo non è legato a quello che faccio nella funzione di callback. La funzione di callback non è nemmeno chiamata, il crash è da qualche parte nel profondo di WinINet.

D'altra parte, se attendo la prossima notifica (come "risolvere il nome"), di solito funziona. Ma a volte si blocca anche! Il problema sembra scomparire se inserisco un minimo di Sleep tra l'ottenimento della maniglia e la chiusura. Ma ovviamente questo non può essere accettato come una soluzione seria.

Tutto ciò mi permette di concludere: Il WinINet è mal progettato.

  • Non esiste una definizione rigida sull'ambito del richiamo della funzione di callback per la sessione specifica (transazione).
  • Non esiste una definizione rigida sul momento da cui sono autorizzato a chiudere l'handle di WinINet.
  • Chissà cos'altro?

Mi sbaglio? È qualcosa che non capisco? Oppure WinINet non può essere usato con sicurezza?

EDIT:

Questo è il blocco di codice minimo che dimostra la 2 ° edizione: crash. Ho rimosso tutta la gestione degli errori e così via

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)); 
} 

Di solito in prima/seconda iterazione l'applicazione si arresta in modo anomalo. Uno dei thread creato dal WinINet genera una violazione di accesso:

Access violation reading location 0xfeeefeee. 

Vale la pena di notare che l'indirizzo di cui sopra ha un significato speciale per il codice scritto in C++ (almeno MSVC). AFAIK quando si elimina un oggetto che ha un vtable (ad esempio - ha funzioni virtuali) - è impostato sull'indirizzo precedente. In questo modo si tratta di un tentativo di chiamare una funzione virtuale di un oggetto già eliminato.

+4

Sono d'accordo che è un po 'difficile da usare, ma non vedo gli stessi problemi. Per il tuo primo numero, sei sicuro che INTERNET_STATUS_HANDLE_CLOSING e INTERNET_STATUS_REQUEST_COMPLETE vengano inviati per lo ** stesso ** handle? Ricevo maniglie diverse nella mia callback, anche se stranamente il REQUEST_COMPLETE sembra darmi l'handle che ottengo da InternetOpen. Inoltre, il tuo secondo scenario non causa un crash per me. Sto usando Windows XP SP3. – Luke

+0

Luke, grazie per la risposta. Sì, INTERNET_STATUS_REQUEST_COMPLETE è disponibile per un altro handle. Ma viene fornito con lo stesso valore di "contesto", che uso per identificare lo stato della mia richiesta. Mezzi: se questa notifica arriva dopo aver eliminato questo stato, si verificherà un problema. Ma, come ho detto, questo può essere risolto. L'altro problema sembra molto più crudele. E questo accade frequentemente (anche se non sempre). Sto parlando di chiudere l'handle di sessione ** immediatamente ** dopo che è stato segnalato da WinINet. Quindi, dopo un breve periodo, il codice WinINet genera una violazione di accesso. – valdo

+4

Penso che internamente InternetOpenUrl sia equivalente a InternetConnect + HttpOpenRequest (per le risorse http), quindi il callback viene richiamato sia con l'handle Connect che con l'handle Request (a seconda di quali eventi particolari si verificano). Poiché InternetOpenUrl accetta solo un parametro di contesto, viene passato per entrambi gli handle. Ho provato molte volte, ma non riesco a farlo andare in crash quando si chiama InternetCloseHandle durante HANDLE_CREATED. Non sei sicuro di quale potrebbe essere il problema. – Luke

risposta

2

Un ringraziamento speciale a Luca.

Tutti i problemi scompaiono quando utilizzo esplicitamente lo InternetConnect + HttpOpenRequest + HttpSendRequest anziché il dispositivo all-in-one InternetOpenUrl.

Non ricevo alcuna notifica sulla maniglia (da non confondere con l'handle 'connessione'). Inoltre non si arresta più.

+1

Suggerisco caldamente di considerare cosa suggerito da zjp. Il tuo nuovo codice potrebbe essere cambiato in modo che questo problema non si verifichi più, ma tu * stavi * usando una variabile che era fuori portata. –

7

dichiarazione di Context ctx è la fonte del problema, è dichiarata all'interno di un ciclo for (;;), quindi è una variabile locale creata per ogni ciclo, sarà distrutta e non più accessibile alla fine di ogni ciclo.

di conseguenza, quando viene richiamata una richiamata, ctx è già stato distrutto, il puntatore viene passato ai punti di richiamata a un ctx distrutto, il puntatore di memoria non valido causa l'arresto.