Comment utiliser HttpWebRequest (.NET) de manière asynchrone?

156

Comment puis-je utiliser HttpWebRequest (.NET, C #) de manière asynchrone?

Jason
la source
1
Consultez cet article sur Developer Fusion: developerfusion.com/code/4654/asynchronous-httpwebrequest
Vous pouvez également voir ce qui suit, pour un exemple assez complet de ce que Jason demande: stuff.seans.com/2009/01/05/… Sean
Sean Sexton
1
use async msdn.microsoft.com/en-us/library/…
Raj Kaimal
1
pendant un instant, je me suis demandé si vous essayiez de commenter un fil récursif?
Kyle Hodgson

Réponses:

125

Utilisation HttpWebRequest.BeginGetResponse()

HttpWebRequest webRequest;

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

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

La fonction de rappel est appelée lorsque l'opération asynchrone est terminée. Vous devez au moins appeler à EndGetResponse()partir de cette fonction.

Jon B
la source
16
BeginGetResponse n'est pas très utile pour une utilisation asynchrone. Il semble se bloquer en essayant de contacter la ressource. Essayez de débrancher votre câble réseau ou de lui donner un uri malformé, puis exécutez ce code. Au lieu de cela, vous devez probablement exécuter GetResponse sur un deuxième thread que vous fournissez.
Ash
2
@AshleyHenderson - Pouvez-vous me fournir un échantillon?
Tohid
1
@Tohid voici une classe complète avec un exemple que j'ai utilisé avec Unity3D.
cregox
3
Vous devez ajouter webRequest.Proxy = nullpour accélérer considérablement la demande.
Trontor
C # renvoie une erreur me disant qu'il s'agit d'une classe obsolète
AleX_
67

Compte tenu de la réponse:

HttpWebRequest webRequest;

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

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

Vous pouvez envoyer le pointeur de requête ou tout autre objet comme celui-ci:

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

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

Salutations

xlarsx
la source
7
+1 pour l'option qui ne dépasse pas la variable "request", mais vous auriez pu effectuer un cast au lieu d'utiliser le mot clé "as". Une InvalidCastException serait lancée au lieu d'une NullReferenceException confuse
Davi Fiamenghi
64

Jusqu'à présent, tout le monde s'est trompé, car BeginGetResponse()il travaille sur le fil actuel. De la documentation :

La méthode BeginGetResponse nécessite l'exécution de certaines tâches de configuration synchrones (résolution DNS, détection de proxy et connexion de socket TCP, par exemple) avant que cette méthode ne devienne asynchrone. Par conséquent, cette méthode ne doit jamais être appelée sur un thread d'interface utilisateur (UI) car cela peut prendre un temps considérable (jusqu'à plusieurs minutes selon les paramètres réseau) pour terminer les tâches de configuration synchrones initiales avant qu'une exception pour une erreur ne soit levée ou la méthode réussit.

Alors pour bien faire:

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

Vous pouvez alors faire ce dont vous avez besoin avec la réponse. Par exemple:

HttpWebRequest request;
// init your request...then:
DoWithResponse(request, (response) => {
    var body = new StreamReader(response.GetResponseStream()).ReadToEnd();
    Console.Write(body);
});
Isak
la source
2
Ne pourriez-vous pas simplement appeler la méthode GetResponseAsync de HttpWebRequest en utilisant await (en supposant que vous ayez rendu votre fonction asynchrone)? Je suis très nouveau en C #, donc cela peut être un jibberish complet ...
Brad
GetResponseAsync a l'air bien, même si vous aurez besoin de .NET 4.5 (actuellement bêta).
Isak
15
Jésus. C'est un mauvais code. Pourquoi le code async ne peut-il pas être lisible?
John Shedletsky
Pourquoi avez-vous besoin de request.BeginGetResponse ()? Pourquoi wrapperAction.BeginInvoke () ne suffit pas?
Igor Gatis
2
@Gatis Il existe deux niveaux d'appels asynchrones - wrapperAction.BeginInvoke () est le premier appel asynchrone à l'expression lambda qui appelle request.BeginGetResponse (), qui est le deuxième appel asynchrone. Comme le souligne Isak, BeginGetResponse () nécessite une configuration synchrone, c'est pourquoi il l'enveloppe dans un appel asynchrone supplémentaire.
marche Objectif
64

Le moyen de loin le plus simple consiste à utiliser TaskFactory.FromAsync à partir du TPL . Il s'agit littéralement de quelques lignes de code lorsqu'il est utilisé en conjonction avec les nouveaux mots clés async / await :

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 vous ne pouvez pas utiliser le compilateur C # 5, les opérations ci-dessus peuvent être effectuées à l'aide de la méthode 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
la source
Depuis .NET 4, cette approche TAP est préférable. Voir un exemple similaire de MS - "Comment: encapsuler des modèles EAP dans une tâche" ( msdn.microsoft.com/en-us/library/ee622454.aspx )
Alex Klaus
Bien plus facile que les autres moyens
Don Rolling
8

J'ai fini par utiliser BackgroundWorker, il est définitivement asynchrone contrairement à certaines des solutions ci-dessus, il gère le retour au thread GUI pour vous, et il est très facile à comprendre.

Il est également très facile de gérer les exceptions, car elles se retrouvent dans la méthode RunWorkerCompleted, mais assurez-vous de lire ceci: Exceptions non gérées dans BackgroundWorker

J'ai utilisé WebClient mais vous pouvez évidemment utiliser HttpWebRequest.GetResponse si vous le souhaitez.

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
la source
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
la source
6

.NET a changé depuis que plusieurs de ces réponses ont été publiées, et j'aimerais fournir une réponse plus à jour. Utilisez une méthode async pour démarrer un Taskqui s'exécutera sur un thread d'arrière-plan:

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

Pour utiliser la méthode asynchrone:

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

Mettre à jour:

Cette solution ne fonctionne pas pour les applications UWP qui utilisent à la WebRequest.GetResponseAsync()place de WebRequest.GetResponse(), et n'appelle pas les Dispose()méthodes le cas échéant. @dragansr a une bonne solution alternative qui résout ces problèmes.

tronman
la source
1
Je vous remercie ! J'ai essayé de trouver un exemple asynchrone, de nombreux exemples utilisant une ancienne approche trop complexe.
WDUK
Cela ne bloquera-t-il pas un thread pour chaque réponse? cela semble assez différent de par exemple docs.microsoft.com/en-us/dotnet/standard/parallel-programming/…
Pete Kirkham
@PeteKirkham Un thread d'arrière-plan effectue la demande, pas le thread d'interface utilisateur. Le but est d'éviter de bloquer le thread de l'interface utilisateur. Toute méthode que vous choisissez pour faire une demande bloquera le thread effectuant la demande. L'exemple Microsoft auquel vous faites référence tente d'effectuer plusieurs demandes, mais ils créent toujours une tâche (un thread d'arrière-plan) pour les demandes.
tronman
3
Pour être clair, il s'agit d'un code 100% synchrone / bloquant. Pour utiliser async, WebRequest.GetResponseAsync()et StreamReader.ReadToEndAync()doivent être utilisés et attendus.
Richard Szalay
4
@tronman L'exécution de méthodes de blocage dans une tâche lorsque des équivalents asynchrones sont disponibles est un anti-pattern fortement déconseillé. Bien qu'il débloque le thread appelant, il ne fait rien pour la mise à l'échelle des scénarios d'hébergement Web, car vous déplacez simplement le travail vers un autre thread plutôt que d'utiliser les ports d'achèvement d'E / S pour réaliser l'asynchronie.
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
la source