2017-05-28 6 views
0

Ich habe einen benutzerdefinierten Server in Swift geschrieben, mit Kitura (http://www.kitura.io), auf einem AWS EC2-Server (unter Ubuntu 16.04) ausgeführt. Ich sichere es mit einem CA-signierten SSL-Zertifikat (https://letsencrypt.org), so kann ich https verwenden, um eine Verbindung vom Client zum Server herzustellen. Der Client läuft nativ unter iOS (9.3). Ich verwende URLSessions auf iOS, um eine Verbindung zum Server herzustellen.Timeout Problem beim Herunterladen von AWS EC2 auf iOS App

Ich habe Client-Timeout-Probleme, wenn ich mehrere große Downloads auf den iOS-Client von hinten nach hinten mache. Die Timeouts wie folgt aussehen:

Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSErrorFailingURLStringKey=https://, _kCFStreamErrorCodeKey=-2102, NSErrorFailingURLKey=https://, NSLocalizedDescription=The request timed out., _kCFStreamErrorDomainKey=4, NSUnderlyingError=0x7f9f23d0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102}}}

Auf dem Server immer die Timeouts an der gleichen Stelle in der code-- auftreten und sie bewirken, dass der spezifische Serveranfrage Thread blockieren und sich nie mehr zu erholen. Die Zeitüberschreitungen treten auf, wenn der Server-Thread die Methode Kitura RouterResponseend aufruft. der Server thread blockiert, wenn er diese end Methode aufruft. Vor diesem Hintergrund ist es nicht verwunderlich, dass die Client-App aussetzt. Dieser Code ist Open-Source, so dass ich, wo die Server-Blöcke verbinden werden:

Ich bin nicht das Herunterladen von so etwas wie Amazon S3 https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53: https://github.com/crspybits/SyncServerII/blob/master/Server/Sources/Server/ServerSetup.swift#L146

Die clientseitige Test, ist ausfällt. Die Daten werden auf dem Server von einer anderen Webquelle abgerufen und dann über https von dem Server, der auf EC2 ausgeführt wird, auf meinen Client heruntergeladen.

Zum Beispiel dauert es 3-4 Sekunden, um 1,2 MB Daten herunterzuladen, und wenn ich 10 dieser 1,2 MB Downloads hintereinander versuchen, drei von ihnen Timeout. Die Downloads erfolgen mithilfe einer HTTPS-GET-Anforderung.

Eine Sache von Interesse ist, dass der Test, der diese Downloads zuerst durchführt, Uploads derselben Datengrößen durchführt. Das heißt, es werden 10 Uploads zu jeweils 1,2 MB durchgeführt. Ich habe bei diesen Uploads keine Timeout-Fehler festgestellt.

Die meisten meiner Anfragen tun Arbeit, so scheint dies nicht nur ein Problem mit zu sein, sagen wir, ein nicht ordnungsgemäß installiert SSL-Zertifikat (Ich habe überprüft, dass mit https://www.sslshopper.com). Es scheint auch kein Problem mit unpassenden https-Setup auf der iOS-Seite zu sein, wo ich NSAppTransportSecurity Setup in meiner App .plist mit Amazon-Empfehlung (https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/) eingerichtet habe.

Gedanken?

Update1: Ich habe gerade versucht, dies mit meinem Server auf einem lokalen Ubuntu 16.04 System ausgeführt wird, und mit einem selbstsignierten SSL andere Faktoren gleich bleibenden certificate--. Ich bekomme das gleiche Problem. Also, es scheint klar, dass dies nicht bezieht sich speziell auf AWS.

Update2: mit dem Server auf dem lokalen Ubuntu 16.04 System ausgeführt wird, und ohne die Verwendung von SSL (nur eine einzeiligen Änderung des Server-Code und die Verwendung von http wie im Client https gegen), um das Problem ist nicht vorhanden. Die Downloads sind erfolgreich. Also, es scheint klar, dass dieses Problem betrifft beziehen sich auf SSL.

Update3: mit dem Server auf dem lokalen Ubuntu 16.04 System ausgeführt wird, und mit Hilfe des selbstsignierten SSL-Zertifikat erneut, habe ich ein einfaches curl Client.Um den Test zu simulieren, den ich so genau wie möglich benutzt habe, habe ich den bestehenden iOS-Client-Test unterbrochen, als er mit dem Download begann, und mit meinem curl Client gestartet, der den Download-Endpunkt auf dem Server verwendet hatte Laden Sie die gleiche 1,2 MB-Datei 20 Mal herunter. Der Fehler ist nicht replizieren. Meine Schlussfolgerung ist, dass das Problem von einer Interaktion zwischen dem iOS-Client und SSL herrührt.

Update4: Ich habe jetzt eine einfachere Version des iOS-Clients reproduzieren das Problem. Ich werde es unten kopieren, aber zusammenfassend URLSession 's und ich sehe das gleiche Timeout-Problem (der Server läuft auf meinem lokalen Ubuntu-System mit dem selbstsignierten SSL-Zertifikat). Wenn ich die SSL-Nutzung (http und kein SSL-Zertifikat auf dem Server verwendet) deaktivieren, mache ich nicht bekomme das Problem.

Hier ist der einfachere Client:

class ViewController: UIViewController {   
    override func viewDidAppear(_ animated: Bool) { 
     super.viewDidAppear(animated) 

     download(10) 
    } 

    func download(_ count:Int) { 
     if count > 0 { 
      let masterVersion = 16 
      let fileUUID = "31BFA360-A09A-4FAA-8B5D-1B2F4BFA5F0A" 

      let url = URL(string: "http://127.0.0.1:8181/DownloadFile/?fileUUID=\(fileUUID)&fileVersion=0&masterVersion=\(masterVersion)")! 
      Download.session.downloadFrom(url) { 
       self.download(count - 1) 
      } 
     } 
    } 
} 

// In einer Datei mit dem Namen "Download.swift":

import Foundation 

class Download : NSObject { 
    static let session = Download() 

    var authHeaders:[String:String]! 

    override init() { 
     super.init() 
     authHeaders = [ 
      <snip: HTTP headers specific to my server> 
     ] 
    } 

    func downloadFrom(_ serverURL: URL, completion:@escaping()->()) { 

     let sessionConfiguration = URLSessionConfiguration.default 
     sessionConfiguration.httpAdditionalHeaders = authHeaders 

     let session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil) 

     var request = URLRequest(url: serverURL) 
     request.httpMethod = "GET" 

     print("downloadFrom: serverURL: \(serverURL)") 

     var downloadTask:URLSessionDownloadTask! 

     downloadTask = session.downloadTask(with: request) { (url, urlResponse, error) in 

      print("downloadFrom completed: url: \(String(describing: url)); error: \(String(describing: error)); status: \(String(describing: (urlResponse as? HTTPURLResponse)?.statusCode))") 
      completion() 
     } 

     downloadTask.resume() 
    } 
} 

extension Download : URLSessionDelegate, URLSessionTaskDelegate /*, URLSessionDownloadDelegate */ { 
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { 
     completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 
    } 
} 

Update5: Puh! Ich komme jetzt in die richtige Richtung! Ich habe jetzt einen einfacheren iOS-Client, der SSL/https verwendet und dieses Problem nicht verursacht. Die Änderung wurde von @Ankit Thakur vorgeschlagen: Ich verwende jetzt URLSessionConfiguration.background anstelle von URLSessionConfiguration.default, und das scheint zu sein, was diese Arbeit macht. Ich bin mir nicht sicher warum. Ist das ein Fehler in URLSessionConfiguration.default? Beispielsweise tritt meine App während meiner Tests nicht explizit in den Hintergrund. Außerdem: Ich bin nicht sicher, wie oder ob ich in der Lage sein werde, dieses Code-Muster in meiner Client-App zu verwenden - es scheint, als ob diese Verwendung von URLSession Sie die httpAdditionalHeaders nicht ändern kann, nachdem Sie die URLSession erstellen . Und es scheint die Absicht der URLSessionConfiguration.background ist, dass die URLSession für die Lebensdauer der App leben soll. Dies ist ein Problem für mich, da sich meine HTTP-Header während eines einzigen Starts der App ändern können.

Hier ist mein neuer Download.swift Code. Der andere Code in meinem einfacheren Beispiel bleibt das gleiche:

import Foundation 

class Download : NSObject { 
    static let session = Download() 

    var sessionConfiguration:URLSessionConfiguration! 
    var session:URLSession! 
    var authHeaders:[String:String]! 
    var downloadCompletion:(()->())! 
    var downloadTask:URLSessionDownloadTask! 
    var numberDownloads = 0 

    override init() { 
     super.init() 
     // https://developer.apple.com/reference/foundation/urlsessionconfiguration/1407496-background 
     sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "MyIdentifier") 

     authHeaders = [ 
      <snip: my headers> 
     ] 

     sessionConfiguration.httpAdditionalHeaders = authHeaders 

     session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main) 
    } 

    func downloadFrom(_ serverURL: URL, completion:@escaping()->()) { 
     downloadCompletion = completion 

     var request = URLRequest(url: serverURL) 
     request.httpMethod = "GET" 

     print("downloadFrom: serverURL: \(serverURL)") 

     downloadTask = session.downloadTask(with: request) 

     downloadTask.resume() 
    } 
} 

extension Download : URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate { 
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { 
     completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 
    } 

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 
     print("download completed: location: \(location); status: \(String(describing: (downloadTask.response as? HTTPURLResponse)?.statusCode))") 
     let completion = downloadCompletion 
     downloadCompletion = nil 
     numberDownloads += 1 
     print("numberDownloads: \(numberDownloads)") 
     completion?() 
    } 

    // This gets called even when there was no error 
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 
     print("didCompleteWithError: \(String(describing: error)); status: \(String(describing: (task.response as? HTTPURLResponse)?.statusCode))") 
     print("numberDownloads: \(numberDownloads)") 
    } 
} 

Update6: ich nun sehen, wie mit der HTTP-Header Situation umgehen. Ich kann einfach die allHTTPHeaderFields Eigenschaft der URLRequest verwenden. Die Situation sollte grundsätzlich gelöst sein!

UPDATE7: Vielleicht habe ich herausgefunden, warum die Hintergrund Technik funktioniert:

Any upload or download tasks created by a background session are automatically retried if the original request fails due to a timeout.

https://developer.apple.com/reference/foundation/nsurlsessionconfiguration/1408259-timeoutintervalforrequest

+0

Code sieht gut für Client-Seite aus. Würden Sie SessionConfiguration in den Hintergrund anstatt in den Standardmodus versetzen. 'lass sessionConfiguration = URLSessionConfiguration.default' –

+0

Danke für die Idee. Zumindest in meinem Code erfordert dies eine ganze Menge Umstrukturierung, um dies zu versuchen. Wenn Sie z. B. den Hintergrund verwenden, können Sie keine Schließung mehr verwenden - Sie müssen einen Delegaten verwenden. Die Sache, die mich jetzt stört, nachdem ich eine Weile daran gearbeitet habe, ist - ich bekomme nicht die Antwort-Header, die ich von den Delegate-Methoden erwarte. Das heißt, in 'func urlSession (_ Sitzung: URLSession, Aufgabe: URLSessionTask, didCompleteWithError Fehler: Fehler?)' 'Task.response' enthält nicht die Header, die ich erwarte. –

+0

versuchen Sie, wireshark. Sie können dann den Anforderungskopf und die Antwort in wireshark bestätigen und dann den Code debuggen. Kann sein, einige Anfrage Header ist das Problem, überprüfen Sie Content-Typ oder andere Anfrage Header. –

Antwort

1

Code für Client-Seite gut aussieht. Würden Sie versuchen SessionConfiguration zu background anstelle von default. let sessionConfiguration = URLSessionConfiguration.default.

Es gibt viele Szenarien, in denen ich gefunden habe .background funktioniert viel besser als .default. z.B. Timeout, GCD-Unterstützung, Hintergrund-Download.

Ich bevorzuge immer .background Sitzungskonfiguration zu verwenden.

+0

Nochmals vielen Dank, @AnkitThakur! –