• Записи 162
  • Теги 66
  • Комментарии 330

Компьютерное

Правильная выдача заголовка 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);

2 комментария:

Нет
Денис
0

Спасибо за полезную информацию, но еще хотелось бы увидеть способ с chunked transfer

4X_Pro
0

Вообще, обычно chunked transfer применяется тогда, когда длина выдаваемой страницы целиком заранее не известна. В этом случае Content-Length лучше не выдавать вообще. А длина каждого отдельного chunk передается перед началом его самого, там все то же самое, только нет слов Content-Lenght и саму длину chunk нужно выдавать не через header, а обычным echo и переводить в hex.
То есть получаем примерно такое:
ob_start();
header('Transfer-Encoding: chunked');
while (!$finish) {
// что-то делаем с выдачей с помощью echo
echo sprintf("%X\r\n",ob_get_length()); // выдаем длину текущего chunk в hex
ob_flush(); // выдаем сам chunk
}
echo "0\r\n"; // пустой chunk означает завершение передачи

Написать комментарий


Задать вопрос