阅读背景:

从AWS EC2下载到iOS应用程序时出现超时问题

来源:互联网 

I have a custom server written in Swift, using Kitura (https://www.kitura.io), running on an AWS EC2 server (under Ubuntu 16.04). I am securing it using a CA signed SSL certificate (https://letsencrypt.org), so I can use https to connect from the client to the server. The client runs natively under iOS (9.3). I use URLSession's on iOS to connect to the server.

我有一个使用Kitura(https://www.kitura.io)在Swift中编写的自定义服务器,在AWS EC2服务器上运行(在Ubuntu 16.04下)。我使用CA签名的SSL证书(https://letsencrypt.org)保护它,所以我可以使用https从客户端连接到服务器。客户端在iOS(9.3)下本地运行。我在iOS上使用URLSession连接到服务器。

I'm having client timeout problems when I make multiple largish downloads to the iOS client back to back. The timeouts look like:

当我对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}}}

错误域= NSURLErrorDomain代码= -1001“请求超时。” UserInfo = {NSErrorFailingURLStringKey = https://,_kCFStreamErrorCodeKey = -2102,NSErrorFailingURLKey = https://,NSLocalizedDescription =请求超时。,_ kCFStreamErrorDomainKey = 4,NSUnderlyingError = 0x7f9f23d0 {错误域= kCFErrorDomainCFNetwork代码= -1001“(null) “UserInfo = {_ kCFStreamErrorDomainKey = 4,_kCFStreamErrorCodeKey = -2102}}}

On the server, the timeouts always occur at the same place in the code-- and they cause the specific server request thread to block and never recover. The timeouts occur just as the server thread calls the Kitura RouterResponse end method. i.e., the server thread blocks when it calls this end method. In light of this, it is not surprising that the client app times out. This code is open-source, so I'll link to where the server blocks: https://github.com/crspybits/SyncServerII/blob/master/Server/Sources/Server/ServerSetup.swift#L146

在服务器上,超时总是发生在代码中的相同位置 - 并且它们导致特定服务器请求线程阻塞并且永远不会恢复。超时发生就像服务器线程调用Kitura RouterResponse结束方法一样。即,服务器线程在调用此结束方法时阻塞。鉴于此,客户端应用程序超时并不奇怪。这段代码是开源的,所以我将链接到服务器阻塞的位置:https://github.com/crspybits/SyncServerII/blob/master/Server/Sources/Server/ServerSetup.swift#L146

The client-side test that fails is: https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53

失败的客户端测试是:https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53

I am not downloading from something like Amazon S3. The data is is obtained on the server from another web source, and then downloaded to my client via https from the server running on EC2.

我不是从Amazon S3下载的。数据是在服务器上从另一个Web源获取的,然后通过https从EC2上运行的服务器下载到我的客户端。

As an example, it's taking 3-4 seconds to download 1.2 MB of data, and when I try 10 of these 1.2 MB downloads back to back, three of them timeout. The downloads occur using a HTTPS GET request.

例如,下载1.2 MB数据需要3-4秒,当我连续尝试其中10个这样的1.2 MB下载时,其中三个会超时。使用HTTPS GET请求进行下载。

One thing of interest is that the test that does these downloads first does uploads of the same data sizes. I.e., it does 10 uploads at 1.2 MB each. I've seen no timeout failures with those uploads.

有趣的是,首先执行这些下载的测试会上传相同数据大小。即,它每次以1.2 MB上传10次。我发现这些上传没有超时失败。

Most of my requests do work, so this does not appear to be simply a problem with, say, an improperly installed SSL certificate (I've checked that with https://www.sslshopper.com). Nor does it seem to be a problem with improper https setup on the iOS side, where I've got NSAppTransportSecurity setup in my app .plist using Amazon's recommendation (https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/).

我的大多数请求都有效,所以这似乎不是一个问题,例如,一个未正确安装的SSL证书(我已经通过https://www.sslshopper.com检查了它)。 iOS端的不正确的https设置似乎也不是问题,我在我的app .plist中使用亚马逊的推荐设置了NSAppTransportSecurity(https://aws.amazon.com/blogs/mobile/preparing-your -apps换IOS-9 /)。

Thoughts?

思考?

Update1: I just tried this with my server running on a local Ubuntu 16.04 system, and using a self-signed SSL certificate-- other factors remaining the same. I get the same issue coming up. So, it seems clear this does not relate to AWS specifically.

Update1:​​我刚尝试在我的服务器上运行本地Ubuntu 16.04系统,并使用自签名SSL证书 - 其他因素保持不变。我得到了同样的问题。因此,似乎很明显这与AWS无关。

Update2: With the server running on the local Ubuntu 16.04 system, and without using SSL (just a one line change in the server code and the use of http as opposed to https in the client), the issue is not present. The downloads occur successfully. So, it seems clear that this issue does relate to SSL.

Update2:服务器在本地Ubuntu 16.04系统上运行,并且不使用SSL(服务器代码只更改一行,而客户端使用http而不是https),问题就不存在了。下载成功发生。因此,很明显这个问题与SSL有关。

Update3: With the server running on the local Ubuntu 16.04 system, and using the self-signed SSL certificate again, I used a simple curl client. In order to simulate the test I've been using as closely as possible, I interrupted the existing iOS client test just as it was beginning to start its downloads, and restarted using my curl client-- which used the download endpoint on the server to download the same 1.2MB file 20 times. The error did not replicate. My conclusion is that the problem stems from an interaction between the iOS client and SSL.

Update3:当服务器在本地Ubuntu 16.04系统上运行,并再次使用自签名SSL证书时,我使用了一个简单的curl客户端。为了模拟我一直在尽可能使用的测试,我打断了现有的iOS客户端测试,就像它开始下载一样,并使用我的curl客户端重新启动 - 它使用服务器上的下载端点下载相同的1.2MB文件20次。错误没有复制。我的结论是问题源于iOS客户端和SSL之间的交互。

Update4: I now have a simpler version of the iOS client reproducing the issue. I'll copy it in below, but in summary, it uses URLSession's and I see the same timeout issue (the server is running on my local Ubuntu system using the self-signed SSL certificate). When I disable the SSL usage (http and no SSL certificate used on the server), I do not get the issue.

Update4:我现在有一个更简单的iOS客户端版本来重现这个问题。我将在下面复制它,但总的来说,它使用URLSession,我看到相同的超时问题(服务器使用自签名SSL证书在我的本地Ubuntu系统上运行)。当我禁用SSL使用(http并且没有在服务器上使用SSL证书)时,我没有遇到问题。

Here's the simpler 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: "https://127.0.0.1:8181/DownloadFile/?fileUUID=\(fileUUID)&fileVersion=0&masterVersion=\(masterVersion)")!
            Download.session.downloadFrom(url) {
                self.download(count - 1)
            }
        }
    }
}

// In a file named "Download.swift":

//在名为“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: Whew! I'm making progress in the right direction now! I now have a simpler iOS client using SSL/https and not causing this issue. The change was suggested by @Ankit Thakur: I'm now using URLSessionConfiguration.background instead of URLSessionConfiguration.default, and that seems to have been what makes this work. I am not sure why though. Does this represent a bug in URLSessionConfiguration.default? e.g., my app is not explicitly going into the background during my tests. Also: I'm not sure how or if I'm going to be able to use this pattern of code in my client app-- it seems like this usage of URLSession's does not let you change the httpAdditionalHeaders after you create the URLSession. And it appears the intent of the URLSessionConfiguration.background is that the URLSession should live for the duration of the app's lifetime. This is a problem for me because my HTTP headers can change during a single launch of the app.

Update5:哇!我现在正朝着正确的方向前进!我现在有一个更简单的iOS客户端使用SSL / https而不会导致此问题。这个变化是由@Ankit Thakur建议的:我现在使用的是URLSessionConfiguration.background而不是URLSessionConfiguration.default,这似乎是使这个工作的原因。我不知道为什么。这是否代表了URLSessionConfiguration.default中的错误?例如,我的应用程序在测试期间没有明确进入后台。另外:我不确定如何或者如果我能够在我的客户端应用程序中使用这种代码模式 - 似乎这种URLSession的使用不允许您在创建URLSession后更改httpAdditionalHeaders。并且看起来URLSessionConfiguration.background的意图是URLSession应该在应用程序的生命周期内存活。这对我来说是一个问题,因为我的HTTP标头可以在应用程序的单次启动期间发生变化。

Here's is my new Download.swift code. The other code in my simpler example remains the same:

这是我的新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: I see now how to deal with the HTTP header situation. I can just use the allHTTPHeaderFields property of the URLRequest. Situation should be basically solved!

Update6:我现在看到如何处理HTTP头部情况。我可以使用URLRequest的allHTTPHeaderFields属性。情况应该基本解决!

Update7: I may have figured out why the background technique works:

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

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

1 个解决方案

#1


1  

code looks good for client side. Would you try SessionConfiguration to background instead of default. let sessionConfiguration = URLSessionConfiguration.default.

代码看起来很适合客户端。你会尝试SessionConfiguration到背景而不是默认。 let sessionConfiguration = URLSessionConfiguration.default。

There are many scenarios, where I have found .background is working much better than .default. e.g. timeout, GCD support, background download.

有很多场景,我发现.background比.default工作得更好。例如超时,GCD支持,后台下载。

I always prefer to use .background session configuration.

我总是喜欢使用.background会话配置。


分享到: