So legen Sie mit AFNetworking ein Timeout fest

79

Mein Projekt verwendet AFNetworking.

https://github.com/AFNetworking/AFNetworking

Wie wähle ich das Timeout herunter? Atm ohne Internetverbindung Der Fail-Block wird für ca. 2 Minuten nicht ausgelöst. Warte zu lange ....

Jennas
quelle
2
Ich rate dringend von jeder Lösung ab, die versucht, Zeitüberschreitungsintervalle zu überschreiben, insbesondere von solchen, mit denen performSelector:afterDelay:...vorhandene Vorgänge manuell abgebrochen werden. Bitte sehen Sie meine Antwort für weitere Details.
Mattt

Antworten:

110

Das Ändern des Timeout-Intervalls ist mit ziemlicher Sicherheit nicht die beste Lösung für das von Ihnen beschriebene Problem. Stattdessen scheint es so, als ob der HTTP-Client das Netzwerk nicht mehr erreichen kann, oder?

AFHTTPClienthat bereits einen eingebauten Mechanismus, der Sie informiert, wenn die Internetverbindung unterbrochen wird -setReachabilityStatusChangeBlock:.

Anfragen können in langsamen Netzwerken lange dauern. Es ist besser, iOS zu vertrauen, um zu wissen, wie man mit langsamen Verbindungen umgeht, und den Unterschied zwischen dem und dem Fehlen einer Verbindung zu erkennen.


Um meine Überlegungen zu erweitern, warum andere in diesem Thread erwähnte Ansätze vermieden werden sollten, hier einige Gedanken:

  • Anfragen können storniert werden, bevor sie überhaupt gestartet wurden. Das Einreihen einer Anforderung übernimmt keine Garantie dafür, wann sie tatsächlich gestartet wird.
  • Zeitüberschreitungsintervalle sollten lang laufende Anforderungen - insbesondere POST - nicht abbrechen. Stellen Sie sich vor, Sie möchten ein 100-MB-Video herunterladen oder hochladen. Wenn die Anfrage in einem langsamen 3G-Netzwerk so gut wie möglich läuft, warum sollten Sie sie dann unnötig stoppen, wenn sie etwas länger dauert als erwartet?
  • Dadurch performSelector:afterDelay:...kann in Multi-Threaded - Anwendungen gefährlich sein. Dies öffnet sich für obskure und schwer zu debuggende Rennbedingungen.
mattt
quelle
Ich glaube, das liegt daran, dass die Frage trotz des Titels lautet, wie man in Fällen ohne Internet so schnell wie möglich scheitert. Der richtige Weg, dies zu tun, ist die Erreichbarkeit. Die Verwendung von Zeitüberschreitungen funktioniert entweder nicht oder es besteht eine hohe Wahrscheinlichkeit, dass schwer zu erkennende / zu behebende Fehler auftreten.
Mihai Timar
2
@mattt Obwohl ich Ihrem Ansatz zustimme, habe ich Probleme mit einem Konnektivitätsproblem, bei dem ich wirklich Timeout-Funktionen benötige. Die Sache ist - ich interagiere mit einem Gerät, das einen "WiFi-Hotspot" freigibt. Wenn ich mit diesem "WiFi-Netzwerk" verbunden bin, bin ich in Bezug auf die Erreichbarkeit nicht verbunden, obwohl ich Anforderungen an das Gerät ausführen kann. Andererseits möchte ich feststellen können, ob das Gerät selbst erreichbar ist. Irgendwelche Gedanken? Vielen Dank
Stavash
41
gute Punkte, beantwortet aber nicht die Frage, wie man eine Zeitüberschreitung einstellt
Max MacLeod
3
In der AFNetworking-Referenz heißt es: "Die Erreichbarkeit des Netzwerks ist ein Diagnosetool, mit dem Sie verstehen können, warum eine Anforderung möglicherweise fehlgeschlagen ist. Es sollte nicht verwendet werden, um zu bestimmen, ob eine Anforderung gestellt werden soll oder nicht." im Abschnitt 'setReachabilityStatusChangeBlock'. Ich denke also, 'setReachabilityStatusChangeBlock' ist nicht die Lösung ...
LKM
1
Ich verstehe, warum wir zweimal darüber nachdenken sollten, ein Timeout manuell festzulegen, aber diese akzeptierte Antwort beantwortet die Frage nicht. Meine App muss so schnell wie möglich wissen, wann die Internetverbindung unterbrochen wird. Bei Verwendung von AFNetworking erkennt der ReachabilityManager nicht den Fall, in dem das Gerät mit einem WLAN-Hotspot verbunden ist, aber der Hotspot selbst verliert das Internet (die Erkennung dauert oft Minuten). Daher scheint es in diesem Szenario meine beste Option zu sein, eine Anfrage bei Google mit einer Zeitüberschreitung von ~ 5 Sekunden zu stellen. Es sei denn, ich vermisse etwas?
Tanner Semerad
44

Ich empfehle dringend, sich die Antwort von mattt oben anzuschauen - obwohl diese Antwort nicht den Problemen widerspricht, die er im Allgemeinen erwähnt, ist es für die Frage nach den Originalplakaten viel besser, die Erreichbarkeit zu überprüfen.

Wenn Sie jedoch dennoch eine Zeitüberschreitung festlegen möchten (ohne alle damit verbundenen Probleme performSelector:afterDelay:usw.), beschreibt die von Lego erwähnte Pull-Anforderung eine Möglichkeit, dies als einen der Kommentare zu tun. Sie tun dies einfach:

NSMutableURLRequest *request = [client requestWithMethod:@"GET" path:@"/" parameters:nil];
[request setTimeoutInterval:120];

AFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^{...} failure:^{...}];
[client enqueueHTTPRequestOperation:operation];

Beachten Sie jedoch, dass @KCHarwood erwähnt, dass Apple anscheinend nicht zulässt, dass dies für POST-Anforderungen geändert wird (was in iOS 6 und höher behoben ist).

Wie @ChrisopherPickslay hervorhebt, handelt es sich nicht um eine allgemeine Zeitüberschreitung, sondern um eine Zeitüberschreitung zwischen dem Empfangen (oder Senden von Daten). Mir ist keine Möglichkeit bekannt, eine allgemeine Zeitüberschreitung sinnvoll durchzuführen. In der Apple-Dokumentation zu setTimeoutInterval heißt es:

Das Timeout-Intervall in Sekunden. Wenn die Anforderung während eines Verbindungsversuchs länger als das Zeitlimitintervall inaktiv bleibt, wird davon ausgegangen, dass die Anforderung abgelaufen ist. Das Standardzeitlimit beträgt 60 Sekunden.

JosephH
quelle
Ja, ich habe es ausprobiert und es funktioniert nicht, wenn eine POST-Anfrage gestellt wird.
Borisdiakur
7
Nebenbemerkung: Apple hat dies in iOS 6 behoben
Raptor
2
Dies macht nicht das, was Sie vorschlagen. timeoutIntervalist ein Leerlaufzeitgeber, kein Anforderungszeitlimit. Sie müssten also 120 Sekunden lang überhaupt keine Daten empfangen, damit der oben genannte Code abgelaufen ist. Wenn Daten langsam eingehen, kann die Anforderung unbegrenzt fortgesetzt werden.
Christopher Pickslay
Es ist ein fairer Punkt, dass ich nicht wirklich viel darüber gesagt habe, wie es funktioniert - ich habe diese Informationen hoffentlich jetzt hinzugefügt, danke!
JosephH
26

Sie können das Zeitlimitintervall über die setTimeoutInterval-Methode von requestSerializer festlegen. Sie können den requestSerializer von einer AFHTTPRequestOperationManager-Instanz abrufen.

Zum Beispiel, um eine Post-Anfrage mit einer Zeitüberschreitung von 25 Sekunden durchzuführen:

    NSDictionary *params = @{@"par1": @"value1",
                         @"par2": @"value2"};

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager.requestSerializer setTimeoutInterval:25];  //Time out after 25 seconds

    [manager POST:@"URL" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {

    //Success call back bock
    NSLog(@"Request completed with response: %@", responseObject);


    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
     //Failure callback block. This block may be called due to time out or any other failure reason
    }];
Mostafa Abdellateef
quelle
7

Ich denke, Sie müssen das im Moment manuell patchen.

Ich bin Unterklasse AFHTTPClient und habe die geändert

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters

Methode durch Hinzufügen

[request setTimeoutInterval:10.0];

in AFHTTPClient.m Zeile 236. Natürlich wäre es gut, wenn das konfiguriert werden könnte, aber soweit ich das sehe, ist das momentan nicht möglich.

Cornelius
quelle
7
Eine andere zu berücksichtigende Sache ist, dass Apple das Zeitlimit für einen POST überschreibt. Automatisch ungefähr 4 Minuten, denke ich, und Sie können es nicht ändern.
Charchar
Ich sehe es auch Wieso ist es so? Es ist nicht gut, den Benutzer 4 Minuten warten zu lassen, bevor die Verbindung fehlschlägt.
Slatvick
2
Zum Hinzufügen zur @ KCHarwood-Antwort. Ab iOS 6 überschreibt Apple das Timeout des Beitrags nicht. Dies wurde in iOS 6 behoben.
ADAM
7

Endlich herausgefunden, wie es mit einer asynchronen POST-Anfrage geht:

- (void)timeout:(NSDictionary*)dict {
    NDLog(@"timeout");
    AFHTTPRequestOperation *operation = [dict objectForKey:@"operation"];
    if (operation) {
        [operation cancel];
    }
    [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
    [self perform:[[dict objectForKey:@"selector"] pointerValue] on:[dict objectForKey:@"object"] with:nil];
}

- (void)perform:(SEL)selector on:(id)target with:(id)object {
    if (target && [target respondsToSelector:selector]) {
        [target performSelector:selector withObject:object];
    }
}

- (void)doStuffAndNotifyObject:(id)object withSelector:(SEL)selector {
    // AFHTTPRequestOperation asynchronous with selector                
    NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
                            @"doStuff", @"task",
                            nil];

    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];

    NSMutableURLRequest *request = [httpClient requestWithMethod:@"POST" path:requestURL parameters:params];
    [httpClient release];

    AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:request] autorelease];

    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                          operation, @"operation", 
                          object, @"object", 
                          [NSValue valueWithPointer:selector], @"selector", 
                          nil];
    [self performSelector:@selector(timeout:) withObject:dict afterDelay:timeout];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {            
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:[operation responseString]];
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NDLog(@"fail! \nerror: %@", [error localizedDescription]);
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:nil];
    }];

    NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
    [[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
    [queue addOperation:operation];
}

Ich habe diesen Code getestet, indem ich meinen Server gelassen habe sleep(aFewSeconds).

Wenn Sie eine synchrone POST - Anforderung tun müssen, tun NICHT verwenden [queue waitUntilAllOperationsAreFinished];. Verwenden Sie stattdessen den gleichen Ansatz wie für die asynchrone Anforderung und warten Sie, bis die Funktion ausgelöst wird, die Sie im Selektorargument weitergeben.

borisdiakur
quelle
18
Nein nein nein nein nein, bitte verwenden Sie dies nicht in einer realen Anwendung. Ihr Code bricht die Anforderung tatsächlich nach einem Zeitintervall ab , das beim Erstellen des Vorgangs und nicht beim Starten beginnt . Dies kann dazu führen, dass Anforderungen abgebrochen werden, bevor sie überhaupt gestartet werden.
Mattt
5
@mattt Bitte geben Sie ein Codebeispiel an, das funktioniert. Eigentlich ist das, was Sie beschreiben, genau das, was ich tun möchte: Ich möchte, dass das Zeitintervall genau in dem Moment beginnt, in dem ich die Operation erstelle.
Borisdiakur
Danke Lego! Ich verwende AFNetworking und in ungefähr 10% der Fälle, in denen meine Vorgänge von AFNetworking niemals die Erfolgs- oder Fehlerblöcke aufrufen [Server ist in den USA, Testbenutzer ist in China]. Dies ist ein großes Problem für mich, da ich absichtlich Teile der Benutzeroberfläche blockiere, während diese Anforderungen ausgeführt werden, um zu verhindern, dass der Benutzer zu viele Anforderungen gleichzeitig sendet. Ich habe schließlich eine auf dieser Lösung basierende Version implementiert, die einen Abschlussblock als Parameter über die Leistungsauswahl zurückzieht und sicherstellt, dass der Block ausgeführt wird, wenn! Operation.isFinished - Mattt: Vielen Dank für AFNetworking!
Corey
5

Basierend auf den Antworten anderer und dem Vorschlag von @ mattt zu verwandten Projektproblemen finden Sie hier einen kurzen Überblick, wenn Sie eine Unterklasse bilden AFHTTPClient:

@implementation SomeAPIClient // subclass of AFHTTPClient

// ...

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

@end

Getestet für iOS 6.

Gurpartap Singh
quelle
0

Können wir das nicht mit einem Timer wie diesem machen:

In der .h-Datei

{
NSInteger time;
AFJSONRequestOperation *operation;
}

In der .m-Datei

-(void)AFNetworkingmethod{

    time = 0;

    NSTtimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer:) userInfo:nil repeats:YES];
    [timer fire];


    operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        [self operationDidFinishLoading:JSON];
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
        [self operationDidFailWithError:error];
    }];
    [operation setJSONReadingOptions:NSJSONReadingMutableContainers];
    [operation start];
}

-(void)startTimer:(NSTimer *)someTimer{
    if (time == 15&&![operation isFinished]) {
        time = 0;
        [operation invalidate];
        [operation cancel];
        NSLog(@"Timeout");
        return;
    }
    ++time;
}
Ulaş Sancak
quelle
0

Die Definition des "Timeouts" hat hier zwei verschiedene Bedeutungen.

Timeout wie in timeoutInterval

Sie möchten eine Anforderung löschen, wenn sie länger als ein beliebiges Zeitintervall inaktiv ist (keine Übertragung mehr). Beispiel: timeoutIntervalWenn Sie 10 Sekunden einstellen , starten Sie Ihre Anfrage um 12:00:00 Uhr. Möglicherweise werden einige Daten bis 12:00:23 Uhr übertragen. Die Verbindung wird dann um 12:00:33 Uhr unterbrochen. Dieser Fall wird hier von fast allen Antworten abgedeckt (einschließlich Joseph H., Mostafa Abdellateef, Cornelius und Gurpartap Singh).

Timeout wie in timeoutDeadline

Sie möchten eine Anfrage löschen, wenn sie eine Frist erreicht, die später willkürlich eintritt. Beispiel: deadlineWenn Sie in Zukunft 10 Sekunden festlegen , starten Sie Ihre Anforderung um 12:00:00 Uhr. Möglicherweise wird versucht, einige Daten bis 12:00:23 Uhr zu übertragen. Die Verbindung wird jedoch früher um 12:00:10 Uhr unterbrochen. Dieser Fall wird von borisdiakur abgedeckt.

Ich möchte zeigen, wie diese Frist in Swift (3 und 4) für AFNetworking 3.1 implementiert wird.

let sessionManager = AFHTTPSessionManager(baseURL: baseURL)
let request = sessionManager.post(endPoint, parameters: parameters, progress: { ... }, success: { ... }, failure: { ... })
// timeout deadline at 10 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 10.0) {
    request?.cancel()
}

Und um ein testbares Beispiel zu geben, sollte dieser Code "Fehler" anstelle von "Erfolg" ausgeben, da in Zukunft eine sofortige Zeitüberschreitung von 0,0 Sekunden auftritt:

let sessionManager = AFHTTPSessionManager(baseURL: URL(string: "https://example.com"))
sessionManager.responseSerializer = AFHTTPResponseSerializer()
let request = sessionManager.get("/", parameters: nil, progress: nil, success: { _ in
    print("success")
}, failure: { _ in
    print("failure")
})
// timeout deadline at 0 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0) {
    request?.cancel()
}
Cœur
quelle
-2

Stimmen Sie mit Matt überein, Sie sollten nicht versuchen, das TimeoutInterval zu ändern. Sie sollten sich aber auch nicht auf die Erreichbarkeitsprüfung verlassen, um zu entscheiden, ob Sie die Verbindung herstellen möchten. Sie wissen es erst, wenn Sie es versuchen.

Wie im Apple-Dokument angegeben:

In der Regel sollten Sie keine kurzen Zeitüberschreitungsintervalle verwenden, sondern dem Benutzer eine einfache Möglichkeit bieten, einen Vorgang mit langer Laufzeit abzubrechen. Weitere Informationen finden Sie unter „Entwerfen für reale Netzwerke“.

Schuss
quelle