#ios #swift #networking #nsurl #urlsession
Вопрос:
У меня есть задача загрузки, которая выполняется путем первого вызова REST API, для которой серверу необходимо сгенерировать довольно большой файл, создание которого занимает несколько минут, так как он требует много ресурсов процессора и ввода-вывода на диск. Клиент ожидает, что сервер выдаст ответ JSON с URL — адресом созданного им файла. Загрузка файла начнется после получения первого результата.
Для вызовов, которые генерируют особенно большой файл, из-за чего сервер очень медленно отвечает, я вижу повторяющиеся запросы, которые мой код не инициирует.
Сначала кто-то, кто работает на стороне сервера, рассказал мне о повторяющихся запросах. Затем я настроил способ проверки сетевого трафика. Это было сделано путем настройки компьютера Mac, подключенного к проводной сети, и включения общего доступа к сети, а также с помощью Proxyman для проверки трафика с iPhone на сервер API. Я вижу несколько экземпляров одного и того же запроса API на сетевом уровне, но мой код никогда не был уведомлен.
Код выглядит так
@objc class OfflineMapDownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
@objc func download(){
let config = URLSessionConfiguration.background(withIdentifier: "OfflineMapDownloadSession")
config.timeoutIntervalForRequest = 500
config.shouldUseExtendedBackgroundIdleMode = true
config.sessionSendsLaunchEvents = true
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
getMapUrlsFromServer(bounds)
}
func getMapUrlsFromServer(){
var urlString = "http://www.fake.com/DoMakeMap.php"
if let url = URL(string: urlString) {
let request = NSMutableURLRequest(url: url)
//...Real code sets up a JSON body in to params...
request.httpBody = params.data(using: .utf8 )
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.timeoutInterval = 500
urlSession?.configuration.timeoutIntervalForRequest = 500
urlSession?.configuration.timeoutIntervalForResource = 500
request.httpShouldUsePipelining = true
let backgroundTask = urlSession?.downloadTask(with: request as URLRequest)
backgroundTask?.countOfBytesClientExpectsToSend = Int64(params.lengthOfBytes(using: .utf8))
backgroundTask?.countOfBytesClientExpectsToReceive = 1000
backgroundTask?.taskDescription = "Map Url Download"
backgroundTask?.resume()
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if (downloadTask.taskDescription == "CTM1 Url Download") {
do {
let data = try Data(contentsOf: location, options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
if let ctm1Url = jsonResult["CTM1Url"] as? String {
if let filesize = jsonResult["filesize"] as? Int {
currentDownload?.ctm1Url = URL(string: ctm1Url)
currentDownload?.ctm1FileSize = Int32(filesize)
if (Int32(filesize) == 0) {
postDownloadFailed()
} else {
startCtm1FileDownload(ctm1Url,filesize)
}
}
}
}
} catch {
postDownloadFailed()
}
}
}
В этом классе загрузки есть еще кое-что, так как он загрузит фактический файл после выполнения первого вызова api. Поскольку проблема возникает до того, как этот код будет выполнен, я не включил его в пример кода.
Журнал от Proxyman показывает, что вызов API вышел в (минуты:секунды) 46:06, 47:13, 48:21, 49:30, 50:44, 52:06, 53:45 Похоже, что запрос повторяется с интервалами чуть более 1 минуты.
Существует поле API, в которое я могу ввести любое значение, и оно будет возвращено мне сервером. Я поставил там метку времени, сгенерированную с помощью CACurrentMediaTime (), и вход в систему Proxyman показывает, что это действительно один и тот же вызов API, поэтому мой код не может вызываться несколько раз. Похоже, что сетевой уровень iOS повторно выдает http-запрос, потому что серверу требуется очень много времени для ответа. Это приводит к возникновению проблем на сервере и сбою API.
Любая помощь была бы очень признательна.
Комментарии:
1. Может быть, httpShouldUsePipelining должен быть ложным? У вас есть 7 ответов, и первый отличается размером ответа. От Apple : «Запросы на публикацию не будут отправляться по конвейеру». Если в этом есть смысл, пожалуйста, скажите мне, и я опубликую ответ
2. Я попытался установить значение httpShouldUsePipelining равным false как для конфигурации сеанса URL, так и для запроса, но все равно увидел, что запрос дублируется.
3. Просто любопытно, сгенерирован ли этот действительно большой файл, отправлен ли он обратно? Или вы просто ищете ответ, который позволит вам узнать, что файл сгенерирован? Спрашиваю, потому что размер ответа на самом деле довольно медленный. По крайней мере, правильные ли ответы?
4. Файл довольно большой, на самом деле не такой большой, может быть 25 МБ. Серверу необходимо проанализировать открытые данные карты улиц и сгенерировать файл, который телефонное приложение может использовать для маршрутизации. Серверу требуется время для анализа данных и создания файла. При быстром подключении Wi-Fi серверу требуется в 3 или 4 раза больше времени для создания файла, чем для загрузки. Один из вариантов, который я рассматриваю, — это оптимизация сервера для более быстрого создания файла.
Ответ №1:
Это очень похоже на повторную передачу TCP. Если клиент отправляет сегмент TCP, а сервер не подтверждает получение в течение короткого промежутка времени, клиент предполагает, что сегмент не попал в пункт назначения, и отправляет сегмент снова. Это механизм значительно более низкого уровня, чем URLSession.
Возможно, приложение HTTP-сервера, которое использует этот API (например, Apache, IIS, LigHTTPd, nginx и т. Д.), Настроено для подтверждения данных ответа, Чтобы сэкономить накладные расходы на упаковку и кадрирование. Если это так, и если данные ответа занимают больше времени, чем тайм-аут повторной передачи TCP клиента, вы получите такое поведение.
У вас есть захват пакетов соединения? Если нет, попробуйте собрать его с помощью tcpdump и просмотреть в Wireshark. Если я прав, вы увидите несколько запросов, и все они будут иметь один и тот же порядковый номер.
Что касается того, как это исправить, если в этом проблема, я не уверен. Сервер должен подтверждать запросы, как только они будут получены.
Комментарии:
1. Я бы присудил награду за оба ответа, но переполнение стека допускает только один.
2. Ничего особенного. Однако мне действительно любопытно, что показывает захват пакетов. Проследите, если вы запустите один из них?
Ответ №2:
Я думаю, что проблема заключается в использовании URLSessionConfiguration.background(с идентификатором:) для этого вызова api.
Используйте этот метод для инициализации объекта конфигурации, подходящего для передачи файлов данных, пока приложение работает в фоновом режиме. Сеанс, настроенный с помощью этого объекта, передает управление передачей системе, которая обрабатывает передачу в отдельном процессе. В iOS эта конфигурация позволяет продолжать передачу, даже если само приложение приостановлено или прекращено.
Таким образом, проблема в том, что система повторяет ваш запрос без необходимости из-за неправильного использования API.
Вот что я рекомендую —
- Используйте конфигурацию сеанса по умолчанию (НЕ фоновую).
- Выполните этот вызов api, который инициирует это длительное задание, не заставляйте клиента ждать этого задания, со стороны сервера верните клиенту идентификатор задания, как только это задание будет инициировано.
- Теперь клиент может опрашивать сервер каждые X секунд, используя значение job_id, чтобы узнать о состоянии задания, и даже при необходимости может показывать прогресс на стороне клиента.
- Когда задание будет завершено и клиент опросит в следующий раз, он получит URL-адрес для загрузки этого большого файла.
- Загрузите файл (используя конфигурацию сеанса по умолчанию / фонового сеанса, как вы предпочитаете).
Комментарии:
1. Это один из возможных обходных путей. Проблема с этим решением заключается в том, что конечный пользователь ожидает, что он откроет приложение, начнет загрузку, а затем закроет приложение, пока они ждут. Это не будет работать без конфигурации фонового сеанса.