Pull to refresh

Ограничение скорости скачивания файлов средствами PHP

PHP *
Иногда появляется необходимость ограничивать скорость скачивания файлов пользователями. Причин тому может быть множество (бесплатный и платный режимы, регистрация пользователя и т.д.), однако не всегда есть возможность приобрести сервер или настроить его должным образом. В данном топике предлагаю переложить задачу на плечи чистый PHP.

Функция:


function loadfile ($filename, $speed=false) {
     // содержимое функции ниже
}
$filename — адрес файла, который собираемся отдавать.
$speed — скорость скачивания.


Убираем лимиты и ограничения, запускаем буферизацию вывода:


// лимит времени выполнения
set_time_limit(0);

// продолжаем выполнятся при отключении пользователя
ignore_user_abort(true);

// старт буферизации
ob_start();


Собираем необходимые данные о файле:


// размер файла
$filesize = filesize($filename);

// время последнего изменения файла
$filetime = gmdate('r', filemtime($filename));

// генерируем Etag
$etag = md5($filename."=".$filesize."=".$filetime);
$etag = substr($etag, 0, 8).'-'.substr($etag, 8, 7).'-'.substr($etag, 15, 8);


Формируем и отдаем заголовки:


// Если «просят» отдать не весь файл, а только его часть (докачка), то так и «отвечаем» 
if (isset($_SERVER['HTTP_RANGE'])) {

	// разделяем строку $_SERVER['HTTP_RANGE'] на подстроки, чтобы иметь
	// представление о необходимой пользователю части файла
	$range = substr($_SERVER['HTTP_RANGE'], strpos($_SERVER['HTTP_RANGE'], '=')+1);
	$from = (integer)(strtok($range, "-")); // с какого байта начать часть
	$to = (integer)(strtok("-")); // каким байтом закончить

	// Отдаем заголовки
	header('HTTP/1.1 206 Partial Content');
	header('Content-Range: bytes '.$from.'-'.($to-1).'/'.$filesize);
} else {
	header('HTTP/1.1 200 Ok');
}

// В случае, если клиент не передал информацию о первом последнем байте, то присвоим
// самостоятельно.
if ($to==0) $to = $filesize;
if (empty($from)) $from = 0;


Ограничение потоков для одного пользователя:


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

Итак, создаем таблицу. Так и назовем — `file_session`. В данном примере нам необходимо всего одно поле `session_ip` которое будет содержать IP-адрес скачивающего. При наличии необходимого IP в БД отдаем true, в противном случае записываем его и отдаем false.

function is_active_user($clear = false) {
	global $dbi; // это подключение к БД
	// проверяем на наличие соединений от пользователя
	$result = mysql_query ("SELECT `session_ip` FROM `file_session` WHERE `session_ip`='".$_SERVER['REMOTE_ADDR']."' LIMIT 1", $dbi);
	if (mysql_num_rows($result)) {
		// если установлено $clear, значит пользователь
		//разорвал соединение. Удаляем запись.
		if (!$clear) {
			return true;
		} else {
			mysql_query ("DELETE FROM `file_session` WHERE session_ip='".$_SERVER['REMOTE_ADDR']."' LIMIT 1", $dbi);
		}
	} else {
		// если запись отсутствует, то добавляем
		mysql_query ("INSERT INTO `file_session` VALUES ('".$_SERVER['REMOTE_ADDR']."')", $dbi);
		return false;
	}
}

В последствии функцию можно модернизировать, добавив еще и проверку на залогиненность, наличие оплаты и т.д.

Отдаем файл:


// Отдаем заголовки
header('ETag: "' . $etag . '"');
header('Accept-Ranges: bytes');
header('Content-Length: ' . ($filesize-$from));
header('Content-Type: application/octet-stream');
header('Last-Modified: ' . gmdate('r', filemtime($filename)));
header('Content-Disposition: attachment; filename="' . $filename . '";');

// Проверяем, есть ли у пользователя
// активные потоки is_active_user()
// и не отключился ли он connection_status().
while(is_active_user() and !connection_status()) {
	// спим пока у пользователя есть активные потоки
	sleep(1);
}

// Открываем файл
$f = fopen($filename, 'rb');

// Устанавливаем указатель а нужную позицию
fseek($f, $from, SEEK_SET);

// Устанавливаем общий объем части и объявляем переменную хранящую скачанный объем
$size = $to - $from;
$isready = 0;

// Начинаем отдачу
while(!feof($f) and !connection_status() and ($isready<$size)) {
	// Если ограничений нет, то считываем по 0.5 Мб и отдаем без задержек.
	// Установлено ограничение? Считываем и отдаем ровно столько, сколько установлено ограничением и засыпаем на 1 секунду.
	echo fread($f, !$speed?512000:$speed); // считываем и отдаем
	flush(); ob_flush(); // очищаем буфер и выводим пользователю
	if ($speed) sleep(1); // засыпаем
	$isready += !$speed?512000:$speed; // обновляем счетчик
}

// Закрываем файл
fclose($f);

// Удаляем информацию о соединении из БД
is_active_user(true);


Вызов функции:


loadfile ("/files/moifilm.avi", 10240); // Скорость указываем в байтах


Итог:


Полный код и рабочий пример скачивания без ограничения и с ограничением в 10 Кб/сек — файл 1 Мб.

P.S. Мой первый хабрапост (:
Tags: phpскачиваниескоростьограничениедокачка
Hubs: PHP
Total votes 30: ↑26 and ↓4 +22
Comments 30
Comments Comments 30

Popular right now