Обслуживание файлов с помощью IdHTTPServer при записи файлов

Я работаю с TIdHTTPServer для предоставления файлов клиентам, используя функцию ResponseInfo->ServeFile. Это отлично работает для файлов, которые являются «статическими»: они не записываются каким-либо другим процессом. Насколько я вижу из кода, функция ServeFile внутри использует TIdReadFileExclusiveStream, что не позволяет мне читать записываемый файл, но мне нужно также иметь возможность отправлять файлы, которые записываются каким-то другим процессом.

Итак, я перешел к созданию FileStream самостоятельно и использовал свойство ContentStream, чтобы вернуть его клиенту, но я получаю в клиенте файл размером 0 байт (для любого файла, записываемого или нет), и я не вижу, что я упускаю или делаю неправильно. Вот код, который я использую в обработчике событий OnCommandGet:

AResponseInfo->ContentStream = new TFileStream(path, fmOpenRead | fmShareDenyNone);
AResponseInfo->ContentStream->Position = 0;
AResponseInfo->ContentLength = AResponseInfo->ContentStream->Size;
AResponseInfo->ResponseNo = 200;
AResponseInfo->WriteHeader();
AResponseInfo->WriteContent();

Свойство ContentLength в этот момент имеет допустимое значение (т. е. размер файла при вызове ContentStream->Size), и это то, что я хотел бы отправить клиенту, даже если файл изменится между ними.

Я попытался удалить функцию WriteContent(), WriteHeader(), но результаты те же. Я искал несколько примеров, но те немногие, что я нашел, более или менее совпадают с этим кодом, поэтому я не знаю, что не так. Большинство примеров не включают вызов WriteContent(), поэтому я попытался удалить их, но, похоже, нет никакой разницы.

В качестве примечания: для завершения записи файлов требуется 24 часа, но этого следует ожидать со стороны клиента: мне просто нужны байты, уже записанные на момент запроса (даже несколько меньше допустимо). Файлы никогда не будут удалены: они просто будут увеличиваться в размерах.

Любые идеи?

Обновить

Используя Fiddler, я получаю некоторые предупреждения о нарушениях протокола, которые могут быть связаны с этим. Я получаю, например:

Content-Length mismatch: Response Header indicated 111,628,288 bytes, but server sent 41 bytes

Длина содержимого правильная, это размер файла, но я не знаю, что я делаю неправильно, из-за чего приложение отправляет всего 41 байт.


person Rodrigo Gómez    schedule 09.02.2015    source источник


Ответы (2)


WriteHeader() и WriteContent() ожидают, что ContentStream будет полным и неизменным в момент их вызова. WriteHeader() создает заголовок Content-Length, используя текущее значение ContentStream->Size, если свойство AResponseInfo->ContentLength равно -1 (на самом деле вы сами устанавливаете значение), а WriteContent() отправляет столько байтов, сколько говорит текущее значение ContentStream->Size. Таким образом, ваш клиент получает 0 байтов, потому что файл Size все еще равен 0 в то время, когда вы вызываете WriteHeader() и WriteContent().

Ни ServeFile(), ни ContentStream не подходят для ваших нужд. Поскольку файл записывается в реальном времени, вы не знаете окончательный размер файла, когда заголовки HTTP создаются и отправляются клиенту. Поэтому вы должны использовать chunked кодирование передачи< /a> для отправки данных файла. Это позволит вам отправлять данные файла порциями по мере записи файла, а затем сигнализировать клиенту, когда файл будет завершен.

Однако TIdHTTPServer изначально не поддерживает отправку ответов chunked, поэтому вам придется реализовать это вручную, например:

TFileStream *fs = new TFileStream(path, fmOpenRead | fmShareDenyNone);
try
{
    AResponseInfo->ResponseNo = 200;
    AResponseInfo->TransferEncoding = "chunked";
    AResponseInfo->WriteHeader();

    TIdBytes buffer;
    buffer.Length = 1024;

    do
    {
        int NumRead = fs->Read(&buffer[0], 1024);
        if (NumRead == -1) RaiseLastOSError();
        if (NumRead == 0)
        {
            // check for EOF, unless you have another way to detect it...
            Sleep(1000);
            NumRead = fs->Read(&buffer[0], 1024);
            if (NumRead <= 0) break;
        }

        // send the current chunk
        AContext->Connection->IOHandler->WriteLn(IntToHex(NumRead));
        AContext->Connection->IOHandler->Write(buffer, NumRead);
        AContext->Connection->IOHandler->WriteLn();
    }
    while (true);

    // send the last chunk to signal EOF
    AContext->Connection->IOHandler->WriteLn("0");

    // send any trailer headers you need, if any...

    // finish the transfer encoding
    AContext->Connection->IOHandler->WriteLn();
}
__finally
{
    delete fs;
}
person Remy Lebeau    schedule 09.02.2015
comment
Спасибо за подробный ответ, но: Размер, который я устанавливаю, не равен 0. Это правильное значение в тот момент для запрашиваемого файла, скажем, 100 МБ, и мне все равно, если файл вырастет (они никогда не получат меньше и не удалены) между ними. У меня нет проблем с отправкой меньше фактического размера. И код, который я разместил, не работает и для уже готовых файлов. Клиент не может ждать весь файл, так как на его запись уходит 24 часа. - person Rodrigo Gómez; 10.02.2015
comment
Вам не нужно устанавливать AResponseInfo->ContentLength вручную, если вы используете AResponseInfo->ContentStream или AResponseInfo->ServeFile(), они обрабатывают ContentLength за вас. Но они работают, поэтому, если у вас проблемы с неправильными/отсутствующими данными, возможно, вы делаете что-то не так в коде, который не указали в этом вопросе. - person Remy Lebeau; 10.02.2015
comment
Я гарантирую вам, что при правильном использовании, если файл имеет размер не менее 111628288 байт, то будет отправлено не менее 111628288 байт. Единственный способ отправить 41 байт — это если файл действительно уменьшится до 41 байта. Однако существует состояние гонки между WriteHeader() и WriteContent(), поэтому, если файл РАСТУТ, возможно, что WriteContent() может отправить БОЛЬШЕ байтов, чем указано в заголовке Content-Length, сгенерированном WriteHeader(), и это будет плохо для клиента. Это похоже на ошибку в TIdHTTPServer, которую нужно исправить. - person Remy Lebeau; 10.02.2015
comment
... поэтому вам, вероятно, нужно обойти WriteContent() и отправить данные файла вручную, чтобы они соответствовали фактическому отправляемому заголовку Content-Length. Или, по крайней мере, оберните исходный TFileStream в пользовательский TStream, который обеспечивает согласованное значение Size, даже если файл увеличивается во время использования потока. - person Remy Lebeau; 10.02.2015
comment
В общем, TIdHTTPServer на самом деле не предназначен для обработки данных в реальном времени, поэтому вам, возможно, придется пройти через некоторые дополнительные обручи, чтобы заставить его работать. - person Remy Lebeau; 10.02.2015
comment
Я пробовал со статическими файлами, и я получаю такое же поведение. Как вы сказали, должно быть что-то, что я делаю неправильно, но здесь больше нет кода, который можно было бы показать (кроме той части, где я локализовал файл для отправки). 41 байт: <HTML><BODY><B>200 OK</B></BODY></HTML>. Я частично решил это уродливым способом: я копирую файл во временное место и использую ServeFile, но мне бы очень хотелось этого избежать. - person Rodrigo Gómez; 10.02.2015
comment
Этот HTML-код отправляется WriteContent(), когда ContentText пусто, а ContentStream равно нулю. TIdHTTPServer вызывает WriteContent() автоматически, когда обработчик событий OnCommand... завершает работу, если назначены ContentText или ContentStream. В коде, который я вам дал, это не так. Таким образом, WriteContent() будет вызываться только в том случае, если вы вызываете его сами, даже если вы не назначаете ContentText или ContentStream. - person Remy Lebeau; 10.02.2015
comment
Я изменил вызов WriteContent() на тот же код, который функция ServeFile выполняет внутри: вызовите 'IOHandler.Write' с указателем TFileStream и размером, переданным в ContentLength, и теперь он работает, как и ожидалось. Есть ли в этом смысл? - person Rodrigo Gómez; 10.02.2015
comment
Это имеет смысл, потому что ни ContentText, ни ContentStream не назначаются при использовании ServeFile() (или кода, который вы скопировали из ServeFile()), и вы удалили свой ручной вызов WriteContent(), а TIdHTTPServer не будет автоматически вызывать WriteContent() в этих условиях, поэтому HTML по умолчанию не быть отправленным. - person Remy Lebeau; 10.02.2015
comment
Ok. Окончательный код точно такой же, как я изначально опубликовал, но вместо WriteContent я вызываю IOHandler->Write с указателем TFileStream. Я имею в виду: я назначаю ContentStream, хотя я думаю, что это не имеет никакого значения при непосредственном использовании IOHandle->Write, верно? - person Rodrigo Gómez; 10.02.2015
comment
Вам не нужно назначать ContentStream, если вы звоните IOHandler->Write(TStream) напрямую. Но если файл увеличивается, пока ваш сервер готовит/отправляет ответ, у вас все еще есть состояние гонки между Size, которое вы назначаете AResponseInfo->ContentLength, и Size, которое видит Write(). Чтобы Write() не отправлял больше байтов, чем сообщает клиенту заголовок Content-Length, вы можете передать AResponseInfo->ContentLength в качестве параметра ASize для Write(). Я собираюсь добавить это исправление в TIdHTTPServer, но пока не знаю, когда оно будет доступно. - person Remy Lebeau; 10.02.2015
comment
Да, это именно то, что я делаю: устанавливаю переменную с размером перед чем-либо (для моего собственного ведения журнала) и использую это значение как для параметра ContentStream, так и для параметра IOHandler->Write. Прямо сейчас это работает нормально, и я использую версию 5204 (IIRC), поэтому я не спешу обновлять Indy. - person Rodrigo Gómez; 10.02.2015
comment
Текущая версия SVN — 5244. - person Remy Lebeau; 10.02.2015
comment
Да, я видел это сегодня, когда искал информацию об этом. Я постараюсь обновить в ближайшее время ... У меня есть несколько других библиотек (в основном RemObjects), которые зависят от Indy, и мне не очень удобно их пересобирать (даже если это довольно легко по сравнению с некоторыми другими сторонними библиотеками) и надеюсь, что никто иначе ломается. В любом случае, спасибо за вашу помощь. - person Rodrigo Gómez; 10.02.2015

Окончательный рабочий код:

    std::unique_ptr< TFileStream >fs(new TFileStream(path, fmOpenRead | fmShareDenyNone));

    fs->Position = 0;
    __int64 size = fs->Size;

    AResponseInfo->ContentLength = size;
    AResponseInfo->ResponseNo    = 200;

    AResponseInfo->WriteHeader();

    AContext->Connection->IOHandler->Write(fs.get(), size);

Это позволяет клиенту получить до size байт исходного файла, даже если файл записывается в то же время.

По какой-то причине передача ContentStream не возвращает никакого контента клиенту, но выполнение IOHandler->Write напрямую (это то, что ServeFile заканчивает делать внутри) работает нормально.

person Rodrigo Gómez    schedule 10.02.2015