Jak asynchronicznie używać HttpWebRequest (.NET)?

156

Jak mogę asynchronicznie używać HttpWebRequest (.NET, C #)?

Jason
źródło
1
Przeczytaj ten artykuł w witrynie Developer Fusion: developerfusion.com/code/4654/asynchronous-httpwebrequest
Możesz również zobaczyć następujące, całkiem kompletny przykład robienia tego, o co prosi Jason: stuff.seans.com/2009/01/05/ ... Sean
Sean Sexton
1
użyj async msdn.microsoft.com/en-us/library/…
Raj Kaimal
1
przez chwilę zastanawiałem się, czy próbujesz skomentować wątek rekurencyjny?
Kyle Hodgson

Odpowiedzi:

125

Posługiwać się HttpWebRequest.BeginGetResponse()

HttpWebRequest webRequest;

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

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

Funkcja wywołania zwrotnego jest wywoływana po zakończeniu operacji asynchronicznej. Musisz przynajmniej zadzwonić EndGetResponse()z tej funkcji.

Jon B.
źródło
16
BeginGetResponse nie jest tak przydatne w przypadku użycia asynchronicznego. Wydaje się, że blokuje się podczas próby skontaktowania się z zasobem. Spróbuj odłączyć kabel sieciowy lub nadać mu zniekształcony identyfikator URI, a następnie uruchom ten kod. Zamiast tego prawdopodobnie będziesz musiał uruchomić GetResponse na drugim wątku, który podasz.
Ash
2
@AshleyHenderson - Czy mógłbyś podać mi próbkę?
Tohid
1
@Tohid tutaj jest pełną klasą z próbką, której używałem z Unity3D.
cregox
3
Należy dodać, webRequest.Proxy = nullaby znacznie przyspieszyć żądanie.
Trontor
C # zgłasza błąd informujący mnie, że to jest przestarzała klasa
AleX_.
67

Biorąc pod uwagę odpowiedź:

HttpWebRequest webRequest;

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

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

Możesz wysłać wskaźnik żądania lub dowolny inny obiekt, taki jak ten:

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

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

Pozdrowienia

xlarsx
źródło
7
+1 dla opcji, która nie przekracza zakresu zmiennej „request”, ale można było wykonać rzutowanie zamiast użyć słowa kluczowego „as”. InvalidCastException zostałby wyrzucony zamiast
niejasnego
64

Do tej pory wszyscy się mylili, ponieważ BeginGetResponse()trochę popracuje nad bieżącym wątkiem. Z dokumentacji :

Metoda BeginGetResponse wymaga ukończenia niektórych synchronicznych zadań konfiguracyjnych (na przykład rozpoznawania nazw DNS, wykrywania serwera proxy i połączenia przez gniazdo TCP), zanim ta metoda stanie się asynchroniczna. W rezultacie ta metoda nigdy nie powinna być wywoływana w wątku interfejsu użytkownika (UI), ponieważ ukończenie początkowych zadań synchronicznej konfiguracji może zająć dużo czasu (do kilku minut w zależności od ustawień sieci), zanim zostanie zgłoszony wyjątek dla błędu lub metoda się powiodła.

Aby to zrobić dobrze:

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

Następnie możesz zrobić z odpowiedzią, co musisz. Na przykład:

HttpWebRequest request;
// init your request...then:
DoWithResponse(request, (response) => {
    var body = new StreamReader(response.GetResponseStream()).ReadToEnd();
    Console.Write(body);
});
Isak
źródło
2
Czy nie mógłbyś po prostu wywołać metody GetResponseAsync obiektu HttpWebRequest przy użyciu await (zakładając, że funkcja została ustawiona jako asynchroniczna)? Jestem bardzo nowy w C #, więc może to być kompletny żart…
Brad,
GetResponseAsync wygląda dobrze, ale potrzebujesz .NET 4.5 (obecnie beta).
Isak,
15
Jezus. To jakiś brzydki kod. Dlaczego nie można odczytać kodu asynchronicznego?
John Shedletsky
Dlaczego potrzebujesz request.BeginGetResponse ()? Dlaczego wrapperAction.BeginInvoke () nie wystarczy?
Igor Gatis
2
@Gatis Istnieją dwa poziomy wywołań asynchronicznych - wrapperAction.BeginInvoke () jest pierwszym asynchronicznym wywołaniem wyrażenia lambda, które wywołuje request.BeginGetResponse (), które jest drugim wywołaniem asynchronicznym. Jak zauważa Isak, BeginGetResponse () wymaga pewnej konfiguracji synchronicznej, dlatego opakowuje ją w dodatkowe wywołanie asynchroniczne.
walkingTarget
64

Zdecydowanie najłatwiejszym sposobem jest użycie TaskFactory.FromAsync z TPL . To dosłownie kilka wierszy kodu w połączeniu z nowymi słowami kluczowymi 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);

Jeśli nie możesz użyć kompilatora C # 5, powyższe można wykonać za pomocą metody 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
źródło
Od .NET 4 preferowane jest to podejście TAP. Zobacz podobny przykład z MS - „How to: Wrap EAP Patterns in a Task” ( msdn.microsoft.com/en-us/library/ee622454.aspx )
Alex Klaus
O wiele łatwiej niż innymi sposobami
Don Rolling
8

Skończyło się na korzystaniu z BackgroundWorker, jest on zdecydowanie asynchroniczny, w przeciwieństwie do niektórych z powyższych rozwiązań, obsługuje powrót do wątku GUI za Ciebie i jest bardzo łatwy do zrozumienia.

Obsługa wyjątków jest również bardzo łatwa, ponieważ kończą się one w metodzie RunWorkerCompleted, ale przeczytaj to: Nieobsłużone wyjątki w BackgroundWorker

Użyłem WebClient, ale oczywiście możesz użyć HttpWebRequest.GetResponse, jeśli chcesz.

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
źródło
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
źródło
6

.NET zmienił się od czasu opublikowania wielu z tych odpowiedzi i chciałbym podać bardziej aktualną odpowiedź. Użyj metody asynchronicznej, aby rozpocząć Taskdziałanie w wątku w tle:

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

Aby użyć metody asynchronicznej:

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

Aktualizacja:

To rozwiązanie nie działa w przypadku aplikacji platformy UWP, które używają WebRequest.GetResponseAsync()zamiast WebRequest.GetResponse()i nie wywołuje Dispose()metod w odpowiednich przypadkach. @dragansr ma dobre alternatywne rozwiązanie, które rozwiązuje te problemy.

tronman
źródło
1
Dziękuję Ci ! Próbowałem znaleźć przykład asynchroniczny, wiele przykładów przy użyciu starego podejścia, które jest zbyt złożone.
WDUK
Czy to nie zablokuje wątku dla każdej odpowiedzi? wydaje się trochę inny niż np. docs.microsoft.com/en-us/dotnet/standard/parallel-programming/ ...
Pete Kirkham
@PeteKirkham Żądanie wykonuje wątek działający w tle, a nie wątek interfejsu użytkownika. Celem jest uniknięcie blokowania wątku interfejsu użytkownika. Każda wybrana metoda wysyłania żądania zablokuje wątek wysyłający żądanie. Przykład firmy Microsoft, do którego się odnosisz, próbuje wysłać wiele żądań, ale nadal tworzy zadanie (wątek w tle) dla żądań.
tronman
3
Żeby było jasne, jest to kod w 100% synchroniczny / blokujący. Aby korzystać asynchronicznie, WebRequest.GetResponseAsync()i StreamReader.ReadToEndAync()muszą być stosowane i oczekiwany.
Richard Szalay
4
@tronman Uruchamianie metod blokowania w zadaniu, gdy dostępne są odpowiedniki asynchroniczne, jest wysoce odradzanym anty-wzorcem. Chociaż odblokowuje wątek wywołujący, nie robi nic w przypadku skalowania scenariuszy hostingu w sieci Web, ponieważ po prostu przenosisz pracę do innego wątku, zamiast używać portów zakończenia operacji we / wy, aby osiągnąć asynchroniczność.
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
źródło