Показаны сообщения с ярлыком http. Показать все сообщения
Показаны сообщения с ярлыком http. Показать все сообщения

понедельник, 6 августа 2012 г.

PHP Sockets - отправка файла

Это продолжение записки "PHP модуль Sockets для HTTP запросов", здесь будет дополнение к ранее написанному и я не буду приводить весь код класса.

Уже при написании HttpSocket мне было интересно узнать, как составить HTTP заголовок для отправки файла. Поэтому я решил разобраться что и как, и дописать эту функциональность.

Как указано в RFC 1867, при отправке файла методом POST, заголовок Content-Type должен принять значение «multipart/form-data». Также Content-Type должен содержать определение boundary:

Content-Type: multipart/form-data; boundary=ABCuniq_idABC

Более того теперь тело запроса разделено на несколько частей начинающихся, разделённых и заканчивающихся строкой указанной в boundary. Каждая часть содержит свой «подзаголовок» и выглядит следующим образом:

--ABCuniq_idABC
Content-Disposition: form-data; name="var_name1"

value1
--ABCuniq_idABC
Content-Disposition: form-data; name="var_name2"

value2
--ABCuniq_idABC
Content-Disposition: form-data; name="img_file"; filename="IMG_20120305_102159.jpg"
Content-Type: image/jpeg

<содержимое файла>
--ABCuniq_idABC--

Обратите внимание, как к строке boundary добавляются символы «--».
Указанная информация о файле, в случае удачной отправки, на принимающей стороне может выглядеть примерно так:

$_FILES = Array(
[img_file] => Array(
[name] => IMG_20120305_102159.jpg
[type] => image/jpeg
[tmp_name] => C:\WINDOWS\Temp\php5DF.tmp
[error] => 0
[size] => 450055
)
)

В дополнение к сделанному, я думал что лучше будет отправлять файл закодированным в base64, чтобы данные не исказились, да и при выводе заголовка он выглядит лучше. Я исследовал такие поля как Content-Encoding, Transfer-Encoding и даже Content-Transfer-Encoding.

Как выяснилось, протокол HTTP принимает двоичные данные (содержимое файла) без проблем и кодирование не нужно. Поля Content-Encoding, Transfer-Encoding нужны соотвественно для сжатия и передачи содержимого по частям, а поле Content-Transfer-Encoding не поддерживается HTTP и используется в почтовых протоколах.

Если с заголовком всё понятно, то написание метода не создаст проблем. Но перед этим, одно «но»: на данный момент в PHP нет надёжного встроенного способа определить MIME-тип произвольного файла. Зато есть простой способ определить MIME-тип изображений. Поэтому я буду отправлять только изображения.

Получившийся у меня код метода класса HttpSocket:

public function reqPostFile($addr,
                            $var_arr = array(),
                            $file_path = null)
{
    $this->parseAddr($addr);
 
    $boundary = '>>============' . uniqid() . '<<';
    $req_body = '';
 
    foreach ($var_arr as $var_name => $var_val)
    {
        $req_body .= '--' . $boundary . NL;
        $req_body .= 'Content-Disposition: form-data;' .
            ' name="' . $var_name . '"' . DNL;
 
        $req_body .= $var_val . NL;
    }
 
    if (!is_null($file_path))
    {
        if (($mime = getimagesize($file_path)) !== false)
        {
            $mime = $mime['mime'];
 
            $req_body .= '--' . $boundary . NL;
            $req_body .= 'Content-Disposition: form-data;' .
                ' name="img_file";' .
                ' filename="' . basename($file_path) . '"' . NL;
 
            $req_body .= 'Content-Type: ' . $mime . DNL;
            $req_body .= file_get_contents($file_path) . NL;
 
        } else {
 
            trigger_error('Mime type of image 
                "' . $file_path . '" can`t be detected',
                E_USER_ERROR);
        }
    }
 
    $req_body .= '--' . $boundary . '--' . NL;
 
 
    $header = 'POST ' . $this->query_str . ' HTTP/1.1' . NL;
    $header .= 'Host: ' . $this->host_str . NL;
    $header .= 'Content-Type: multipart/form-data;' .
        ' boundary=' . $boundary . NL;
 
    $header .= 'Content-Length: ' . strlen($req_body) . NL;
    $header .= 'Connection: Close' . DNL;
 
    $header .= $req_body;
 
    return $this->doQuery($header);
}

вторник, 17 июля 2012 г.

PHP модуль Sockets для HTTP запросов

По работе мне потребовалось использовать удалённое API для получения информации об IP клиентов. В PHP я нашёл два варианта как можно выполнить запрос на удалённый сервер: Sockets и cURL. Так как с cURL я был уже знаком, решил узнать что такое Sockets.

Насколько я понял - Сокет это просто точка соединения. Есть разные типы Сокетов, наиболее распространённые (и реализованные в PHP) это AF_INET для интернет-соединений и AF_UNIX для коммуникаций между процессами ОС.

Это мой первый опыть работы с Sockets, поэтому наверняка есть какие-то недочёты в моём решении. Плюс к этому - моя задача, на данный момент, ограничена работой с HTTP-запросами: отправить Get или Post, и считать ответ.

Первым делом, я взял один из примеров с документации по PHP и начал работу с него. Принцип работы с этим модулем следующий:

// создание сокета
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// подключение созданного сокета
socket_connect($socket, $address, $service_port);

// запись в сокет запроса
socket_write($socket, $in, strlen($in));

// считывание ответа фиксированными порциями
while ($out = socket_read($socket, 2048))
{
}

// закрытие
socket_close($socket);

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

Необходимую информацию о заголовках я нашёл тут:
, и официальный ресурс (которым я почти не воспользовался):

У меня получился следующий класс:

if (!defined('NL'))
{
 define('NL', "\r\n");
 define('DNL', NL . NL);
}


class HttpSocket
{
 private $socket;

 private $host_str;
 private $query_str;
 private $ip_addr;

 private $resp_code;


 public function __construct()
 {
  $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

  if ($this->socket === false)
   trigger_error('socket_create() failed: reason: ' . socket_strerror(socket_last_error()), E_USER_ERROR);
 }


 public function reqGet($addr)
 {
  $this->parseAddr($addr);

  $header = 'GET ' . $this->query_str . ' HTTP/1.1' . NL;
  $header .= 'Host: ' . $this->host_str . NL;
  $header .= 'Connection: Close' . DNL;

  return $this->doQuery($header);
 }


 public function reqPost($addr, $var_arr = array())
 {
  $this->parseAddr($addr);
  $req_str = http_build_query($var_arr);

  $header = 'POST ' . $this->query_str . ' HTTP/1.1' . NL;
  $header .= 'Host: ' . $this->host_str . NL;
  $header .= 'Content-Type: application/x-www-form-urlencoded' . NL;
  $header .= 'Content-Length: ' . strlen($req_str) . NL;
  $header .= 'Connection: Close' . DNL;

  $header .= $req_str;

  return $this->doQuery($header);
 }


 public function getRespCode()
 {
  return $this->resp_code;
 }


 public function __destruct()
 {
  socket_close($this->socket);
 }


 private function parseAddr($addr)
 {
  // http://hostname.com/query/string?v=1
  // http://192.168.241.125/query/string?v=1

  preg_match('/^(?:https?:\/\/)?([^\/]+)(.*)/', $addr, $matches);

  if (filter_var($matches[1], FILTER_VALIDATE_IP))
  {
   $this->ip_addr = $matches[1];

  } else {

   $this->host_str = $matches[1];
   $this->ip_addr = gethostbyname($this->host_str);
  }

  $this->query_str = strlen($matches[2]) ? $matches[2] : '/';
 }


 private function doQuery($header)
 {
  $service_port = getservbyname('www', 'tcp');
  $result   = socket_connect($this->socket, $this->ip_addr, $service_port);

  if ($result === false)
   trigger_error('socket_connect() failed. Reason: (' . $result . ') ' . socket_strerror(socket_last_error($this->socket)), E_USER_ERROR);

  socket_write($this->socket, $header, strlen($header));

  $resp_str = $this->doFetch();
  $resp_bdy = $this->parseResponse($resp_str);

  return $resp_bdy;
 }


 private function doFetch()
 {
  $resp = '';

  while ($out = socket_read($this->socket, 2048))
   $resp .= $out;

  return $resp;
 }


 private function parseResponse($resp_str)
 {
  $pos_cr  = strpos($resp_str, "\n\n");
  $pos_crlf = strpos($resp_str, "\r\n\r\n");

  if ($pos_cr !== false AND $pos_cr < $pos_crlf)
  {
   $sep_pos = $pos_cr;
   $sep_len = 2;

  } else {

   $sep_pos = $pos_crlf;
   $sep_len = 4;
  }

  $head = substr($resp_str, 0, $sep_pos);

  preg_match('/HTTP[^\s]+\s(\d+)/i', $head, $matches);
  $this->resp_code = $matches[1];

  preg_match('/Content-Length:\s(\d+)/i', $head, $matches);
  $length = intval($matches[1]);

  $body = substr($resp_str, $sep_pos + $sep_len, $length);

  return $body;
 }
}

Функции __construct() и __destruct() соответственно создают и закрывают сокет. Функции reqGet() и reqPost() получают URL в качестве аргумента и выполняют соответствующие запросы.

Пару слов о HTTP заголовках:
  1. Строка "Host: ..." сообщает на какой именно домен идёт запрос (на случай если на одном IP несколько доменов).
  2. Строка "Connection: Close" сообщает что соединение не должно считаться постоянным.
Ещё немного напишу о процессе разбора ответа в функции parseResponse():
  1. Сначала нужно отделить заголовок от основного сообщения, для этого ищу двойную новую строку. Это может быть как "\r\n\r\n", так и просто "\n\n".
  2. Далее сохраняю код ответа и выбираю информацию о длинне сообщения (Content-Length).
После этого уже выделяю само сообщение.