Correct way to send data through a socket with NSOutputStream

后端 未结 2 379
不思量自难忘°
不思量自难忘° 2021-02-03 11:30

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

2条回答
  •  青春惊慌失措
    2021-02-03 12:16

    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;
            }
        }
    }
    

提交回复
热议问题