Как использовать URLSessionStreamTask с URLSession для передачи фрагментированного кодирования

Я пытаюсь подключиться к конечной точке API потоковой передачи Twitter. Похоже, что URLSession поддерживает потоковую передачу через URLSessionStreamTask, однако я не могу понять, как использовать API. Я также не смог найти ни одного примера кода.

Я попытался протестировать следующее, но сетевой трафик не регистрируется:

let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let stream = session.streamTask(withHostName: "https://stream.twitter.com/1.1/statuses/sample.json", port: 22)
stream.startSecureConnection()
stream.readData(ofMinLength: 0, maxLength: 100000, timeout: 60, completionHandler: { (data, bool, error) in
   print("bool = \(bool)")
   print("error = \(String(describing: error))")
})
stream.resume()

Я также реализовал методы делегата (включая URLSessionStreamDelegate), но они не вызываются.

Было бы очень полезно, если бы кто-нибудь опубликовал пример того, как открыть постоянное соединение для chunked ответов от конечной точки потоковой передачи. Кроме того, я ищу решения, в которых не используются сторонние библиотеки. Ответ, аналогичный https://stackoverflow.com/a/9473787/5897233, но обновленный эквивалентом URLSession, был бы идеальным. .

Примечание. Информация об авторизации была исключена из приведенного выше примера кода.


person iOS dev    schedule 17.06.2017    source источник
comment
Вы когда-нибудь решали это? Сам искал часами. Документация действительно скудная.   -  person Ryan    schedule 16.08.2017
comment
@Ryan Только что опубликовал ответ прямо сейчас!   -  person iOS dev    schedule 06.09.2017


Ответы (1)


Получил много информации от Куинна «Эскимо» из Apple.

Увы, здесь у вас не тот конец палки. URLSessionStreamTask предназначен для обработки открытого соединения TCP (или TLS через TCP) без HTTP-фрейминга сверху. Вы можете думать об этом как о высокоуровневом эквиваленте BSD Sockets API.

Кодирование групповой передачи является частью HTTP и, таким образом, поддерживается всеми другими типами задач URLSession (задача данных, задача загрузки, задача загрузки). Вам не нужно делать ничего особенного, чтобы включить это. Кодирование передачи по частям является обязательной частью стандарта HTTP 1.1 и поэтому всегда включено.

Однако у вас есть возможность выбора способа получения возвращаемых данных. Если вы используете удобные API-интерфейсы URLSession (dataTask(with:completionHandler:) и т. д.), URLSession буферизует все входящие данные, а затем передает их обработчику завершения в одном большом значении Data. Это удобно во многих ситуациях, но плохо работает с потоковым ресурсом. В этом случае вам нужно использовать API-интерфейсы на основе делегата URLSession (dataTask(with:) и т. д.), которые будут вызывать метод делегата сеанса urlSession(_:dataTask:didReceive:) с фрагментами данных по мере их поступления.

Что касается конкретной конечной точки, которую я тестировал, было обнаружено следующее: кажется, что сервер разрешает свой потоковый ответ (фрагментированное кодирование передачи), только если клиент отправляет ему потоковый запрос. Это довольно странно и определенно не требуется спецификацией HTTP.

К счастью, можно заставить URLSession отправлять потоковый запрос:

  1. Создайте свою задачу с помощью uploadTask(withStreamedRequest:)

  2. Реализуйте метод делегата urlSession(_:task:needNewBodyStream:) для возврата входного потока, который при чтении возвращает тело запроса.

  3. Выгода!

Я прикрепил тестовый код, который показывает это в действии. В этом случае он использует связанную пару потоков, передавая входной поток запросу (согласно шагу 2 выше) и удерживая выходной поток.

Если вы действительно хотите отправить данные как часть тела запроса, вы можете сделать это, написав в выходной поток.

class NetworkManager : NSObject, URLSessionDataDelegate {

static var shared = NetworkManager()

private var session: URLSession! = nil

override init() {
    super.init()
    let config = URLSessionConfiguration.default
    config.requestCachePolicy = .reloadIgnoringLocalCacheData
    self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
}

private var streamingTask: URLSessionDataTask? = nil

var isStreaming: Bool { return self.streamingTask != nil }

func startStreaming() {
    precondition( !self.isStreaming )

    let url = URL(string: "ENTER STREAMING URL HERE")!
    let request = URLRequest(url: url)
    let task = self.session.uploadTask(withStreamedRequest: request)
    self.streamingTask = task
    task.resume()
}

func stopStreaming() {
    guard let task = self.streamingTask else {
        return
    }
    self.streamingTask = nil
    task.cancel()
    self.closeStream()
}

var outputStream: OutputStream? = nil

private func closeStream() {
    if let stream = self.outputStream {
        stream.close()
        self.outputStream = nil
    }
}

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
    self.closeStream()

    var inStream: InputStream? = nil
    var outStream: OutputStream? = nil
    Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
    self.outputStream = outStream

    completionHandler(inStream)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    NSLog("task data: %@", data as NSData)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    if let error = error as NSError? {
        NSLog("task error: %@ / %d", error.domain, error.code)
    } else {
        NSLog("task complete")
    }
}
}

И вы можете вызывать сетевой код из любого места, например:

class MainViewController : UITableViewController {

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if NetworkManager.shared.isStreaming {  
       NetworkManager.shared.stopStreaming() 
    } else {
       NetworkManager.shared.startStreaming() 
    }
    self.tableView.deselectRow(at: indexPath, animated: true)
}
}

Надеюсь это поможет.

person iOS dev    schedule 06.09.2017
comment
Я только что наткнулся на этот очень полезный ответ, и хотя он отлично работает, когда мое приложение находится на переднем плане, оно не работает, когда я перехожу в фоновый режим. В основном я общаюсь с конечной точкой RESTful, которую я запрашиваю для потока событий. Поток событий открывается и точно передает событие до момента перехода приложения в фоновый режим. Я больше не получаю события, хотя мой поток все еще работает. Я пробовал несколько подходов, связанных с созданием URLSession с фоновой конфигурацией. Есть общие мысли? - person QBit; 02.06.2019
comment
Хотя этот подход отлично работает для меня в iOS 12, он больше не работает в iOS 13. Я получаю сообщение об ошибке: метод GET не должен иметь тела. Но, глядя на объект запроса, поле httpBody равно нулю, что для меня не имеет смысла. Есть предположения? - person QBit; 26.09.2019
comment
У запроса GET никогда не должно быть тела. Используйте методы PUT или POST, чтобы сделать запрос с телом. - person Greg Ball; 30.10.2019
comment
QBit, вам не нужен uploadTask, просто измените его на let task = self.session.dataTask(with: request) и все заработает. - person Dmitry; 15.03.2020