Correct way to send data through a socket with NSOutputStream
Asked Answered
W

2

10

I am just getting started with socket programming on iOS and I am struggling to determine the use of the NSStreamEventHasSpaceAvailable event for NSOutputStreams.

On the one hand, Apple's official documentation (Listing 2) shows that in the -stream:handleEvent: delegate method, data should be written to the output buffer with -write:maxLength: message, passing data continually from a buffer, whenever the NSStreamEventHasSpaceAvailable event is received.

On the other hand, this tutorial from Ray Wenderlich and this iOS TCP socket example on GitHub ignore the NSStreamEventHasSpaceAvailable event altogether, and just go ahead and -write:maxLength: to the buffer whenever they need to (even ignoring -hasSpaceAvailable).

Thirdly, there is this example code which appears to do both...

My question is therefore, what is the correct way(s) to handle writing data to an NSOutputStream that is attached to a socket? And of what use is the NSStreamEventHasSpaceAvailable event code if it can (apparently) be ignored? It seems to me that there is either very fortunate UB happening (in examples 2 and 3), or there are several ways of sending data through a socket-based NSOutputStream...

Williford answered 7/4, 2014 at 11:30 Comment(0)
E
13

You can write to a stream at any time, but for network streams, -write:maxLength: returns only until at least one byte has been written to the socket write buffer. Therefore, if the socket write buffer is full (e.g. because the other end of the connection does not read the data fast enough), this will block the current thread. If you write from the main thread, this will block the user interface.

The NSStreamEventHasSpaceAvailable event is signalled when you can write to the stream without blocking. Writing only in response to that event avoids that the current thread and possibly the user interface is blocked.

Alternatively, you can write to a network stream from a separate "writer thread".

Econah answered 7/4, 2014 at 11:46 Comment(0)
W
12

After seeing @MartinR's answer, I re-read the Apple Docs and did some reading up on NSRunLoop events. The solution was not as trivial as I first thought and requires some extra buffering.

Conclusions

While the Ray Wenderlich example works, it is not optimal - as noted by @MartinR, if there is no room in the outgoing TCP window, the call to write:maxLength will block. The reason Ray Wenderlich's example does work is because the messages sent are small and infrequent, and given an error-free and large-bandwidth internet connection, it will 'probably' work. When you start dealing with (much) larger amounts of data being sent (much) more frequently however, the write:maxLength: calls could start to block and the App will start to stall...

For the NSStreamEventHasSpaceAvailable event, Apple's documentation has the following advice:

If the delegate receives an NSStreamEventHasSpaceAvailable event and does not write anything to the stream, it does not receive further space-available events from the run loop until the NSOutputStream object receives more bytes. ... ... You can have the delegate set a flag when it doesn’t write to the stream upon receiving an NSStreamEventHasSpaceAvailable event. Later, when your program has more bytes to write, it can check this flag and, if set, write to the output-stream instance directly.

It is therefore only 'guaranteed to be safe' to call write:maxLength: in two scenarios:

  1. Inside the callback (on receipt of the NSStreamEventHasSpaceAvailable event).
  2. Outside the callback if and only if we have already received the NSStreamEventHasSpaceAvailable but elected not to call write:maxLength: inside the callback itself (e.g. we had no data to actually write).

For scenario (2), we will not receive the callback again until write:maxLength is actually called directly - Apple suggest setting a flag inside the delegate callback (see above) to indicate when we are allowed to do this.

My solution was to use an additional level of buffering - adding an NSMutableArray as a data queue. My code for writing data to a socket looks like this (comments and error checking omitted for brevity, the currentDataOffset variable indicates how much of the 'current' NSData object we have sent):

// Public interface for sending data.
- (void)sendData:(NSData *)data {
    [_dataWriteQueue insertObject:data atIndex:0];
    if (flag_canSendDirectly) [self _sendData];
}

// NSStreamDelegate message
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    // ...
    case NSStreamEventHasSpaceAvailable: {
        [self _sendData];
        break;
    }
}

// Private
- (void)_sendData {
    flag_canSendDirectly = NO;
    NSData *data = [_dataWriteQueue lastObject];
    if (data == nil) {
        flag_canSendDirectly = YES;
        return;
    }
    uint8_t *readBytes = (uint8_t *)[data bytes];
    readBytes += currentDataOffset;
    NSUInteger dataLength = [data length];
    NSUInteger lengthOfDataToWrite = (dataLength - currentDataOffset >= 1024) ? 1024 : (dataLength - currentDataOffset);
    NSInteger bytesWritten = [_outputStream write:readBytes maxLength:lengthOfDataToWrite];
    currentDataOffset += bytesWritten;
    if (bytesWritten > 0) {
        self.currentDataOffset += bytesWritten;
        if (self.currentDataOffset == dataLength) {
            [self.dataWriteQueue removeLastObject];
            self.currentDataOffset = 0;
        }
    }
}
Williford answered 11/4, 2014 at 0:55 Comment(6)
Thanks for the insight, ephemera, looks like I had the same level of confusion as you. Thanks again!Sternick
note that bytesWritten may be negative, indicating an error, therefor in case of an error the above code would get stuck, because currentDataOffset wouldnt match... edit suggestedMestee
Could you help me to understand how the receiver know whether it receive just another chunk of data or data as a whole? Perhaps my buffer is of 2 bytes capacity and I can write only two ASCII chars at once. How the receiver tell if I send "do" or "dope" words to it?Rogerrogerio
@purrrminator You have to define a communication protocol that can be parsed correctly at each end. This code is just about sending data, not interpreting what it means.Williford
Awesome! Actually, 1 question resolved 2 problems: 1)Queue realisation for transmitting data 2)Transmitting data itself. Thanks a lot!Eous
I implemented something similar but also used dispatch_sync(self.lockQueueWriteData) when touching the array, in order to avoid any conflicts when trying to access it - otherwise you may get a race condition.Harlanharland

© 2022 - 2024 — McMap. All rights reserved.