我有一个使用Kitura(http://www.kitura.io)在Swift中编写的自定义服务器,在AWS EC2服务器上运行(在Ubuntu 16.04下)。我使用CA签名的SSL证书(https://letsencrypt.org)保护它,因此我可以使用https从客户端连接到服务器。客户端在iOS(9.3)下本地运行。我在iOS上使用URLSession来连接服务器。
当我对iOS客户端进行多次大量下载时,我遇到了客户端超时问题。超时看起来像:
错误域= NSURLErrorDomain代码= -1001"请求超时。" 的UserInfo = {NSErrorFailingURLStringKey = https://开头, _kCFStreamErrorCodeKey = -2102,NSErrorFailingURLKey = https://,NSLocalizedDescription =请求超时。, _kCFStreamErrorDomainKey = 4,NSUnderlyingError = 0x7f9f23d0 {错误域= kCFErrorDomainCFNetwork代码= -1001"(null)" UserInfo = {_ kCFStreamErrorDomainKey = 4,_kCFStreamErrorCodeKey = -2102}}}
在服务器上,超时总是发生在代码中的同一位置 - 它们会导致特定的服务器请求线程阻塞并永不恢复。超时发生就像服务器线程调用Kitura RouterResponse
end
方法一样。即,服务器线程在调用此end
方法时阻塞。鉴于此,客户端应用程序超时并不奇怪。此代码是开源的,因此我将链接到服务器阻止的位置: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
我没有从Amazon S3下载。数据是在服务器上从另一个Web源获取的,然后通过https从EC2上运行的服务器下载到我的客户端。
例如,它需要3-4秒才能下载1.2 MB的数据,当我尝试其中10个这样的1.2 MB数据库背靠背时,其中三个会超时。使用HTTPS GET请求进行下载。
有趣的是,首先执行这些下载的测试会上传相同数据大小的内容。即,它每次以1.2 MB上传10次。我发现这些上传没有超时失败。
我的大多数请求都有效,所以这似乎不是一个问题,例如,一个未正确安装的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证书 - 其他因素保持不变。我得到了同样的问题。因此,似乎很明显,不与AWS具体相关。
UPDATE2: 服务器在本地Ubuntu 16.04系统上运行,并且不使用SSL(服务器代码只需更改一行,而客户端使用http而不是https),问题是不当下。下载成功发生。因此,很明显此问题确实与SSL相关。
UPDATE3:
当服务器在本地Ubuntu 16.04系统上运行,并再次使用自签名SSL证书时,我使用了一个简单的curl
客户端。为了模拟我尽可能密切使用的测试,我打断了现有的iOS客户端测试,就像它开始下载一样,并使用我的curl
客户端重启了在服务器上下载端点,下载相同的1.2MB文件20次。该错误不复制。我的结论是问题源于iOS客户端和SSL之间的交互。
UPDATE4:
我现在有一个更简单的iOS客户端版本来重现这个问题。我将在下面复制它,但总的来说,它使用URLSession
并且我看到相同的超时问题(服务器使用自签名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: 我可能已经弄明白为什么背景技术有效:
后台会话创建的任何上传或下载任务都是 如果原始请求因超时而失败,则自动重试。
答案 0 :(得分:1)
代码看起来很适合客户端。您会尝试SessionConfiguration
到background
而不是default
。 let sessionConfiguration = URLSessionConfiguration.default
。
在很多情况下,我发现.background
比.default
工作得更好。
例如超时,GCD支持,后台下载。
我总是喜欢使用.background
会话配置。