Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to send data through a socket with NSOutputStream

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...

like image 293
Ephemera Avatar asked Apr 07 '14 11:04

Ephemera


2 Answers

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".

like image 78
Martin R Avatar answered Oct 28 '22 20:10

Martin R


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;
        }
    }
}
like image 35
Ephemera Avatar answered Oct 28 '22 21:10

Ephemera