Skip to content

Instantly share code, notes, and snippets.

@zeromodule
Created August 31, 2015 22:20
Show Gist options
  • Save zeromodule/fd5d3bfd4a77ca2bd5a4 to your computer and use it in GitHub Desktop.
Save zeromodule/fd5d3bfd4a77ca2bd5a4 to your computer and use it in GitHub Desktop.
<?php
/**
* Класс для асинхронной работы с множеством одновременных HTTP-запросов
*
* @author Sharonov Anton
*/
class MultiHttp
{
// значение по умолчанию для таймаута соединения
const CONNECTTIMEOUT = 15;
// значение по умолчанию для таймаута ожидания ответа
const TIMEOUT = 15;
// метод по умолчанию
const DEFAULT_METHOD = "GET";
private $connecttimeout = null;
private $timeout = null;
private $method = null;
private $callback = null;
private $urls = array();
private $urlPrefix = "";
private $cmh = null;
private $curlOptions = array();
// эти опции нельзя устанавливать через метод setCurlOption, т.к. они устанавливаются другими способами
private $illegalCurlOptions = array(CURLOPT_URL, CURLOPT_POST, CURLOPT_POSTFIELDS);
private $validMethods = array("GET", "POST");
public function __construct()
{
$this->connecttimeout = self::CONNECTTIMEOUT;
$this->timeout = self::TIMEOUT;
$this->method = self::DEFAULT_METHOD;
}
/**
* Один сеттер для двух таймаутов
*
* @param type $seconds
*/
public function setTimeOuts($seconds)
{
$this->connecttimeout = $this->timeout = $seconds;
}
/**
* Устанавливает дополнительные опции для CURL
*
* @param type $option
* @param type $value
*/
final public function setCurlOption($option, $value)
{
if (!in_array($option, $this->illegalCurlOptions)) {
$this->curlOptions[$option] = $value;
} else {
throw new RuntimeException("Невозможно установить данную CURL опцию ($option) через метод setCurlOption.
Данная опция или её аналог устанавливается в другом месте другим способом");
}
}
final public function setMethod($method)
{
if (in_array($method, $this->validMethods)) {
$this->method = $method;
} else {
throw new RuntimeException("Неизвестный метод $method для выполнения HTTP-запроса");
}
}
/**
* Общий префикс для урлов
*
* @param type $p
*/
public function setUrlPrefix($p)
{
$this->urlPrefix = $p;
}
/**
* Сюда лучше всего передавать анонимную функцию (см. контроллер AutoOffline)
*
* @param type $cb
*/
public function setCallback($cb)
{
$this->callback = $cb;
}
/**
* Формат аргумента $urls должен быть следующим:
*
* array(
* array(
* "url" => "/what/i/need/to/get",
* "params" => array (
* "param1" => "value1",
* "param2" => "value2",
* "headers" => array(
* "header1" => "header1_value",
* "header2" => "header2_value"
* )
* ),
*
* array(
* "url" => ....,
* "params" => array( .... )
* ),
*
* ....
*
* );
*
* Внимание:
* - в ["url"] может как полный URL, так и его часть, если вы хотите использовать $this->urlPrefix
*
* - массив ["params"] нужен исключительно как механизм передачи параметров по каждому
* конкретному URL обратно в коллбек (там могут быть ID смс-сообщения, или номер телефона,
* или что вам угодно). ЭТО НЕ ПАРАМЕТРЫ GET/POST ЗАПРОСА ДЛЯ URL!!!
*
* Единственный зарезервированный элемент в массиве ["params"] - это ["headers"],
* туда можно положить HTTP заголовки для запроса. Пример использования всего этого смотрите в контроллере AutoOffline
*
* - ["params"] может быть пустым или вообще не существовать, равно как и ["params"]["headers"]
*
*
* @param array $urls
*/
public function setUrls(array $urls)
{
$this->urls = $urls;
}
public function exec()
{
// инициализируем "контейнер" для отдельных соединений (мультикурл)
$this->cmh = curl_multi_init();
// массив заданий для мультикурла
$tasks = array();
// массив настроек для урлов
$params = array();
// перебираем наши урлы
foreach ($this->urls as $url) {
// инициализируем отдельное соединение (поток)
if ($this->method == self::DEFAULT_METHOD) {
$ch = curl_init($this->urlPrefix . $url["url"]);
} elseif ($this->method == "POST") {
// так сделано для того, чтобы в случае с POST
// не менять входящие URL (или сводить их к одному)
// и не передавать параметры запроса в каком-нибудь отдельном массиве
$url_parts = parse_url($this->urlPrefix . $url["url"]);
$ch = curl_init(
$url_parts["scheme"] . "://" .
$url_parts["host"] .
$url_parts["path"]
);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $url_parts["query"]);
}
if (isset($url["params"]) && isset($url["params"]["headers"])) {
$curl_headers = array();
foreach ($url["params"]["headers"] as $header => $value) {
$curl_headers[] = "{$header}: {$value}";
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_headers);
}
// если будет редирект - перейти по нему
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
// возвращать результат
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// не возвращать http-заголовок
curl_setopt($ch, CURLOPT_HEADER, 0);
// таймаут соединения
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout);
// таймаут ожидания
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
foreach ($this->curlOptions as $option => $value) {
curl_setopt($ch, $option, $value);
}
// добавляем дескриптор потока в массив заданий
$tasks[$url["url"]] = $ch;
// сохраняем настройки по каждому урлу в отдельном массиве
$params[$url["url"]] = isset($url["params"]) ? $url["params"] : array();
// добавляем дескриптор потока в мультикурл
curl_multi_add_handle($this->cmh, $ch);
}
$act = 0;
do {
usleep(10000);
curl_multi_exec($this->cmh, $act);
$info = curl_multi_info_read($this->cmh);
if (!empty($info) && $info["msg"] == CURLMSG_DONE) {
$h = $info["handle"];
$url = array_search($h, $tasks);
if ($url) {
$this->processHandle($h, $url, $params);
unset($tasks[$url]);
}
}
} while ($act > 0);
foreach ($tasks as $url => $h) {
$this->processHandle($h, $url, $params);
}
curl_multi_close($this->cmh);
}
/**
* Обрабатывает хэндл - читает из него информацию и вызывает коллбек
*
* @param mixed $h
* @param string $url
* @param array $params
*/
private function processHandle($h, $url, $params)
{
$params_to_callback = $params[$url];
$params_to_callback['response_code'] = curl_getinfo($h, CURLINFO_HTTP_CODE);
$params_to_callback['request_url'] = $url;
$params_to_callback['curl_info'] = curl_getinfo($h);
// собственно получаем ответ сервера
$data = curl_multi_getcontent($h);
// вызываем коллбек, передаём в него ответ сервера и параметры URL
if (!empty($this->callback)) {
call_user_func($this->callback, $data, $params_to_callback);
}
curl_multi_remove_handle($this->cmh, $h);
curl_close($h);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment