Правильный способ отправки данных через сокет с помощью NSOutputStream

Я только начинаю с программирования сокетов на iOS, и я изо всех сил пытаюсь определить использование события NSStreamEventHasSpaceAvailable для NSOutputStreams.

С одной стороны, официальная документация Apple (листинг 2) показывает, что в методе делегата -stream:handleEvent: данные должны быть записаны в выходной буфер с сообщением -write:maxLength:, постоянно передавая данные из буфера, всякий раз, когда принимается событие NSStreamEventHasSpaceAvailable.

С другой стороны, этот учебник от Ray Wenderlich и этот iOS Пример сокета TCP в GitHub игнорирует событие NSStreamEventHasSpaceAvailable и просто продолжайте и -write:maxLength: в буфер, когда им нужно (даже игнорируя -hasSpaceAvailable).

В-третьих, есть этот примерный код, который, как представляется, делает оба...

Итак, мой вопрос, каков правильный способ обрабатывать записи данных в NSOutputStream, который подключен к сокету? И каково использование кода события NSStreamEventHasSpaceAvailable, если он может (по-видимому) игнорироваться? Мне кажется, что происходит очень удачное выполнение UB (в примерах 2 и 3) или существует несколько способов отправки данных через NSOutputStream на основе сокетов...

Ответ 1

Вы можете писать в поток в любое время, но для сетевых потоков -write:maxLength: возвращается только до тех пор, пока по меньшей мере один байт не будет записан в буфер записи сокета. Поэтому, если буфер записи сокета заполнен (например, поскольку другой конец соединения не считывает данные достаточно быстро) это заблокирует текущий поток. Если вы напишете из основного потока, это заблокирует пользовательский интерфейс.

Событие NSStreamEventHasSpaceAvailable сигнализируется, когда вы можете записать в поток без блокировки. Запись только в ответ на это событие позволяет избежать того, что текущий поток и, возможно, пользовательский интерфейс заблокирован.

Кроме того, вы можете писать в сетевой поток из отдельного "потока писем".

Ответ 2

После просмотра ответа @MartinR я перечитал Документы Apple и немного прочитал события NSRunLoop. Решение было не таким тривиальным, как я думал раньше, и требует дополнительной буферизации.

Выводы

В то время как пример Ray Wenderlich работает, он не является оптимальным - как отмечено @MartinR, если в окне исходящего TCP нет места, вызов write:maxLength будет блокироваться. Причина, по которой работает пример Рэя Вендерлиха, заключается в том, что отправленные сообщения являются небольшими и нечастыми, и, учитывая безошибочное и широкополосное интернет-соединение, оно "вероятно" работает. Когда вы начинаете общаться с большим количеством отправляемых данных (чаще), тем не менее, вызовы write:maxLength: могут начать блокироваться, и приложение начнет останавливаться...

Для события NSStreamEventHasSpaceAvailable документация Apple имеет следующий совет:

Если делегат получает событие NSStreamEventHasSpaceAvailable и ничего не пишет в потоке, он не получает дополнительных пространственно доступных событий из цикла выполнения, пока объект NSOutputStream не получит больше байтов....... У вас может быть установлен флаг делегата, когда он не пишет в поток после получения события NSStreamEventHasSpaceAvailable. Позже, когда ваша программа имеет больше байтов для записи, она может проверить этот флаг и, если задана, записать непосредственно в экземпляр выходного потока.

Таким образом, он "гарантированно быть безопасным" для вызова write:maxLength: в двух сценариях:

  • Внутри обратного вызова (при получении события NSStreamEventHasSpaceAvailable).
  • За пределами обратного вызова тогда и только тогда, когда мы уже получили 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;
        }
    }
}