Правильная выдача заголовка Content-Length

Однажды, скачивая один из своих сайтов с помощью WGet, с удивлением обнаружил, что после каждой страницы программа останавливается и ждет несколько секунд. Да пользователи иногда тоже жаловались на то, что загрузка сайта очень долго не заканчивается. Стал разбираться, в чем дело, и вот что удалось выяснить.


Как известно, в HTTP-протоколе есть три способа сообщить клиенту о том, что передача завершена. Первый, самый старый, — разорвать соединение после того, как весь контент передан. Он же самый медленный, так как при этом не работает механизм Keep-Alive, и для следующего запроса клиент должен установить новое соединение. Второй — это передача в заголовке Content-Length длины (в байтах) передаваемого контента. И наконец, третий — передача контента блоками, перед каждым из которых передается его длина в шестнадцатеричном виде, а завершение передачи обозначается передачей блока с нулевой длиной (так наказываемый chunked transfer, возможно, о нем я напишу со временем отдельную запись).

По каким-то причинам я считал, что в случае использования PHP используется chunked transfer, причем длину chunks определяет и выдает либо сам Apache, либо модуль PHP. Увы, это оказалось не так, и на моем сайте использовался самый медленный первый способ — закрытие соединения (причем не сразу, а после какого-то тайматуа, что и вызывало задержки).

Возник вопрос, как выводить Content-Length. На первый взгляд, все просто: собрать весь выводимый HTML-код в строку-буфер, посчитать ее длину и выдать. Но во-первых, в некоторых скриптах выдача делалась сразу, а не через буфер, а во-вторых, если включено сжатие GZip, реальная длина отдаваемого контента меньше переданной в Content-Length, и клиент ждет оставшейся части до тех пор, пока не истечет таймаут соединения. (Особенно плохо к этому относятся поисковики, от такого сайт может даже выпасть из выдачи.)

Тем не менее, решение все же есть. Нужно использовать встроенный механизм буферизации вывода в PHP, в частности, функцию ob_get_length, позволюящую узнать реальную длину этого буфера. Выглядит это примерно так:

<?php
ob_start('ob_gzhandler');
ob_implicit_flush(0); // отключаем неявную отправку буфера
/* Здесь идет код скрипта, в нем не должно быть ob_flush, так как потом нельзя будет выдавать заголовки */
if (!header_sent()) header('Content-Length: '.ob_get_length()); // если заголовки еще можно отправить, выдаем загловок Content-Length, иначе придется завершать передачу по закрытию

ob_end_flush();
exit(0);