¿Cómo utilizar HttpWebRequest (.NET) de forma asincrónica?

156

¿Cómo puedo usar HttpWebRequest (.NET, C #) de forma asincrónica?

Jason
fuente
1
Consulte este artículo sobre Developer Fusion: developerfusion.com/code/4654/asynchronous-httpwebrequest
También puede ver lo siguiente, para obtener un ejemplo bastante completo de lo que Jason está pidiendo: stuff.seans.com/2009/01/05/… Sean
Sean Sexton
1
use async msdn.microsoft.com/en-us/library/…
Raj Kaimal
1
Por un momento, me pregunté si estabas tratando de comentar un hilo recursivo.
Kyle Hodgson

Respuestas:

125

Utilizar HttpWebRequest.BeginGetResponse()

HttpWebRequest webRequest;

void StartWebRequest()
{
    webRequest.BeginGetResponse(new AsyncCallback(FinishWebRequest), null);
}

void FinishWebRequest(IAsyncResult result)
{
    webRequest.EndGetResponse(result);
}

Se llama a la función de devolución de llamada cuando se completa la operación asincrónica. Necesita al menos llamar EndGetResponse()desde esta función.

Jon B
fuente
16
BeginGetResponse no es tan útil para el uso asíncrono. Parece bloquearse al intentar contactar con el recurso. Intente desconectar su cable de red o darle un uri con formato incorrecto, y luego ejecute este código. En su lugar, probablemente necesite ejecutar GetResponse en un segundo hilo que proporcione.
Ceniza
2
@AshleyHenderson - ¿Podrían proporcionarme una muestra?
Tohid
1
@Tohid aquí es una clase completa con muestra que he usado con Unity3D.
cregox
3
Debe agregar webRequest.Proxy = nullpara acelerar la solicitud dramáticamente.
Trontor
C # arroja un error diciéndome que esta es una clase obsoleta
AleX_
67

Considerando la respuesta:

HttpWebRequest webRequest;

void StartWebRequest()
{
    webRequest.BeginGetResponse(new AsyncCallback(FinishWebRequest), null);
}

void FinishWebRequest(IAsyncResult result)
{
    webRequest.EndGetResponse(result);
}

Puede enviar el puntero de solicitud o cualquier otro objeto como este:

void StartWebRequest()
{
    HttpWebRequest webRequest = ...;
    webRequest.BeginGetResponse(new AsyncCallback(FinishWebRequest), webRequest);
}

void FinishWebRequest(IAsyncResult result)
{
    HttpWebResponse response = (result.AsyncState as HttpWebRequest).EndGetResponse(result) as HttpWebResponse;
}

Saludos

xlarsx
fuente
77
+1 para la opción que no excede el alcance de la variable 'solicitud', pero podría haber realizado una conversión en lugar de utilizar la palabra clave "como". Se lanzaría una InvalidCastException en lugar de una
confusa
64

Todos hasta ahora se han equivocado, porque BeginGetResponse()funciona un poco en el hilo actual. De la documentación :

El método BeginGetResponse requiere algunas tareas de configuración síncrona para completar (resolución DNS, detección de proxy y conexión de socket TCP, por ejemplo) antes de que este método se vuelva asíncrono. Como resultado, este método nunca debe llamarse en un subproceso de interfaz de usuario (UI) porque puede llevar un tiempo considerable (hasta varios minutos dependiendo de la configuración de la red) completar las tareas iniciales de configuración síncrona antes de que se produzca una excepción por un error o El método tiene éxito.

Para hacer esto bien:

void DoWithResponse(HttpWebRequest request, Action<HttpWebResponse> responseAction)
{
    Action wrapperAction = () =>
    {
        request.BeginGetResponse(new AsyncCallback((iar) =>
        {
            var response = (HttpWebResponse)((HttpWebRequest)iar.AsyncState).EndGetResponse(iar);
            responseAction(response);
        }), request);
    };
    wrapperAction.BeginInvoke(new AsyncCallback((iar) =>
    {
        var action = (Action)iar.AsyncState;
        action.EndInvoke(iar);
    }), wrapperAction);
}

Luego puede hacer lo que necesite con la respuesta. Por ejemplo:

HttpWebRequest request;
// init your request...then:
DoWithResponse(request, (response) => {
    var body = new StreamReader(response.GetResponseStream()).ReadToEnd();
    Console.Write(body);
});
Isak
fuente
2
¿No podría simplemente llamar al método GetResponseAsync de HttpWebRequest usando waitit (suponiendo que haya hecho que su función sea asíncrona)? Soy muy nuevo en C #, así que esto puede ser un completo truco ...
Brad
GetResponseAsync se ve bien, aunque necesitará .NET 4.5 (actualmente beta).
Isak
15
Jesús. Ese es un código feo. ¿Por qué no se puede leer el código asíncrono?
John Shedletsky,
¿Por qué necesita request.BeginGetResponse ()? ¿Por qué wrapperAction.BeginInvoke () no es suficiente?
Igor Gatis
2
@Gatis Hay dos niveles de llamadas asincrónicas: wrapperAction.BeginInvoke () es la primera llamada asincrónica a la expresión lambda que llama a request.BeginGetResponse (), que es la segunda llamada asincrónica. Como señala Isak, BeginGetResponse () requiere una configuración síncrona, por lo que lo envuelve en una llamada asincrónica adicional.
walkingTarget
64

Con mucho, la forma más fácil es usar TaskFactory.FromAsync desde el TPL . Es literalmente un par de líneas de código cuando se usa junto con las nuevas palabras clave async / wait :

var request = WebRequest.Create("http://www.stackoverflow.com");
var response = (HttpWebResponse) await Task.Factory
    .FromAsync<WebResponse>(request.BeginGetResponse,
                            request.EndGetResponse,
                            null);
Debug.Assert(response.StatusCode == HttpStatusCode.OK);

Si no puede usar el compilador C # 5, lo anterior se puede lograr usando el método Task.ContinueWith :

Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse,
                                    request.EndGetResponse,
                                    null)
    .ContinueWith(task =>
    {
        var response = (HttpWebResponse) task.Result;
        Debug.Assert(response.StatusCode == HttpStatusCode.OK);
    });
Nathan Baulch
fuente
Desde .NET 4 este enfoque TAP es preferible. Vea un ejemplo similar de MS - "Cómo: ajustar patrones EAP en una tarea" ( msdn.microsoft.com/en-us/library/ee622454.aspx )
Alex Klaus
Mucho más fácil que las otras formas
Don Rolling
8

Terminé usando BackgroundWorker, definitivamente es asíncrono a diferencia de algunas de las soluciones anteriores, maneja el retorno al hilo de la GUI por usted y es muy fácil de entender.

También es muy fácil manejar excepciones, ya que terminan en el método RunWorkerCompleted, pero asegúrese de leer esto: Excepciones no controladas en BackgroundWorker

Usé WebClient pero obviamente podrías usar HttpWebRequest.GetResponse si quisieras.

var worker = new BackgroundWorker();

worker.DoWork += (sender, args) => {
    args.Result = new WebClient().DownloadString(settings.test_url);
};

worker.RunWorkerCompleted += (sender, e) => {
    if (e.Error != null) {
        connectivityLabel.Text = "Error: " + e.Error.Message;
    } else {
        connectivityLabel.Text = "Connectivity OK";
        Log.d("result:" + e.Result);
    }
};

connectivityLabel.Text = "Testing Connectivity";
worker.RunWorkerAsync();
Eggbert
fuente
7
public static async Task<byte[]> GetBytesAsync(string url) {
    var request = (HttpWebRequest)WebRequest.Create(url);
    using (var response = await request.GetResponseAsync())
    using (var content = new MemoryStream())
    using (var responseStream = response.GetResponseStream()) {
        await responseStream.CopyToAsync(content);
        return content.ToArray();
    }
}

public static async Task<string> GetStringAsync(string url) {
    var bytes = await GetBytesAsync(url);
    return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
dragansr
fuente
6

.NET ha cambiado desde que se publicaron muchas de estas respuestas, y me gustaría proporcionar una respuesta más actualizada. Use un método asíncrono para iniciar un Taskque se ejecutará en un hilo de fondo:

private async Task<String> MakeRequestAsync(String url)
{    
    String responseText = await Task.Run(() =>
    {
        try
        {
            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            WebResponse response = request.GetResponse();            
            Stream responseStream = response.GetResponseStream();
            return new StreamReader(responseStream).ReadToEnd();            
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: " + e.Message);
        }
        return null;
    });

    return responseText;
}

Para usar el método asíncrono:

String response = await MakeRequestAsync("http://example.com/");

Actualizar:

Esta solución no funciona para aplicaciones UWP que usan en WebRequest.GetResponseAsync()lugar de WebRequest.GetResponse(), y no llama a los Dispose()métodos cuando corresponde. @dragansr tiene una buena solución alternativa que aborda estos problemas.

tronman
fuente
1
Gracias ! He estado tratando de encontrar un ejemplo asíncrono, muchos ejemplos usando un enfoque antiguo que es demasiado complejo.
WDUK
¿Esto no bloqueará un hilo para cada respuesta? parece bastante diferente a, por ejemplo, docs.microsoft.com/en-us/dotnet/standard/parallel-programming/…
Pete Kirkham el
@PeteKirkham Un hilo de fondo está haciendo la solicitud, no el hilo de la interfaz de usuario. El objetivo es evitar bloquear el hilo de la interfaz de usuario. Cualquier método que elija para realizar una solicitud bloqueará el hilo que realiza la solicitud. El ejemplo de Microsoft al que hace referencia está tratando de realizar múltiples solicitudes, pero todavía están creando una Tarea (un hilo de fondo) para las solicitudes.
tronman
3
Para ser claros, este es un código 100% síncrono / de bloqueo. Para usar async, WebRequest.GetResponseAsync()y StreamReader.ReadToEndAync()necesita ser usado y esperado.
Richard Szalay
44
@tronman Ejecutar métodos de bloqueo en una Tarea cuando hay equivalentes asíncronos disponibles es un antipatrón altamente desaconsejado. Si bien desbloquea el hilo de llamada, no hace nada para escalar en escenarios de alojamiento web, ya que solo está moviendo el trabajo a otro hilo en lugar de usar puertos de finalización de E / S para lograr la asincronía.
Richard Szalay
3
public void GetResponseAsync (HttpWebRequest request, Action<HttpWebResponse> gotResponse)
    {
        if (request != null) { 
            request.BeginGetRequestStream ((r) => {
                try { // there's a try/catch here because execution path is different from invokation one, exception here may cause a crash
                    HttpWebResponse response = request.EndGetResponse (r);
                    if (gotResponse != null) 
                        gotResponse (response);
                } catch (Exception x) {
                    Console.WriteLine ("Unable to get response for '" + request.RequestUri + "' Err: " + x);
                }
            }, null);
        } 
    }
Sten Petrov
fuente