Тело запроса 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 ответа
Решения, которые я могу поддержать, это:
- используя NSURLSessionStreamTask, от iOS9 и OSX10.11.
- используя вместо этого ASIHTTPRequest.
К несчастью, NSInputStream
это кластер классов. Это затрудняет создание подклассов. И в случае NSInputStream
любые подклассы полностью не поддерживаются и могут потерпеть неудачу захватывающим образом. (Подробнее см. http://blog.bjhomer.com/2011/04/subclassing-nsinputstream.html.)
Вместо подклассов NSInputStream
Вы должны использовать связанную пару потоков и создать свой собственный класс провайдера данных для подачи данных в него. Сделать это:
- Вызов
CFStreamCreateBoundPair
, - Бросить получившийся
CFReadStream
возражать противNSInputStream
указатель. - Брось
CFWriteStream
возражать противNSOutputStream
указатель. - Передайте поток ввода при создании задачи загрузки или объекта запроса.
- Создайте класс, который использует таймер для периодической передачи следующего фрагмента данных в выходной поток.
Если вы сделаете это, данные, которые ваш класс поставщика данных передает NSOutputStream
станет доступным для чтения из NSInputStream
на другом конце.