6

All,

I have a server that has a tcp socket stream for communication. I need to get to that stream and read the initial data that it needs to send me.

My current code is as follows. To be honest, I'm going at this completely blind. I'm not sure if this is correct let alone the right thing for the job.

-(void) initNetworkCommunication
{
    //input stream
    NSInputStream *iStream;
    NSURL *url = [url initWithString:@"192.168.17.1:2004"];

    [iStream initWithURL:url];
    [iStream setDelegate:self];
    [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [iStream open];

}

So from the way I see it, this code initializes the stream, but how do I read from the stream?

Thanks

Baub
  • 5,004
  • 14
  • 56
  • 99
  • 1
    The codeblock doesn't seem to properly allocate the `NSInputStream` object (i.e. there's an `init` but no `alloc`). – newenglander Dec 10 '15 at 20:25

2 Answers2

23

There are two ways to get data from a stream: polling and using stream events.

Polling is simpler, but will block the thread it is running in. If you use this method, you don't need to perform the setDelegate: or scheduleInRunLoop:forMode: calls. Polling is performed by repeatedly calling read:maxLength:.

NSInteger result;
uint8_t buffer[BUFFER_LEN]; // BUFFER_LEN can be any positive integer
while((result = [iStream read:buffer maxLength:BUFFER_LEN]) != 0) {
    if(result > 0) {
        // buffer contains result bytes of data to be handled
    } else {
        // The stream had an error. You can get an NSError object using [iStream streamError]
    }
}
// Either the stream ran out of data or there was an error

Using stream events requires setting the delegate and adding the stream to a run loop. Instead of blocking the thread, the stream will send a stream:handleEvent: message to its delegate when certain events occur, including when it receives data. The delegate can then retrieve the data from the stream. Here is an example stream:handleEvent: method:

- (void)stream:(NSInputStream *)iStream handleEvent:(NSStreamEvent)event {
    BOOL shouldClose = NO;
    switch(event) {
        case  NSStreamEventEndEncountered:
            shouldClose = YES;
            // If all data hasn't been read, fall through to the "has bytes" event
            if(![iStream hasBytesAvailable]) break;
        case NSStreamEventHasBytesAvailable: ; // We need a semicolon here before we can declare local variables
            uint8_t *buffer;
            NSUInteger length;
            BOOL freeBuffer = NO;
            // The stream has data. Try to get its internal buffer instead of creating one
            if(![iStream getBuffer:&buffer length:&length]) {
                // The stream couldn't provide its internal buffer. We have to make one ourselves
                buffer = malloc(BUFFER_LEN * sizeof(uint8_t));
                freeBuffer = YES;
                NSInteger result = [iStream read:buffer maxLength:BUFFER_LEN];
                if(result < 0) {
                    // error copying to buffer
                    break;
                }
                length = result;
            }
            // length bytes of data in buffer
            if(freeBuffer) free(buffer);
            break;
        case NSStreamEventErrorOccurred:
            // some other error
            shouldClose = YES;
            break;
    }
    if(shouldClose) [iStream close];
}
Osman
  • 1,771
  • 4
  • 24
  • 47
ughoavgfhw
  • 39,734
  • 6
  • 101
  • 123
  • This is a valuable summary answer for us beginners, and it's worth editing: As it stands now, with the call to NSInputStream's `read:maxLength:` *inside* the if-stmt, you won't ever read anything if you succeed in getting the stream's internal buffer. But I'm not solid enough on the topic to edit it myself -- could you fix it, please? – Wienke Jan 06 '12 at 04:10
  • P.S. I wonder why NSOutputStream doesn't have an equivalent `getBuffer` method? – Wienke Jan 06 '12 at 04:20
  • 1
    @Wienke In this example, the call to `read:maxLength:` simply copies the data into a temporary buffer created inside the if statement. You only need to do this if you don't already have a buffer with the data in it. Immediately after the if statement, you should add code to do something with the data in the buffer, because the buffer is only temporary. NSOutputStream probably doesn't provide access to an internal buffer because it doesn't allow access to the data you've already written, and if you wrote directly to the buffer, it would have no way of knowing that it should send that data. – ughoavgfhw Jan 06 '12 at 18:09
  • Thanks, I get it now. (I was thinking the internal buffer was just an alternate empty holder, and that you still needed to call `read:maxlength` to put the data into it.) – Wienke Jan 06 '12 at 20:51
  • Shouldn't you also call `removeFromRunLoop` additionally to `close` in the `if (shouldClose)` block? – Luis Nell Nov 21 '13 at 10:56
  • @origiNell It might be good practice, but is not required. Closing the stream implicitly removes it from the run loop if necessary. – ughoavgfhw Nov 21 '13 at 20:03
0

I came up with this, based on some other answers.

public enum StreamError: Error {
    case Error(error: Error?, partialData: [UInt8])
}

extension InputStream {

    public func readData(bufferSize: Int = 1024) throws -> Data {
        var buffer = [UInt8](repeating: 0, count: bufferSize)
        var data: [UInt8] = []

        open()

        while true {
            let count = read(&buffer, maxLength: buffer.capacity)

            guard count >= 0 else {
                close()
                throw StreamError.Error(error: streamError, partialData: data)
            }

            guard count != 0 else {
                close()
                return Data(bytes: data)
            }

            data.append(contentsOf: (buffer.prefix(count)))
        }

    }
}
Mark Bridges
  • 8,228
  • 4
  • 50
  • 65