Created
August 31, 2015 22:20
-
-
Save zeromodule/fd5d3bfd4a77ca2bd5a4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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