使用NSOutputStream通过套接字发送数据的正确方法

Eph*_*era 10 sockets cocoa-touch ios

我刚刚开始使用iOS上的套接字编程,我正在努力确定NSStreamEventHasSpaceAvailable事件的使用NSOutputStreams.

一方面,Apple的官方文档(清单2)显示,在-stream:handleEvent:委托方法中,应该使用-write:maxLength:消息将数据写入输出缓冲区,并在NSStreamEventHasSpaceAvailable收到事件时不断地从缓冲区传递数据.

另一方面,Ray Wenderlich的这个教程GitHub上的这个iOS TCP套接字示例NSStreamEventHasSpaceAvailable完全忽略了这个事件,-write:maxLength:只要他们需要(甚至忽略-hasSpaceAvailable)就可以继续前进到缓冲区.

第三,这个示例代码似乎同时做到 ......

因此,我的问题是,处理将数据写入NSOutputStream附加到套接字的正确方法是什么?NSStreamEventHasSpaceAvailable如果事件代码可以(显然)被忽略,它的用途是什么?在我看来,有非常幸运的UB发生(在示例2和3中),或者有几种方式通过基于套接字的方式发送数据NSOutputStream...

Mar*_*n R 13

您可以随时写入流,但对于网络流,-write:maxLength:只返回至少一个字节已写入套接字写入缓冲区.因此,如果套接字写缓冲区已满(例如,因为连接的另一端没有足够快地读取数据),这将阻止当前线程.如果从主线程写入,则会阻止用户界面.

NSStreamEventHasSpaceAvailable时,你可以写到流而不阻塞事件通知.仅响应该事件而写入可避免阻止当前线程和可能的用户界面.

或者,您可以从单独的"编写器线程"写入网络流.


Eph*_*era 12

在看到@ MartinR的回答之后,我重新阅读了Apple Docs,并对NSRunLoop事件进行了一些阅读.解决方案并不像我最初想的那样微不足道,需要一些额外的缓冲.

结论

虽然Ray Wenderlich示例有效,但它并不是最优的 - 正如@MartinR所指出的,如果传出的TCP窗口中没有空间,write:maxLength则会阻止调用.Ray Wenderlich的例子确实起作用的原因是因为发送的消息很小且不常见,并且给出了无错误和大带宽的互联网连接,它可能"有效".然而,当您开始处理(更多)更大量的数据(更多)时,write:maxLength:调用可能会开始阻止,应用程序将开始停止...

对于此次NSStreamEventHasSpaceAvailable活动,Apple的文档有以下建议:

如果委托收到NSStreamEventHasSpaceAvailable事件并且没有向流写入任何内容,则它不会从运行循环接收更多空间可用事件,直到NSOutputStream对象接收到更多字节.... ...当接收到NSStreamEventHasSpaceAvailable事件时,如果委托不写入流,则可以让委托设置一个标志.稍后,当您的程序有更多字节要写时,它可以检查此标志,如果设置,则直接写入输出流实例.

因此,write:maxLength:在两种情况下调用只能"保证安全" :

  1. 回调内部(收到NSStreamEventHasSpaceAvailable事件时).
  2. 在回调之外,当且仅当我们已经收到NSStreamEventHasSpaceAvailable但被选为不在write:maxLength:回调内部调用时(例如我们没有实际写入的数据).

对于场景(2),我们将不会再次收到回调,直到write:maxLength实际直接调用 - Apple建议在委托回调中设置一个标志(见上文)以指示我们何时允许这样做.

我的解决方案是使用额外级别的缓冲 - 添加NSMutableArray一个数据队列.我将数据写入套接字的代码如下所示(为简洁起见,省略了注释和错误检查,currentDataOffset变量表示NSData我们发送了多少'当前' 对象):

// 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)