Correct way to send data through a socket with NSOutputStream

后端 未结 2 358
不思量自难忘°
不思量自难忘° 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:06

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

    0 讨论(0)
  • 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;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题