Отправка POST запросов с помощью php. Отправка файлов
Немного раньше мы рассмотрели отправку пост запросов в PHP с помощью сокетов, с помощью библиотеки curl и с помощью врапперов потока . В этих статьях были приведены конкретные примеры программной отправки переменных по методу POST. Но иногда возникает ситуация, когда программно нужно отправить не только переменные, но и еще отправить произвольный файл по протоколу HTTP. Именно эту конкретную ситуацию мы и рассмотрим сегодня.
В протоколе HTTP предусмотрены два способа отправки файлов. По методам PUT и POST. Сегодня речь пойдет об отправке файлов по методу POST.
Пример
Для начала напишем тестовый пример, который выведет все данные, которые мы будем отправлять программно.
test.php
<?php if(isset($_FILES['my_file']['name'])&&$_FILES['my_file']['size']>0 &&$_FILES['my_file']['error']==0){ echo 'Name:'.$_POST['name'].' Surname:'.$_POST['surname'].'<br/>'; readfile($_FILES['my_file']['tmp_name']); }else{ echo 'Ошибка: не был получен файл'; } ?>
Сначала проверяем, был ли закачан файл посредством проверки данных в массиве $_FILES. Если файл успешно закачан, то выводим пост-переменные name и surname, после чего выводим содержимое загруженного файла.
Файл я расположил на локальном сервере по адресу http://www.example.loc/test.php. Еще для тестов я взял jpeg-файл, test_file.jpg, который положил рядом.
Форма
Чтобы уяснить некоторые нюансы отправки файлов по методу post, давайте напишем небольшую html-форму, которая смогла бы справиться с загрузкой файла и установки двух переменных. Вот код формы.
form.html
<form action= "http://www.example.loc/test.php" name= "upload" method="post" enctype="multipart/form-data"> Name:<input type="text" /><br/> Surname:<input type="text" /><br/> File:<input type="file" /><br/> <input type="submit" value="Submit" /> </form>
Эта форма вполне может справиться с отправкой данных и файла за счет браузера. Прошу обратить внимание на параметр enctype тега form. Этот параметр по умолчанию имеет значение application/x-www-form-urlencoded и обычно не пишется. Для отправки файла этот параметр нужно установить в значение multipart/form-data. Этот параметр на самом деле указывает браузеру на тип данных, которые будут переданы. В соответствии с этим параметром устанавливается заголовок Content-type (подробнее про заголовки см. в первой статье по отправке пост запросов).
Структура
Когда мы пытаемся отправить файл, то структура тела запроса меняется. Теперь это не просто query-string (имя_переменной1=значение&имя_переменной2=значение…). Все тело запроса разделяется на несколько частей. Одна часть – это файл, вторая часть – это первая переменная, третья часть – это вторая переменная и т.д.
Каждая часть состоит из заголовков и тела. Заголовки отделяются от тела пустой строкой. Каждый заголовок написан на отдельной строке. Первым заголовком здесь является Content-Disposition. Записывается так:
Content-Disposition:form-data;name=”имя_переменной”
Если в этой части находится файл, то через точку с запятой ; после name в Content-Disposition записывается имя файла вот так
Content-Disposition:form-data;name=”file”;filename=”file.jpg”
Если в этой части находится файл, то добавляются заголовки Content-type и Content-Transfer-Encoding. Последний устанавливается в binary.
В начале каждой части на отдельной строке вписывается специальный разделитель. Разделитель начинается с --, а затем без пробелов какой-либо уникальный текст. В php он обычно генерируется так
$boundary = md5(uniqid(time()));
Этот код генерирует такого рода уникальный разделитель.
ce77f5f41b001bd35088f9a3fd6c957a
После последней части на отдельной строке пишется разделитель, перед и после которого добавляется --, вот так:
--ce77f5f41b001bd35088f9a3fd6c957a--
Этот разделитель указывается в заголовке Content-type через точку с запятой,вот так
Content-type:multipart/form-data;boundary= ce77f5f41b001bd35088f9a3fd6c957a
Теперь, ознакомившись с основами, можно приступать к конкретным примерам.
Сокеты
Для начала рассмотрим пример отправки файла с помощью сокетов. При отправке файла этим способом придется собрать все заголовки.
socket.php
<?php //открываем сокет к www.example.loc на 80-й порт //с таймаутом в 30 секунд $socket = fsockopen('www.example.loc', 80, $errno, $errstr, 30); //если fsockopen вернула false, то завершаем работу //скрипта и выводим текст //и номер ошибки if(!$socket)die("$errstr($errno)"); //разделитель $boundary = md5(uniqid(time())); /*собираем часть с файлом: сначала разделитель\r\n со следующей строки заголовки\r\n потом пустая строка\r\n после чего сам файл\r\n */ $file = "--$boundary\r\n". "Content-Disposition: form-data; name=\"my_file\";". " filename=\"test_file.jpg\"\r\n". "Content-Type: image/jpeg\r\n". "Content-Transfer-Encoding: binary\r\n\r\n"; $file.= file_get_contents(dirname(__FILE__).DIRECTORY_SEPARATOR. 'test_file.jpg'); $file.="\r\n"; /* как и файл, собираем переменные сначала разделитель\r\n со следующей строки заголовки\r\n пустая строка\r\n потом значение переменной\r\n */ $var1 = "--$boundary\r\nContent-Disposition: '. 'form-data; name=\"name\"\r\n\r\n". urlencode("John")."\r\n"; $var2 = "--$boundary\r\nContent-Disposition:'. 'form-data;name=\"surname\"\r\n\r\n". urlencode("Smith")."\r\n"; //пишем в сокет метод, URI и протокол fwrite($socket, "POST /test.php HTTP/1.1\r\n"); //а также имя хоста fwrite($socket, "Host: www.example.loc\r\n"); //представимся оперой fwrite($socket,"User-agent:Opera 10.00\r\n"); fwrite($socket, "Connection: close\r\n"); //теперь отправляем заголовки //Content-type должен быть multipart/form-data, //также должен быть указан разделитель, //который мы сгенерировали выше fwrite($socket,"Content-Type: '. 'multipart/form-data; boundary=$boundary\r\n"); //размер передаваемых данных передаем в заголовке //Content-length fwrite($socket,"Content-length:".(strlen($file)+strlen($var1)+ strlen($var2))."\r\n"); //типы принимаемых данных. */* //означает, что принимаем все типы данных fwrite($socket,"Accept:*/*\r\n"); fwrite($socket,"\r\n"); //теперь передаем данные //передаем файл fwrite($socket,"$file"); //и переменные fwrite($socket, "$var1$var2"); //в конце разделитель fwrite($socket,"--$boundary--\r\n"); //и пустая строка fwrite($socket, "\r\n"); //теперь читаем и выводим ответ $answer = ''; while(!feof($socket)){ $answer= fgets($socket, 4096); echo $answer; } //закрываем сокет fclose($socket); ?>
Сохранив его на локальном сервере и обратившись к нему из браузера можно увидеть приблизительно такую картину
HTTP/1.1 200 OK Date: Sun, 06 Dec 2009 19:02:01 GMT Server: Apache/2.2.9 (Win32) PHP/5.2.11 X-Powered-By: PHP/5.2.11 Connection: close Transfer-Encoding: chunked Content-Type: text/html Name:John Surname:Smith
╪ р
…………………….
Далее идет вывод jpeg-файла. Если бы не вывод заголовков и вывод пост-переменных, то отобразилась бы картинка.
Curl
В библиотеке curl проблем с отправкой пост-переменных нет. А вот как только задашься целью отправить файл, так тут и поджидает подвох. Как же быть с отправкой файла? Куда же нужно устанавливать файл?
Ответ очень прост. В CURLOPT_POSTFIELDS все данные передаются массивом, а в поле, в котором будет файл передается путь к файлу, а перед значением пути ставится @.
curl.php
<?php //инициализируем сеанс $curl = curl_init(); //уcтанавливаем урл, к которому обратимся curl_setopt($curl, CURLOPT_URL, 'http://www.example.loc/test.php'); //включаем вывод заголовков curl_setopt($curl, CURLOPT_HEADER, 1); //передаем данные по методу post curl_setopt($curl, CURLOPT_POST, 1); //теперь curl вернет нам ответ, а не выведет curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); /*переменные, которые будут переданные по методу post Перед именем файла ставится @ */ curl_setopt($curl, CURLOPT_POSTFIELDS, array('name'=>urlencode('John'), 'surname'=>urlencode('Smith'), 'my_file'=>'@'.dirname(__FILE__). DIRECTORY_SEPARATOR.'test_file.jpg' ) ); //я не скрипт, я браузер опера curl_setopt($curl, CURLOPT_USERAGENT, 'Opera 10.00'); $res = curl_exec($curl); //проверяем, если ошибка, то получаем номер и сообщение if(!$res){ $error = curl_error($curl).'('.curl_errno($curl).')'; echo $error; } //если не ошибка, то выводим результат else{ echo $res; } curl_close($curl); ?>
В результате как и в прошлый раз выведет заголовки, значение пост-переменных и jpeg-файл
HTTP/1.1 100 Continue HTTP/1.1 200 OK Date: Sun, 06 Dec 2009 19:13:43 GMT Server: Apache/2.2.9 (Win32) PHP/5.2.11 X-Powered-By: PHP/5.2.11 Transfer-Encoding: chunked Content-Type: text/html Name:John Surname:Smith
╪ р
………………………
Как выяснилось, курл не умеет правильно устанавливать тип файла. Во всех случаях он ставит тип application/octet-stream. Поэтому в некоторых случаях сервер будет выдавать ошибку связанную с типом файла.
Stream
Теперь попробуем совладать с врапперами для потоков. Для врапперов нужно собирать почти все заголовки как и в случае с сокетами.
context.php
<?php $boundary = md5(uniqid(time())); $file = "--$boundary\r\nContent-Disposition:". " form-data; name=\"my_file\";". " filename=\"test_file.jpg\"\r\n"."Content-Type: image/jpeg\r\n". "Content-Transfer-Encoding: binary\r\n\r\n"; $file.= file_get_contents(dirname(__FILE__). DIRECTORY_SEPARATOR.'test_file.jpg'); $file.="\r\n"; //собираем переменные $var1 = "--$boundary\r\nContent-Disposition:". " form-data; name=\"name\"\r\n\r\n". urlencode("John")."\r\n"; $var2 = "--$boundary\r\nContent-Disposition:". "form-data;name=\"surname\"\r\n\r\n". urlencode("Smith")."\r\n"; //опции для создания контекста $c_options = array( //имя враппера - http 'http'=>array( //метод - POST 'method'=>'POST', //заголовки 'header'=>"Content-type: ". "multipart/form-data;boundary=$boundary\r\n". "User-agent:Opera 10.00\r\nContent-length:". (strlen($file)+strlen($var1)+strlen($var2)), //данные 'content'=>$file.$var1.$var2."--$boundary--\r\n\r\n" ) ); //создаем контекст $context = stream_context_create($c_options); //читаем файл с контекстом $res = file_get_contents('http://www.example.loc/test.php', false,$context); //выводим результат echo $res; ?>
В результате выведет
Name:John Surname:Smith
╪ р
……………………
Что собственно и ожидали от сервера.
Читайте все статьи цикла.
В первой части рассматривается работа с сокетами.
Во второй части описывается библиотека cURL.
В третьей части можно почитать о работе с Stream Function.
В четвертой части описывается отправка файла через POST.
В пятой части описывается отправка POST-запросов в Zend Framework.
Популярность: 57%
Интересное из других блогов:
2leep.comИ не забывайте комментировать статью.
Добавляйся в группу во вконтакте, чтобы самым первым узнавать все новости сайта
Автор: artbox, 16 декабря 2010 в 15:39
использует1й пунк решился путем перекодирования оператором iconv c 1251 в utf8 и отказом от urlencode.
К сожелению сервер отказался принимать изображение ругаясь на неверный формат и все оказалось напрасным((
Спасибо за пояснения насчет (__FILE__).DIRECTORY_SEPARATOR. и за полезную статью!
Автор: Владимир, 29 марта 2011 в 12:56
используетСпасибо, очень выручили) Сайт в закладки!
Автор: Владимир, 30 марта 2011 в 01:53
используетА не подскажете как отправить файл со стороннего сервера?? С помощью cURL
Автор: web-junior, 30 марта 2011 в 19:57
используетну только если скачать его со стороннего сервера себе. А потом отправить куда нужно так, как описано в статье.
Автор: Владимир, 17 апреля 2011 в 00:08
используетА вот ещё вопрос, не знаете как избавится от заголовков в сокетах?)
Автор: nazar-pc, 5 июня 2011 в 15:07
используетЗдравствуйте!
Очень благодарен за простой выклад материала, ибо с такими запросами сталкиваюсь впервые, и вы мне очень помогли.
В свою очередь хочу сообщить об ошибке в строках последнего примера:
‘header’=>”Content-type: ‘.
‘multipart/form-data;boundary=$boundary\r\n”.
Там конкатенация строк неправильно записана, должно быть:
‘header’=>”Content-type: “.
“multipart/form-data;boundary=$boundary\r\n”.
Автор: web-junior, 5 июня 2011 в 18:40
используетЗдравствуйте.
Спасибо, что указали на ошибку. Всё исправил.
Автор: stepennwolf, 4 мая 2012 в 20:44
используетЗдравствуйте, а вот имеется вопрос такого характера.
Что если вдруг размер файла скажем уже более 10Мб, а функция file_get_contents и ей производные загружают содержимое в память, а это я думаю не эффективно( тем более если размер файла и 100Мб) как быть в этом случае?
Автор: web-junior, 5 мая 2012 в 21:04
используетПриветствую. В этом случае можно использовать только curl. Он не требует сбора файла в строку, чем может сильно снизить нагрузку, если вы это имеете ввиду.