14

I have this consumer class that takes an NSInputStream as argument which will be processed async, and I want to push data that comes from a producer class that requires that it has an NSOutputStream provided as its output source. Now how could I set up a buffering (or transparent) stream that acts as the output stream for the producer, and at the same time as the NSInputStream for my consumer class?

I've looked a bit at the NSOutputStream +outputStreamToMemory and +outputStreamToBuffer:capacity: but haven't really figured out how to use it as input for an NSInputSource.

I had some idea of setting up a middle-man class that holds the actual buffer, then creating two subclasses (one for each NSInput/OutputStream) which holds a reference to this buffering class, and having these subclasses delegate most calls to that class, e.g output subclass methods hasSpaceAvailable, write:maxLength:, and for the input, hasBytesAvailable, read:maxLength: etc.

Any tips on how to approach this situation are appreciated. Thanks.

cahlbin
  • 1,039
  • 10
  • 17
  • This is a great question, I'm struggling to find an answer to it too! What I've come up so far is use a file as a middle man (which doesn't seem to be very efficient). Hope someone finds a better solution – Nick Jul 20 '10 at 00:00
  • I got some tips in the apple dev forums which pointed me to CFStreamCreateBoundPair. There is some example code for it in the SimpleURLConnections sample project. You could also check out https://devforums.apple.com/message/258868#258868 . I haven't looked into it much more after that though. – cahlbin Jul 20 '10 at 16:12

4 Answers4

11

One way to accomplish this would be to use the example code on the apple developer site. SimpleURLConnection example

This is how to do it, as can be seen in the PostController.m code

@interface NSStream (BoundPairAdditions)
+ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr bufferSize:(NSUInteger)bufferSize;
@end

@implementation NSStream (BoundPairAdditions)

+ (void)createBoundInputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr bufferSize:(NSUInteger)bufferSize
{
    CFReadStreamRef     readStream;
    CFWriteStreamRef    writeStream;

    assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) );

    readStream = NULL;
    writeStream = NULL;

    CFStreamCreateBoundPair(
        NULL, 
        ((inputStreamPtr  != nil) ? &readStream : NULL),
        ((outputStreamPtr != nil) ? &writeStream : NULL), 
        (CFIndex) bufferSize);

    if (inputStreamPtr != NULL) {
        *inputStreamPtr  = [NSMakeCollectable(readStream) autorelease];
    }
    if (outputStreamPtr != NULL) {
        *outputStreamPtr = [NSMakeCollectable(writeStream) autorelease];
    }
}
@end

Basically you attach the ends of two streams together with a buffer.

JugsteR
  • 1,144
  • 8
  • 22
  • Will the handleEvent method for the dataReceived occur when the method is invoked (in the same method stack) or will it be triggered in another event? – Mats Stijlaart Dec 07 '11 at 09:58
  • Do you mean if the handleEvent method will be in the same call stack as the createBoundInputStream? IIRC, no. – JugsteR Dec 08 '11 at 00:41
  • No I meant if the handleEvent method for the inputStream will be on the same call stack as the write method that is called on the outputStream? Or will it be scheduled as new event? – Mats Stijlaart Dec 08 '11 at 12:21
  • They are separate events. One for the producer and one for the consumer. – JugsteR Dec 09 '11 at 07:47
  • Ok, this would make it less suitable to unit test while tests would be dependent on different events though? – Mats Stijlaart Dec 09 '11 at 10:12
  • 1
    I created a pair of streams using this method. While the NSOutputStream gets all the handleEvent delegate callbacks when it gets data pushed into it, I get nothing on my NSInputStream (no delegate callbacks). Any ideas? – elsurudo Jul 16 '13 at 10:10
  • Nevermind. What I neglected to do was first open the input stream, then schedule it on my run-loop. Thanks! – elsurudo Jul 16 '13 at 13:48
  • CFStreamCreateBoundPair is supposed to be safer than trying to subclass NSInputStream and override private APIs[1]. However, AFNetworking had issues with this, so unfortunately, private API overrides are the best solution for now[2]. [1]: osdir.com/ml/general/2012-02/msg08594.html [2]: github.com/AFNetworking/AFNetworking/pull/1044 – Heath Borders Aug 30 '13 at 14:53
  • also see https://github.com/BoydYang/BYRequest/blob/master/BYRequest/MKNetworkKit/NSStream%2BBoundPairAdditions.m – Peter Lapisu Feb 03 '15 at 19:18
  • and also here https://developer.apple.com/library/ios/samplecode/SimpleURLConnections/Listings/PostController_m.html – Peter Lapisu Feb 03 '15 at 20:37
  • @JugsteR I've been having a hell of a time finding the equivalent of "NSInputStream **" in Swift. Pointers are discouraged in Swift, so I'm not sure if there's an alternative? – jrisberg Apr 17 '16 at 02:22
  • In Swift 3 the Stream framework provides the type method: `class func getBoundStreams(withBufferSize bufferSize: Int, inputStream: AutoreleasingUnsafeMutablePointer?, outputStream: AutoreleasingUnsafeMutablePointer?)` – Teo Sartori Sep 26 '16 at 09:15
1

You might want to consider subclassing NSInputStream, and wrapping the source stream in your new class that buffers and/or modifies the bytes as they pass through.

The main reason I found for doing this over the bound sockets approach is to support seeking. File based NSInputStreams use a stream property to seek within the file, and I couldn't easily arrange this without subclassing.

A problem with this approach is that it seems toll-free bridging won't work for you subclass - but there is a very nice article that will also give you a template subclass to start from if you need one:

http://bjhomer.blogspot.co.uk/2011/04/subclassing-nsinputstream.html

I got a buffering solution working using both approaches - although another issue I had with the subclass approach is that you need to take care to send events to listeners appropriately - for example, when your source stream sends you an EOF event, you won't pass it on to your consumer until they have emptied the buffer - so there is some messing about to do there.

Also - you might need to ensure that clients do their reading off the main run loop (I got it working with grand central dispatch) - because any observing you do in your subclass - on the source stream - will clash with the consumer otherwise. Although you appear to be able to pick any run loop to observe streams on, only the main one works.

So overall I'd say go with the paired streams unless you need to support seeking - or are particularly averse to the paired streams method.

Adrian Bigland
  • 1,439
  • 1
  • 13
  • 8
0

Here is an already implemented class that does exactly what you want

BufferOutputStreamToInputStream

// initialize
self.bufferWriter = [[BufferOutputStreamToInputStream alloc] init];
[self.bufferWriter openOutputStream];

// later you want to set the delegate of the inputStream and shedule it in runloop
// remember, you are responsible for the inputStream, the outputStream is taken care off;)
self.bufferWriter.inputStream.delegate = self;
[self.bufferWriter.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.bufferWriter.inputStream open]

// fill with data when desired on some event      
[self.bufferWriter addDataToBuffer:someData];
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179
0

Anyone still using Objecive C, as of iOS 8 this is the canonical way to do it:

NSStream:getBoundStreamWithBufferSize:inputStream:outputStream:

charshep
  • 416
  • 4
  • 14