2017-05-28 76 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! –