2012-05-25 12 views
26

Sto lavorando a un correttore di link, in generale posso eseguire richieste HEAD, tuttavia alcuni siti sembrano disabilitare questo verbo, quindi in caso di errore devo anche eseguire una richiesta GET (per ricontrollare che il link sia davvero morto)Come posso eseguire una richiesta GET senza scaricare il contenuto?

io uso il seguente codice come il mio link tester:

public class ValidateResult 
{ 
    public HttpStatusCode? StatusCode { get; set; } 
    public Uri RedirectResult { get; set; } 
    public WebExceptionStatus? WebExceptionStatus { get; set; } 
} 


public ValidateResult Validate(Uri uri, bool useHeadMethod = true, 
      bool enableKeepAlive = false, int timeoutSeconds = 30) 
{ 
    ValidateResult result = new ValidateResult(); 

    HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest; 
    if (useHeadMethod) 
    { 
    request.Method = "HEAD"; 
    } 
    else 
    { 
    request.Method = "GET"; 
    } 

    // always compress, if you get back a 404 from a HEAD it can be quite big. 
    request.AutomaticDecompression = DecompressionMethods.GZip; 
    request.AllowAutoRedirect = false; 
    request.UserAgent = UserAgentString; 
    request.Timeout = timeoutSeconds * 1000; 
    request.KeepAlive = enableKeepAlive; 

    HttpWebResponse response = null; 
    try 
    { 
    response = request.GetResponse() as HttpWebResponse; 

    result.StatusCode = response.StatusCode; 
    if (response.StatusCode == HttpStatusCode.Redirect || 
     response.StatusCode == HttpStatusCode.MovedPermanently || 
     response.StatusCode == HttpStatusCode.SeeOther) 
    { 
     try 
     { 
     Uri targetUri = new Uri(Uri, response.Headers["Location"]); 
     var scheme = targetUri.Scheme.ToLower(); 
     if (scheme == "http" || scheme == "https") 
     { 
      result.RedirectResult = targetUri; 
     } 
     else 
     { 
      // this little gem was born out of http://tinyurl.com/18r 
      // redirecting to about:blank 
      result.StatusCode = HttpStatusCode.SwitchingProtocols; 
      result.WebExceptionStatus = null; 
     } 
     } 
     catch (UriFormatException) 
     { 
     // another gem... people sometimes redirect to http://nonsense:port/yay 
     result.StatusCode = HttpStatusCode.SwitchingProtocols; 
     result.WebExceptionStatus = WebExceptionStatus.NameResolutionFailure; 
     } 

    } 
    } 
    catch (WebException ex) 
    { 
    result.WebExceptionStatus = ex.Status; 
    response = ex.Response as HttpWebResponse; 
    if (response != null) 
    { 
     result.StatusCode = response.StatusCode; 
    } 
    } 
    finally 
    { 
    if (response != null) 
    { 
     response.Close(); 
    } 
    } 

    return result; 
} 

Questo tutto funziona bene e dandy. Tranne che quando eseguo una richiesta GET, l'intero payload viene scaricato (l'ho visto in wireshark).

C'è un modo per configurare lo ServicePoint sottostante o lo HttpWebRequest per non bufferizzare o caricare il corpo della risposta a tutti?

(Se fossi d'uso e questo vorrei impostare la finestra di ricezione TCP veramente basso, e poi prendere solo abbastanza pacchetti per ottenere le intestazioni, smettere di ACKing pacchetti TCP, non appena ho abbastanza informazioni.)

per quelli che si chiedono che cosa si intende ottenere, non voglio scaricare un 40k 404 quando ottengo un 404, facendo questo alcune centinaia di migliaia di volte è costoso sulla rete

+0

nota, anche se la codifica manuale della versione HTTP è abbastanza semplice, l'HTTPS mi spaventa un po '. (forse esiste già una libreria del sistema operativo?) –

+0

Prova un download parziale. È possibile scaricare solo un intervallo con l'intestazione http del range. – rekire

+1

@rekire 'Content-Range' può essere ok per i server HTTP 1.1 che hanno il contenuto, ma se si ottiene un 404, verrebbe comunque rispedito completamente –

risposta

8

Quando si esegue un GET, il server inizia l'invio dei dati dall'inizio del file fino alla fine. A meno che tu non lo interrompa. Garantito, a 10 Mb/sec, sarà un megabyte al secondo, quindi se il file è piccolo otterrete il tutto. Puoi ridurre al minimo la quantità effettivamente scaricata in due modi.

In primo luogo, è possibile chiamare request.Abort dopo aver ricevuto la risposta e prima di chiamare response.close. Ciò garantirà che il codice sottostante non provi a scaricare l'intero elemento prima di chiudere la risposta. Non so se questo aiuti su file di piccole dimensioni. So che impedirà la sospensione dell'applicazione quando si tenta di scaricare un file multi-gigabyte.

L'altra cosa che puoi fare è richiedere un intervallo, piuttosto che l'intero file. Vedi il metodo AddRange e i suoi sovraccarichi. Ad esempio, è possibile scrivere request.AddRange(512), che scaricherà solo i primi 512 byte del file. Ciò dipende, ovviamente, dal server che supporta le query di intervallo. La maggior parte lo fa. Ma poi, la maggior parte supporta anche le richieste HEAD.

probabilmente finirete per dover scrivere un metodo che cerca le cose in sequenza:

  • cercare di fare una richiesta HEAD. Se ciò funziona (ad es.non restituisce un 500), quindi hai finito
  • prova GET con una query di intervallo. Se quello non restituisce un 500, allora hai finito.
  • fare un GET regolare, con un request.Abort dopo i ritorni GetResponse.
+0

Una chiamata a request.Abort, abbastanza presto farà sì che l'ACK torni con un flag "FIN" impostato , questo chiuderà la connessione con garbo senza che il client riceva una grande quantità di dati. Solo un leggero punto interrogativo che ho riguarda la possibilità di impostare le dimensioni della finestra di ricezione del client ... –

+0

ci sono alcune correzioni critiche ... HEAD può restituire 404 ma ottenere può restituire un 200. La query GET range fa davvero poca differenza nella scia di un abortimento funzionante. (dovrebbe essere, cioè, restituire un codice di stato inferiore a 400) –

+0

"Si potrebbe, ad esempio, scrivere' request.AddRange (512) ', che scaricherà solo i primi 512 byte del file." Non dovrebbe essere '-512'? MSDN afferma: "Se l'intervallo è negativo, il parametro intervallo specifica il punto finale dell'intervallo e il server deve iniziare a inviare i dati dall'inizio dei dati nell'entità HTTP al parametro di intervallo specificato." (Http://msdn.microsoft.com/en-us/library/4ds43y3w) –

0

Non potresti usare un Web Client per aprire un flusso e leggere solo i pochi byte necessari?

using (var client = new WebClient()) 
     { 
      using (var stream = client.OpenRead(uri)) 
      { 
       const int chunkSize = 100; 
       var buffer = new byte[chunkSize]; 
       int bytesRead; 
       while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) 
       { 
        //check response here 
       } 
      } 
     } 

Non sono sicuro di come WebClient apre lo stream internamente. Ma sembra consentire la lettura parziale dei dati.

+2

WebClient.OpenRead (...) utilizza internamente il metodo GetResponse(), quindi questo metodo non funzionerà. Scaricherà il tutto. – Mikhail

+0

confermato ... non funziona ... http://i.stack.imgur.com/wGRrv.png –

+0

Sì, l'ho provato anche io. Non riesco a trovare alcuna classe incorporata che consenta di elaborare risposte web parziali. Dovrebbe essere possibile atleast quando si utilizzano operazioni asincrone. – nunespascal

1

Se si utilizza una richiesta GET, si riceverà il corpo del messaggio sia che lo si voglia o no. I dati verranno comunque trasmessi al tuo endpoint indipendentemente dal fatto che tu lo abbia letto o meno dal socket. I dati rimarranno in coda nel RecvQ in attesa di essere selezionati.

Per questo, si dovrebbe davvero utilizzare una richiesta "HEAD" se possibile, che ti risparmierà il corpo del messaggio.

+1

Vedere la risposta di Jim, il metodo .Abort funziona, imposta la flag FIN con l'ACK, che chiude la connessione con garbo –

Problemi correlati