1

Hi based on this answer I wrote subclass of NSInputStream and it works pretty well.

Now It turned out that I have scenario where I'm feeding to server large amount of data and to prevent starvation of other services I need control speed of feeding data. So I improved functinality of my subclass with following conditions:

  • when data should be postponed, hasBytesAvailable returns NO and reading attempts ends with zero bytes read
  • when data can be send, - read:maxLength: allows to read some maximum amount data at once (by default 2048).
  • when - read:maxLength: returns zero bytes read, needed delay is calculated and after that delay NSStreamEventHasBytesAvailable event is posted.

Here is interesting parts of code (it is mixed with 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;
}

I wrote some test for that and it worked.

Problem appeared when I used this stream with NSURLSession as source of body of HTTP request. From logs I can see that NSURLSession tries to read everything at once. On first read I return limited portion of data. Immediately after that NSURLSession asks if there are bytes available (I return NO). After some time (for example 170 ms), I'm sending notification that bytes are now available but NSURLSession doesn't respond to that and do not invoke any method of my stream class.

Here is what I see in logs (when running some test):

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

Where time is amount of milliseconds since stream has been opened.

Looks looks NSURLSession is unable to handle input streams with limited data rate. Does anyone else had similar problem? Or has alternative concept how to achieve bandwidth management on NSURLSession?

Community
  • 1
  • 1
Marek R
  • 32,568
  • 6
  • 55
  • 140

2 Answers2

1

solutions tha I can support is:

  1. using NSURLSessionStreamTask, from iOS9 and OSX10.11.
  2. using ASIHTTPRequest instead.
Alex Lee
  • 21
  • 4
  • `ASIHTTPRequest` is some solution (it supports throttling), but my project is to far in progress, that is is already to late to change used framework now. Also this `ASIHTTPRequest` looks like abandoned project and manageress doesn't like this. – Marek R Nov 13 '16 at 10:47
0

Unfortunately, NSInputStream is a class cluster. That makes subclassing hard. And in the case of NSInputStream, any subclasses are completely unsupported and are likely to fail in fascinating ways. (See http://blog.bjhomer.com/2011/04/subclassing-nsinputstream.html for details.)

Instead of subclassing NSInputStream, you should use a bound pair of streams and create your own data provider class to feed data into it. To do this:

  • Call CFStreamCreateBoundPair.
  • Cast the resulting CFReadStream object to an NSInputStream pointer.
  • Cast the CFWriteStream object to an NSOutputStream pointer.
  • Pass the input stream when you create the upload task or request object.
  • Create a class that uses a timer to periodically pass the next chunk of data to the output stream.

If you do this, the data your data provider class passes to the NSOutputStream will become available for reading from the NSInputStream on the other end.

dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • I already have working subclass of `NSOutputStream` (which owns socket stream) which works perfectly SocketRocket and provides throttling. Same solution for `NSInputStream` doesn't work for `NSURLSession`. If I remember correctly (question is quite old) I've already tried use pair of bounded streams with same result. – Marek R Nov 13 '16 at 10:41