2017-05-28 78 views
0

我使用在AWS EC2服務器(在Ubuntu 16.04下)運行的Kitura(http://www.kitura.io)在Swift中編寫了自定義服務器。我使用CA簽名的SSL證書(https://letsencrypt.org)來保護它,所以我可以使用https從客戶端連接到服務器。客戶端在iOS(9.3)下本機運行。我使用iOS上的URLSession來連接到服務器。從AWS EC2下載到iOS應用程序時發生超時

當我向iOS客戶端進行多次大量下載時,我遇到了客戶端超時問題。超時看起來像:

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

在服務器上,超時總是發生在代碼 - 同一個地方,他們會導致特定服務器請求線程被阻塞,永不恢復。超時發生就像服務器線程調用Kitura RouterResponseend方法一樣。即服務器線程在調用該方法時阻塞。鑑於此,客戶端應用超時並不奇怪。此代碼是開源的,所以我會鏈接到服務器塊:https://github.com/crspybits/SyncServerII/blob/master/Server/Sources/Server/ServerSetup.swift#L146

客戶端測試失敗是:https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53

我不是從像亞馬遜S3下載。該數據是從另一個Web源服務器獲得的,然後通過https從運行在EC2上的服務器下載到我的客戶端。

作爲一個例子,下載1.2 MB的數據需要3-4秒,而當我嘗試連接這些1.2 MB下載中的10個時,其中三個超時。使用HTTPS GET請求進行下載。

有趣的一件事是,做這些下載的測試首先會上傳相同的數據大小。也就是說,它每個上傳1.2 MB。這些上傳我沒有看到超時失敗。

我請求的大多數做工作,所以這並不似乎是簡單地用,比方說一個問題,安裝不當的SSL證書(我檢查與https://www.sslshopper.com)。在iOS端不正確的https設置似乎也沒有問題,我使用亞馬遜的建議(https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/)在我的應用.plist中設置了NSAppTransportSecurity

想法?

UPDATE1: 我只是想這與我的服務器本地的Ubuntu 16.04系統上運行,並使用自簽名的SSL certificate--保持不變等因素的影響。我遇到了同樣的問題。所以,看起來很清楚,不是與AWS有關。

UPDATE2: 與服務器的本地的Ubuntu 16.04系統上運行,並且不使用SSL(只是在服務器代碼中的一條線的變化和使用HTTP作爲客戶端而不是HTTPS),發行是不是禮物。下載成功發生。所以,似乎很清楚,這個問題確實與有關。

UPDATE3: 與服務器的本地的Ubuntu 16.04系統上運行,並再次使用自簽名的SSL證書,我用了一個簡單的客戶端curl。爲了模擬我一直儘可能使用的測試,我正在開始下載時中斷了現有的iOS客戶端測試,並使用我的curl客戶端重新啓動 - 它使用服務器上的下載端點來測試下載相同的1.2MB文件20次。錯誤確實不是重複。我的結論是,問題源於iOS客戶端和SSL之間的交互。

Update4: 我現在有一個更簡單的iOS客戶端版本來重現問題。我將在下面複製它,但總之,它使用URLSession's,我看到相同的超時問題(服務器正在使用自簽名SSL證書在本地Ubuntu系統上運行)。當我禁用SSL使用(服務器上使用http和沒有使用SSL證書),我做不是得到問題。

下面是簡單的客戶端:

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

//在一個名爲 「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: 呼!我現在正朝着正確的方向前進!我現在有一個簡單的iOS客戶端使用SSL/HTTPS,並沒有引起這個問題。更改是由@Ankit Thakur提出的:我現在使用URLSessionConfiguration.background而不是URLSessionConfiguration.default,而這似乎是使這項工作的原因。我不知道爲什麼。這是否代表URLSessionConfiguration.default中的錯誤?例如,我的應用在我的測試過程中沒有明確地進入背景。另外:我不知道如何或如果我能夠在我的客戶端應用程序中使用這種模式的代碼 - 看起來這種URLSession的用法不會讓您在創建URLSession之後更改httpAdditionalHeaders 。而且看起來URLSessionConfiguration.background的意圖是URLSession應該在應用程序的整個生存期內生存。這對我來說是一個問題,因爲我的HTTP頭可以在應用程序的單次啓動過程中更改。

這是我的新的Download.swift代碼。在我簡單的例子的其他代碼保持不變:

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: 我現在看到如何處理HTTP標頭的情況。我只能使用URLRequest的allHTTPHeaderFields屬性。情況應該基本解決!

Update7: 我可能已經想通了,爲什麼後臺技術的原理:

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

代碼對於客戶端來說看起來不錯。你會嘗試SessionConfiguration到後臺而不是默認。 '讓sessionConfiguration = URLSessionConfiguration.default' –

+0

感謝您的想法。至少在我的代碼中,這需要進行相當多的重組,甚至可以嘗試。例如,當你切換到使用背景時,你不能再使用閉包 - 你必須使用委託。現在讓我陷入困境的是,在處理了一段時間後 - 我沒有得到我期望在委託方法中的響應頭文件。也就是說,在'func urlSession(_ session:URLSession,任務:URLSessionTask,didCompleteWithError錯誤:錯誤?)''task.response'不包含我期望的頭文件。 –

+0

嘗試使用wireshark。然後您可以在wireshark中確認請求頭和響應,然後調試代碼。可能是一些請求頭是問題,請檢查內容類型或其他請求頭。 –

回答

1

代碼看起來不錯的客戶端。你會嘗試SessionConfigurationbackground而不是defaultlet sessionConfiguration = URLSessionConfiguration.default

有很多情況下,我發現.background.default好得多。 例如超時,GCD支持,後臺下載。

我總是更喜歡使用.background會話配置。

+0

再次感謝@AnkitThakur! –