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

时间:2014-04-07 11:30:04

标签: ios cocoa-touch sockets

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

一方面,Apple's official documentation (Listing 2)表明在-stream:handleEvent:委托方法中,应该使用-write:maxLength:消息将数据写入输出缓冲区,每当从缓冲区连续传递数据时收到NSStreamEventHasSpaceAvailable事件。

另一方面,this tutorial from Ray Wenderlichthis iOS TCP socket example on GitHub完全忽略了NSStreamEventHasSpaceAvailable事件,只要他们需要,就可以继续-write:maxLength:到缓冲区(甚至忽略{ {1}})。

第三,this example code似乎 ......

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

2 个答案:

答案 0 :(得分:13)

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

当您可以写入流时,会发出NSStreamEventHasSpaceAvailable事件的信号 没有阻止。仅响应该事件而写入可避免当前线程 并且可能阻止了用户界面。

或者,您可以从单独的“编写器线程”写入网络流。

答案 1 :(得分: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:时(例如我们没有实际写入的数据)。
  3. 对于方案(2),我们将不会再次收到回调,直到实际直接调用write:maxLength - Apple建议在委托回调中设置一个标志(见上文),以指示我们何时允许这样做。

    我的解决方案是使用额外级别的缓冲 - 添加NSMutableArray作为数据队列。我将数据写入套接字的代码如下所示(为简洁起见,省略了注释和错误检查,currentDataOffset变量表示我们发送的当前&#39; 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;
            }
        }
    }