Тело запроса NSURLSession, переданное медленным NSInputStream (управление пропускной способностью)

Привет на основе этого ответа я написал подкласс NSInputStream и это работает довольно хорошо.

Теперь выяснилось, что у меня есть сценарий, в котором я передаю серверу большой объем данных и для предотвращения перебоев в работе других служб мне необходим контроль скорости подачи данных. Поэтому я улучшил функциональность моего подкласса с помощью следующих условий:

  • когда данные должны быть отложены, hasBytesAvailable возвращается NO и попытки чтения заканчиваются чтением нулевого байта
  • когда данные могут быть отправлены, - read:maxLength: позволяет считывать некоторое максимальное количество данных одновременно (по умолчанию 2048).
  • когда - read:maxLength: возвращает нулевые прочитанные байты, вычисляется необходимая задержка и после этой задержки NSStreamEventHasBytesAvailable Событие опубликовано.

Вот интересные части кода (он смешан с C++):

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
    if (![self isOpen]) {
        return kOperationFailedReturnCode;
    }
    int delay =  0;
    NSInteger readCount = (NSInteger)self.cppInputStream->Read(buffer, len, delay);
    if (readCount<0) {
        return kOperationFailedReturnCode;
    }
    LOGD("Stream") << __PRETTY_FUNCTION__
            << " len: " << len
            << " readCount: "<< readCount
            << " time: " << (int)(-[openDate timeIntervalSinceNow]*1000)
            << " delay: " << delay;

    if (!self.cppInputStream->IsEOF()) {
        if (delay==0)
        {
            [self enqueueEvent: NSStreamEventHasBytesAvailable];
        } else {
            NSTimer *timer = [NSTimer timerWithTimeInterval: delay*0.001
                                                     target: self
                                                   selector: @selector(notifyBytesAvailable:)
                                                   userInfo: nil
                                                    repeats: NO];

            [self enumerateRunLoopsUsingBlock:^(CFRunLoopRef runLoop) {
                CFRunLoopAddTimer(runLoop, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
            }];
        }
    } else {
        [self setStatus: NSStreamStatusAtEnd];
        [self enqueueEvent: NSStreamEventEndEncountered];
    }

    return readCount;
}

- (void)notifyBytesAvailable: (NSTimer *)timer {
    LOGD("Stream") << __PRETTY_FUNCTION__ << "notifyBytesAvailable time: " << (int)(-[openDate timeIntervalSinceNow]*1000);

    [self enqueueEvent: NSStreamEventHasBytesAvailable];
}

- (BOOL)hasBytesAvailable {
    bool result = self.cppInputStream->HasBytesAvaible();
    LOGD("Stream") << __PRETTY_FUNCTION__ << ": " << result << " time: " << (int)(-[openDate timeIntervalSinceNow]*1000);
    return result;
}

Я написал тест для этого, и это сработало.

Проблема появилась, когда я использовал этот поток с NSURLSession как источник тела HTTP-запроса. Из журналов видно, что NSURLSession пытается прочитать все сразу. При первом прочтении я возвращаю ограниченную часть данных. Сразу после этого NSURLSession спрашивает, есть ли доступные байты (я возвращаю NO). Через некоторое время (например, 170 мс) я отправляю уведомление, что байты теперь доступны, но NSURLSession не отвечает на это и не вызывает любой метод моего потокового класса.

Вот что я вижу в логах (при запуске какого-то теста):

09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper open]
09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper hasBytesAvailable]: 1 time: 0
09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper read:maxLength:] len: 32768 readCount: 2048 time: 0 delay: 170
09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper hasBytesAvailable]: 0 time: 0
09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper hasBytesAvailable]: 0 time: 0
09:32:14990[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper hasBytesAvailable]: 0 time: 0
09:32:15161[0x7000002a0000] D/Stream: -[CSCoreFoundationCppInputStreamWrapper notifyBytesAvailable:]notifyBytesAvailable time: 171

Где время - количество миллисекунд с момента открытия потока.

Выглядит выглядит NSURLSession не может обрабатывать входные потоки с ограниченной скоростью передачи данных. У кого-нибудь еще была подобная проблема? Или есть альтернативная концепция, как добиться управления полосой пропускания на NSURLSession?

2 ответа

Решения, которые я могу поддержать, это:

  1. используя NSURLSessionStreamTask, от iOS9 и OSX10.11.
  2. используя вместо этого ASIHTTPRequest.

К несчастью, NSInputStream это кластер классов. Это затрудняет создание подклассов. И в случае NSInputStreamлюбые подклассы полностью не поддерживаются и могут потерпеть неудачу захватывающим образом. (Подробнее см. http://blog.bjhomer.com/2011/04/subclassing-nsinputstream.html.)

Вместо подклассов NSInputStreamВы должны использовать связанную пару потоков и создать свой собственный класс провайдера данных для подачи данных в него. Сделать это:

  • Вызов CFStreamCreateBoundPair,
  • Бросить получившийся CFReadStream возражать против NSInputStream указатель.
  • Брось CFWriteStream возражать против NSOutputStream указатель.
  • Передайте поток ввода при создании задачи загрузки или объекта запроса.
  • Создайте класс, который использует таймер для периодической передачи следующего фрагмента данных в выходной поток.

Если вы сделаете это, данные, которые ваш класс поставщика данных передает NSOutputStream станет доступным для чтения из NSInputStream на другом конце.

Другие вопросы по тегам