вторник, 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).
После этого уже выделяю само сообщение.

Комментариев нет:

Отправить комментарий