Question

Per Apple’s “Polling Versus Run-Loop Scheduling”:

[hasSpace/BytesAvailable] can mean that there is available bytes or space or that the only way to find out is to attempt a read or a write operation (which could lead to a momentary block).

The doc does not explicitly state that hasSpace/BytesAvailable events behave the same way, only, obscurely, that they have “identical semantics."

Am I to conclude that a write/read streamError or a bytes read/written return of less than the amount expected could be due to a “momentary block”?

If so, should I attempt the transmission again? Should I use some sort of timer mechanism to give the blockage a chance to clear? This would be a lot of work to implement, so I’d rather not if it’s unlikely to help.

(It’s tempting to initiate a limited polling loop in such a case, say a while loop that makes 10 attempts, but I don’t know if it’s safe to do that at the same time as the stream is scheduled in the run loop, and I have no way to test it.)

Was it helpful?

Solution

Here is a good wrapper for sockets: https://github.com/robbiehanson/CocoaAsyncSocket

It will queue reads and writes if the connection is not available. You don't mention if you're using UDP or TCP, however I suspect you're using TCP, in which case it will handle any interruptions on its own -- provided the connection doesn't get torn down.

OTHER TIPS

It’s been a long haul. Here’s some followup on this issue:

Early on, I threw out the idea of maintaining and checking a leftover cache because that would have worked only for the output stream, when further reflection suggested that the input stream could also become blocked.

Instead, I set up idling while-loops:

- (void) stream:(NSStream *)theStream handleEvent:(NSStreamEvent)eventCode {

    switch (eventCode) 
        // RECEIVING
    case NSStreamEventHasBytesAvailable: {
        if (self.receiveStage == kNothingToReceive)
            return;
        // Get the data from the stream. (This method returns NO if bytesRead < 1.)
        if (![self receiveDataViaStream:(NSInputStream *)theStream]) {
            // If nothing was actually read, consider the stream to be idling.
            self.bStreamIn_isIdling = YES;
            // Repeatedly retry read, until (1) the read is successful, or (2) stopNetwork is called, which will clear the idler.
            // (Just in case, add nil stream property as a loop breaker.) 
            while (self.bStreamIn_isIdling && self.streamIn) {
                if ([self receiveDataViaStream:(NSInputStream *)theStream]) {
                    self.bStreamIn_isIdling = NO;
                    // The stream will have started up again; prepare for next event call.
                    [self assessTransmissionStage_uponReadSuccess];
                }
            }
        }
        else
            // Prepare for what happens next.
            [self assessTransmissionStage_uponReadSuccess];
        break;
        // SENDING
    case NSStreamEventHasSpaceAvailable: 
        if (self.sendStage == kNothingToSend)
            return;
        if (![self sendDataViaStream:(NSOutputStream *)theStream]) {
            self.bStreamOut_isIdling = YES;
            while (self.bStreamOut_isIdling && self.streamOut) {
                if ([self sendDataViaStream:(NSOutputStream *)theStream]) {
                    self.bStreamOut_isIdling = NO;
                    [self assessTransmissionStage_uponWriteSuccess];
                }
            }
        }
        else
            [self assessTransmissionStage_uponWriteSuccess]; 
        break;
    // other event cases…

Then it came time to test a user-initiated cancellation, via a “cancel” button. Midway through the sync, there’s a pause on the Cocoa side, awaiting user input. If the user cancels at this point, the Cocoa app closes the streams and removes them from the runloop, so I expected that the streams on the other side of the connection would generate NSStreamEventEndEncountered events, or perhaps NSStreamEventErrorOccurred. But, no, only one event came through, an NSStreamEventHasBytesAvailable! Go figure.

Of course, there weren’t really any “bytes available,” as the stream had been closed on the Cocoa side, not written to — so the stream handler on the iOS side went into an infinite loop. Not so good.

Next I tested what would happen if one of the devices went to sleep. During the pause for user input, I let the iPhone sleep via auto-lock*, and then supplied the user input on the Cocoa side. Surprise again: the Cocoa app continued without perturbation to the end of the sync, and when I woke up the iPhone, the iOS app proved to have completed its side of the sync too.

Could there have been a hiccup on the iPhone side that was fixed by my idle loop? I threw in a stop-network routine to check:

if (![self receiveDataViaStream:(NSInputStream *)theStream])
    [self stopNetwork]; // closes the streams, etc.

The sync still ran through to completion. There was no hiccup.

Finally, I tested what happened if the Mac (the Cocoa side) went to sleep during that pause for input. This produced a sort of backward belch: Two NSStreamEventErrorOccurred events were received — on the Mac side, after which it was no longer possible to write to the output stream. No events at all were received on the iPhone side, but if I tested the iPhone's stream status, it would return 5, NSStreamStatusAtEnd.

CONCLUSIONS & PLAN:

  • The "temporary block" is something of a unicorn. Either the network runs smoothly or it disconnects altogether.
  • If there is truly such a thing as a temporary block, there is no way to distinguish it from a complete disconnection. The only stream-status constants that seem logical for a temporary block are are NSStreamStatusAtEnd and NSStreamStatusError. But per the above experiments, these indicate disconnection.
  • As a result of which I’m discarding the while-loops and am detecting disconnection solely by checking for bytesRead/Written < 1.

*The iPhone won’t ever sleep if it’s slaved to Xcode. You have to run it straight from the iPhone.

You can anticipate "disconnection" whenever you attempt to write 0 bytes to the output stream, or when you receive 0 bytes on the input stream. If you want to keep the streams alive, make sure you check the length of bytes you're writing to the output stream. That way, the input stream never receives 0 bytes, which triggers the event handler for closed streams.

There's no such thing as an "idling" output stream. Only an idling provider of bytes to the output stream, which doesn't need to indicate its idleness.

If you're getting disconnected from your network connection by the sleep timer, you can disable that when you open your streams, and then disable it when you close them:

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    switch(eventCode) {
        case NSStreamEventOpenCompleted:
        {
            [UIApplication sharedApplication].idleTimerDisabled = YES;
            break;
        }

        case NSStreamEventEndEncountered:
        {
            [UIApplication sharedApplication].idleTimerDisabled = NO;
            break;
        }
    }
}

I wouldn't delve any further into the specifics of your situation because I can tell right-off-the-bat that you aren't completely clear on what streams are, exactly. I understand that the documentation on streams is really poor at priming newbies, and is scant, to-boot; but, they model the same streams that have been around for 30 years, so any documentation on streams for any operating system (except Windows) will work perfectly at bringing you up to speed.

By the way, the other, inextricable part of streams is your network connection code, which you did not supply. I suggest that, if you're not already using NSNetService and NSNetServiceBrowser to find peers, connect to them, and acquire your streams accordingly, you should. Doing so allows you to easily monitor the state of your network connection, and quickly and easily reopen your streams should they closed unexpectedly.

I have very thorough, yet easy-to-follow sample code for this, which would require no customization on your end at all to use, if anyone would like it.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top