Guzzle — очень мощный и стабильный http-клиент. В отличие от обычных компонентов пакета cURL, Guzzle использует различные внутренние методы запросов для реализации HTTP-запросов.cURL — это наиболее часто используемый метод, а Guzzle предоставляет мощные асинхронные и параллельные функции, упрощающие создание http-запроса и расширяемые. Теперь, когда Guzzle интегрирован в основной модуль Drupal, его надежность очевидна. Guzzle в настоящее время использует спецификацию Psr7, а масштабируемость и совместимость также лучше. Дов записи о рефакторингеЯ упомянул об этом, но не анализировал его подробно. На этот раз я решил представить несколько примеров использования и подробно проанализировать лежащие в его основе принципы реализации. Если есть какие-либо проблемы, пожалуйста, оставьте сообщение, чтобы указать и добиться прогресса вместе.
Примечание. Чтобы свести к минимуму объем чтения, в некоторых анализах исходного кода перечислены только ключевые шаги.
окрестности
Версия Guzzle, используемая в этой статье, — 6.3.0, а содержимое файла composer.json —
{
"require": {
"guzzlehttp/guzzle": "^6.3"
}
}
настроить
Различные настройки Guzzle связаны с http-запросами, например, отслеживать ли переадресацию 302, хранить ли файлы cookie, использовать ли ssl, тайм-аут и т. д.
Элементы конфигурации передаются при создании объекта клиента в виде массива.Вся конфигурация здесь. Guzzle предоставит конфигурацию по умолчанию, которая будет объединена с пользовательской конфигурацией,и отдавать приоритет пользовательской конфигурации.
public function __construct(array $config = [])
{
$this->configureDefaults($config);
}
private function configureDefaults(array $config)
{
// 自定义配置和默认配置,在这里合并,并赋值给了成员变量
$this->config = $config + $defaults;
}
Например:
$config = [
'allow_redirects' => [
'max' => 5,
'referer' => true,
],
'http_errors' => false,
'decode_content' => true,
'cookies' => true,
'connect_timeout' => 1.5,
'timeout' => 2.5,
'headers' => [
'User-Agent' => 'test client for chengxiaobai.cn',
],
];
$client = new \GuzzleHttp\Client($config);
Вы также можете передать конфигурацию при создании запроса, который будет объединен с конфигурацией, переданной в конструкторе,и действует только для текущего запроса.
private function prepareDefaults($options)
{
$defaults = $this->config;
// 这里这是赋值给了局部变量,所以只对当前请求有效
$result = $options + $defaults;
return $result;
}
Например:
$client = new \GuzzleHttp\Client($config);
$client->request('GET', 'https://www.chengxiaobai.cn/',
[
'allow_redirects' => [
'max' => 1,
'referer' => false,
],
]);
специальный параметр обработчика
Параметры обработчика являются специальными,это должно быть закрытие, а параметрыPsr7\Http\Message\RequestInterface
иarray
параметр типа и должен возвращатьGuzzleHttp\Promise\PromiseInterface
или удовлетворить в случае успехаPsr7\Http\Message\ResponseInterface
.
Если вы описываете его с точки зрения объектной ориентации, вы должны реализовать такой интерфейс,Chengxiaobai\handler
.
interface Chengxiaobai
{
/**
* handler interface
*
* @param RequestInterface $request
* @param array $options
*
* @return Psr\Http\Message\ResponseInterface | GuzzleHttp\Promise\PromiseInterface
*/
public function handler(Psr\Http\Message\RequestInterface $request,array $options);
}
Это делает структуру обработчика очень понятной. Давайте посмотрим, как исходный код анализирует конфигурацию обработчика.
public function __construct(array $config = [])
{
if (!isset($config['handler'])) {
// 创建一个默认的 handler 栈
$config['handler'] = HandlerStack::create();
} elseif (!is_callable($config['handler'])) {
throw new \InvalidArgumentException('handler must be a callable');
}
}
Очевидно, что если вы настроите обработчик, он откажется от handlerStack, предоставляемого Guzzle по умолчанию. Если вы не уверены, пожалуйста, не действуйте по своему усмотрению.
Возьмем пример пользовательской операции обработчика, например возврата 404 для любого запроса.
$client = new \GuzzleHttp\Client($config);
$response = $client->request('GET', 'www.chengxiaobai.cn/history.html',
[
'handler' => function (\Psr\Http\Message\RequestInterface $request, array $options) {
return new \GuzzleHttp\Psr7\Response(404);
},
]);
echo $response->getStatusCode();// 404
Выше мы сказали, что сам Guzzle поставляется с некоторыми обработчиками.Давайте сначала посмотрим, что такое handlerStacks по умолчанию.Независимо от реализации в каждом обработчике, мы подробно обсудим их на этапе обработки запроса.
public static function create(callable $handler = null)
{
// 这里定义了底层请求实现方法
$stack = new self($handler ?: choose_handler());
// 下面都会添加一些 Middleware 中间件
$stack->push(Middleware::httpErrors(), 'http_errors');
$stack->push(Middleware::redirect(), 'allow_redirects');
$stack->push(Middleware::cookies(), 'cookies');
$stack->push(Middleware::prepareBody(), 'prepare_body');
return $stack;
}
Обратите внимание на метод Choose_handler, этот метод определяет базовый метод реализации запроса, через него мы можем иметь предварительное представление о базовом методе реализации Guzzle запроса, то есть через него отправляются все запросы. Очень важно внимательно смотреть на комментарии к исходному коду.
function choose_handler()
{
$handler = null;
// 判定 curl 方法,如果并发和常规 curl 同时存在
if (function_exists('curl_multi_exec') && function_exists('curl_exec')) {
// 注册并发 curl 为默认请求方式,常规 curl 为同步请求方式
$handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
} elseif (function_exists('curl_exec')) {
// 如果两种 curl 方法同时只有一个存在,则优先常规 curl
$handler = new CurlHandler();
} elseif (function_exists('curl_multi_exec')) {
$handler = new CurlMultiHandler();
}
// 如果 allow_url_fopen 开启
if (ini_get('allow_url_fopen')) {
$handler = $handler
// 已有 handler ? 再注册一个流处理 handler
? Proxy::wrapStreaming($handler, new StreamHandler())
// 否则只有流处理 handler
: new StreamHandler();
} elseif (!$handler) {
throw new \RuntimeException('GuzzleHttp requires cURL, the '
. 'allow_url_fopen ini setting, or a custom HTTP handler.');
}
return $handler;
}
После создания обработчика в стек добавится некоторое middleware, то есть middleware. Краткое введение, первый параметр функции push — замыкание, второй параметр — строка, имя промежуточного ПО, промежуточное ПО в основном состоит из замыканий, а некоторые промежуточные ПО могут быть вложены немного больше, что немного сложная, но неважно структура. Насколько сложная, по существу используется для обработки различных данных запроса, типа его структуры иChengxiaobai\handler
Такой же.
Необходимо глубокое понимание обработчиков и ПО промежуточного слоя.Нажмите здесь, чтобы увидеть официальный документ, я лично считаю, что необходимо хорошо разбираться в затворах, чтобы иметь хорошее представление о его дизайнерских идеях.
Согласно приведенному выше анализу исходного кода, вы можете заметить, что обработчик, предоставляемый системой по умолчанию, существует в виде объекта. Но когда он действительно используется, он используется как замыкание Вот структура замыкания, которая действительно работает, а не поверхностный объект HandlerStack. Раздел «Обработка запросов» будет подробно описан далее.
запрос на сборку
На самом деле, в борьбе со всеми запросами асинхронные, синхронный запрос — это просто запрос на возврат результата сразу после построения асинхронного запроса, а асинхронный — на синхронный. Но построение как асинхронного, так и синхронного запроса похоже, и я объясню различия.
public function request($method, $uri = '', array $options = [])
{
$options[RequestOptions::SYNCHRONOUS] = true;
// requestAsync 就是异步请求,不过直接调用了 wait 转同步
return $this->requestAsync($method, $uri, $options)->wait();
}
запросить параметр uri
Если вы определяете параметр base_uri в конфигурации, вы можете использовать относительный адрес в это время. Если нет, относительный адрес не поддерживается. Guzzle не поможет вам проверить правильность конечного параметра uri. Вы можете только узнать, является ли uri правильный после отправки запроса.
private function buildUri($uri, array $config)
{
// for BC we accept null which would otherwise fail in uri_for
$uri = Psr7\uri_for($uri === null ? '' : $uri);
if (isset($config['base_uri'])) {
$uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri);
}
// 这里使用了 psr7 规范,返回的是一个实现了 UriInterface 的对象
return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
}
Например, это выдаст ошибку
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', '/history.html');
/**
* ountput :
* Fatal error: Uncaught GuzzleHttp\Exception\RequestException:
* cURL error 3: <url> malformed (see http://curl.haxx.se/libcurl/c/libcurl-errors.html)
* in /app/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php on line 187
*/
Подробные правила см.RFC 3986, section 2.Чиновник помог нам организовать несколько примеров быстрого понимания. Я разобрал здесь 4 ситуации.
base_uri | uri | result |
---|---|---|
chengxiaobai.cn/first/ | /second | chengxiaobai.cn/second |
chengxiaobai.cn/first/ | second | chengxiaobai.cn/first/second |
chengxiaobai.cn/first | /second | chengxiaobai.cn/second |
chengxiaobai.cn/first | second | chengxiaobai.cn/second |
Страховая ситуация заключается в том, чтобы каждый раз использовать абсолютный путь, но иногда относительный путь очень полезен при сканировании, и он используется в соответствии с реальными потребностями.
запрос на сборку
Объекты запроса, используемые Guzzle внутри:Psr\Http\Message\RequestInterface
Реализация , поэтому, если вы можете следовать спецификации psr7, чтобы легко расширять Guzzle.
Здесь еще раз напоминаю всем, что разработка современного php должна следовать спецификации psr, что способствует лучшему сотрудничеству и стабильному развитию сообщества.
public function requestAsync($method, $uri = '', array $options = [])
{
$request = new Psr7\Request($method, $uri, $headers, $body, $version);
return $this->transfer($request, $options);
}
богатый запрос
тип структуры передачи иChengxiaobai\handler
Такой же.
private function transfer(RequestInterface $request, array $options)
{
// 这个方法会根据你的请求类型,构建更具体的请求对象
$request = $this->applyOptions($request, $options);
$handler = $options['handler'];
}
applyOptions Как видно из названия, этот метод создаст соответствующий объект запроса в соответствии с вашей конфигурацией. Например, в соответствии с различными типами запросов выполните кодирование параметров, задайте тело, такое как json, поток, и задайте детали запроса, такие как заголовок.
Обратите внимание, что переданная конфигурация является ссылкой, поэтому любое изменение конфигурации повлияет на последующие операции.
private function applyOptions(RequestInterface $request, array &$options)
{
// 各种判定,修改 $options,如果有没覆盖到的,会新生成一个 $modify 说明需要重新构建 $request
// 构建新的对象方法
$request = Psr7\modify_request($request, $modify);
return $request;
}
Если нечего изменять, он вернется напрямую, если есть, он перестроит новый объект запроса.
Обратите внимание на обязательные параметры, некоторые параметры сборки взяты из$changes
взято, но некоторые из оригинала$request
Суть выделения объекта в том, что если есть новый, то будет использован новый, а если нет, то старый останется без изменений.
function modify_request(RequestInterface $request, array $changes)
{
if (!$changes) {
return $request;
}
return new Request(
isset($changes['method']) ? $changes['method'] : $request->getMethod(),
$uri,
$headers,
isset($changes['body']) ? $changes['body'] : $request->getBody(),
isset($changes['version'])
? $changes['version']
: $request->getProtocolVersion()
);
}
Введение в обещания
Что касается обещаний, то оно принадлежитguzzlehttp/promises
Библиотека классов - это библиотека классов, которую стоит изучить. Я проанализирую принцип ее реализации, когда у меня будет возможность. В настоящее время мы по-прежнему сосредоточены на анализе процесса реализации запроса.
Глядя на исходный код, вы обнаружите, что, хотя Guzzle использует много промисов и усложнен замыканиями, промисы играют ту же роль. В настоящее время можно понять, что обещание — это конечный автомат, который имеет три состояния: ожидание, удовлетворение и отклонение.
Следующий пример — это всего лишь пример того, как он будет выполняться Спецификация промиса имеет различные требования.Обещания/спецификация A+, обещание, используемое Guzzle, также является реализацией этой спецификации.
$promise = new Promise(
function () {
echo 'wait';
},
function () {
echo 'cancle';
}
);
$promise->then(
function () {
echo 'onFulfilled';
},
function () {
echo 'onRejected';
}
)->then(
function () {
echo 'onFulfilled';
},
function () {
echo 'onRejected';
}
);
Начать выполнение из состояния ожидания, выполнить onFulfilled при удовлетворении, выполнить onRejected при отклонении и выполнить ряд различных методов в зависимости от разных состояний, с HTTP-запросом либо успешно, либо неудачно, сценария третьего состояния не будет, просто его можно легко понял.
private function transfer(RequestInterface $request, array $options)
{
$handler = $options['handler'];
try {
return Promise\promise_for($handler($request, $options));
} catch (\Exception $e) {
return Promise\rejection_for($e);
}
}
Успех — это обещание_для, а неудача — отклонение_для.
promise_for
Этот метод в основном используется для того, чтобы гарантировать, что возвращаемый объект является обещанием, потому что значение, обрабатываемое $handler, может быть объектом обещания (обработка $handler будет обсуждаться позже), объектом ответа или исключением, поэтому вы необходимо выполнить «очищающее преобразование» данных и вернуть обещание, удовлетворяющее состоянию.
function promise_for($value)
{
// 如果是一个 promise 对象就直接返回
if ($value instanceof PromiseInterface) {
return $value;
}
// 如果是一个包含 then 方法的对象,会把它转换成一个 promise 对象
if (method_exists($value, 'then')) {
// 如果里面有 wait、cancel、resolve、reject 等方法,会把它添加进去作为默认方法,否则置为 null
$wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null;
$cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null;
$promise = new Promise($wfn, $cfn);
$value->then([$promise, 'resolve'], [$promise, 'reject']);
return $promise;
}
// 前俩者都不满不足的情况下,直接返回一个满足状态的 promise。
return new FulfilledPromise($value);
}
rejection_for
Исключения попадают в метод rejection_for. Точно так же выполняется «очистка данных» и возвращается промис со статусом отклонено.
function rejection_for($reason)
{
if ($reason instanceof PromiseInterface) {
return $reason;
}
return new RejectedPromise($reason);
}
обработка обработчика
Или метод передачи, перед тем как передать его в promise_for, вызывается $handler, который является функцией-обработчиком в конфигурации. Следующим шагом является возврат объекта Promise для внешнего асинхронного вызова.
private function transfer(RequestInterface $request, array $options)
{
$handler = $options['handler'];
try {
// 这里会先调用配置中的 handler 方法
return Promise\promise_for($handler($request, $options));
} catch (\Exception $e) {
return Promise\rejection_for($e);
}
}
обработать запрос
Для раздела обработки обработчика выше у вас могут возникнуть сомнения, почему вызывается функция обработчика, разве это не начинает обработку запроса напрямую?
Ранее мы представили структуру данных обработчика, который представляет собой объект handlerStack, но его вызов, по сути, представляет собой серию комбинированных замыканий. Но структура данных — это объект, как она может стать замыканием при использовании?
При попытке вызвать объект способом, вызывающим функцию,__invoke()Метод будет вызван автоматически.
Исходя из этого, давайте посмотрим на исходный код handlerStack.
Руководитель из названия, мы можем знать, что это структура данных «стеки» для удовлетворения функций «Last In, Fiar Out».
public function __invoke(RequestInterface $request, array $options)
{
// 这个函数主要是实现 Middleware 中间件操作
$handler = $this->resolve();
//这里下面紧接着会有分析
return $handler($request, $options);
}
public function resolve()
{
// 变量缓存,能优化部分性能
if (!$this->cached) {
// 这个 handler 就是之前选择的 实现请求的底层方法
// 如果没有的话,请求都无法实现,就别折腾了,抛个异常终止吧
if (!($prev = $this->handler)) {
throw new \LogicException('No handler has been specified');
}
// 反转顺序,实现”后进先出“特性,调用每个中间件
foreach (array_reverse($this->stack) as $fn) {
// 中间件的注册是 [$middleware, $name] 形式的
// 所以取第一个元素是其具体实现,第二个参数只是名字
// 调用第一次传入的是 handler,后续传入的就是上一次处理的结果
$prev = $fn[0]($prev);
}
// 所有的都处理完毕,缓存起来
$this->cached = $prev;
}
return $this->cached;
}
Выше приведена очень классическая реализация модели промежуточного программного обеспечения, реализация в laravel немного отличается, в основном используетсяarray_reduce
Эта функция, но принцип аналогичен, знайте, что ее принцип все-в-одном.
Продолжим смотреть на исходный код. Этот метод все тот же, но мы разбираем реализацию его финального вызова.
Согласно блок-схеме Middleware, мы знаем, что последним вызовом является http_errors, давайте проанализируем его, нет никакой особенности, другие структуры Middleware одинаковы, но некоторые middleware используются несколько раз.__invoke()
Просто магический метод.
Структура замыкания в промежуточной программе сложна, хорошо понимаю.
public function __invoke(RequestInterface $request, array $options)
{
// 这个函数主要是实现 Middleware 中间件操作
$handler = $this->resolve();
// 现在我们分析这个
return $handler($request, $options);
}
public static function httpErrors()
{
// 第一次调用返回!传入一个 闭包-A
return function (callable $handler) {
// 第二次调用返回!传入 $request,$options
return function ($request, array $options) use ($handler) {
// Middleware 自己的逻辑判定返回什么样的闭包
if (empty($options['http_errors'])) {
// 第三次调用返回!返回 闭包-A 的处理结果
// 这里根据配置 没有注册 then 函数,直接进行下一步处理
return $handler($request, $options);
}
// 第三次调用返回!返回 闭包-A,附加 promise
// 根据上面我们说到的 promise 特性,这里用 then
// 附加了 闭包-A 处理完毕之后要调用的逻辑
return $handler($request, $options)->then(
function (ResponseInterface $response) use ($request, $handler) {
$code = $response->getStatusCode();
if ($code < 400) {
return $response;
}
throw RequestException::create($request, $response);
}
);
};
};
}
Что касается количества возвращенных слоев, вы можетеreturn
быстро найти, аreturn
Это соответствует призыву вернуться.
Теперь давайте разберемся, сколько раз handlerStack вызывается на этом шаге, и узнаем, где вызываются три уровня замыканий, что поможет нам получить окончательный результат.
// 第一次
public static function create(callable $handler = null)
{
$stack->push(Middleware::httpErrors(), 'http_errors');
}
// 第二次
public function resolve()
{
$prev = $fn[0]($prev);
}
// 第三次
public function __invoke(RequestInterface $request, array $options)
{
$handler($request, $options);
}
Конечный результат должен быть, если по структуре Middleware должен быть таким:
$handler($request, $options)->then('http_errors')
->then('allow_redirects')
->then('cookies')
->then('prepare_body')
это$handler
Это первый входящий запрос, реализующий базовый метод.
Реализовано все Middleware.При поступлении сначала обрабатываются данные запроса.После выполнения запроса снова обрабатывается результат запроса.
Уведомление! ! ! Из-за разных ролей ПО промежуточного слоя некоторые ПО промежуточного слоя могут не обрабатывать результат запроса, поэтому функция then не будет зарегистрирована. Здесь описан весь процесс Middleware, и ни по одному из них не делается специального анализа, потому что сценарии спроса разные, и логика обработки немного изменится.
это$handler
Какой метод запроса? все еще помнюchoose_handler()
метод? Он определяет, какой базовый метод используется для реализации запроса. Теперь мы, наконец, выполняем шаг инициирования запроса.
оглядыватьсяchoose_handler()
метод.
function choose_handler()
{
$handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
$handler = new CurlHandler();
$handler = new CurlHandler();
$handler = $handler
? Proxy::wrapStreaming($handler, new StreamHandler())
: new StreamHandler();
return $handler;
}
Оба эти метода имеют анализ исходного кода, и вы можете вернуться и посмотреть, если у вас нет впечатления.
разные$handler
все в__invoke()
Поднимите шумиху вокруг метода.
Мы анализируем первую$handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
.
public static function wrapSync(
callable $default,
callable $sync
) {
return function (RequestInterface $request, array $options) use ($default, $sync) { // 注意这里的三目运算符,判定同步请求选项是否为空
return empty($options[RequestOptions::SYNCHRONOUS])
// 默认是并发请求 new CurlMultiHandler()
? $default($request, $options)
// 这里是同步请求 new CurlHandler()
: $sync($request, $options);
};
}
Теперь, наконец, проводится различие между асинхронными и синхронными запросами. Сначала рассмотрим синхронные запросы.
синхронный запрос
Давайте рассмотримrequest()
метод, отметив шаг.
public function request($method, $uri = '', array $options = [])
{
// 这里往配置中添加了一个选项,设置该请求为同步的
$options[RequestOptions::SYNCHRONOUS] = true;
}
Итак, вот синхронный запрос, разберемCurlHandler()
.
public function __invoke(RequestInterface $request, array $options)
{
// 如果设置了延迟请求,会在这里阻塞一会
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
// 创建一个 handler 抽象对象
$easy = $this->factory->create($request, $options);
// 执行
curl_exec($easy->handle);
$easy->errno = curl_errno($easy->handle);
// 请求处理结束
return CurlFactory::finish($this, $easy, $this->factory);
}
Здесь нам нужно проанализировать фабричный классCurlFactory
,Большинство из них связаны с некоторыми конфигурациями cURL.Если вам интересно, вы можете прочитать исходный код, чтобы узнать.В официальной документации о значении конфигурации есть специальное введение.Я не буду их здесь анализировать, а анализ основных процесса не будет хватать.
public function create(RequestInterface $request, array $options)
{
if (isset($options['curl']['body_as_string'])) {
$options['_body_as_string'] = $options['curl']['body_as_string'];
unset($options['curl']['body_as_string']);
}
// handle 的一个抽象对象
$easy = new EasyHandle;
$easy->request = $request;
$easy->options = $options;
// 获取默认配置
$conf = $this->getDefaultConf($easy);
// 解析请求方法
$this->applyMethod($easy, $conf);
// 解析配置
$this->applyHandlerOptions($easy, $conf);
// 解析头部
$this->applyHeaders($easy, $conf);
unset($conf['_headers']);
// 解析自定义 curl 配置
if (isset($options['curl'])) {
$conf = array_replace($conf, $options['curl']);
}
// 设置回调函数用于处理返回头
$conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
// 从 handle池 获取一个 handle,没有就新建一个
$easy->handle = $this->handles
? array_pop($this->handles)
: curl_init();
curl_setopt_array($easy->handle, $conf);
return $easy;
}
public static function finish(
callable $handler,
EasyHandle $easy,
CurlFactoryInterface $factory
) {
// 这里会调用配置用设置的 on_stats 函数
if (isset($easy->options['on_stats'])) {
self::invokeStats($easy);
}
// 有错误的话走错误处理流程
if (!$easy->response || $easy->errno) {
return self::finishError($handler, $easy, $factory);
}
// 释放资源,还到 handle池
$factory->release($easy);
// 处理 流数据
$body = $easy->response->getBody();
if ($body->isSeekable()) {
$body->rewind();
}
// 返回一个满足状态的 promise
return new FulfilledPromise($easy->response);
}
Согласно анализу исходного кода, синхронный запрос уже выдал запрос на этом шаге, а обратный вызов в конфигурацииon_stats
Функция получила исходное возвращаемое значение необработанного возвращаемого значения, и синхронизация запрашивает пул обработчиков, который является дескриптором мультиплексного запроса как 3, это не изменено, прописано в коде.
асинхронный запрос
public function __invoke(RequestInterface $request, array $options)
{
$easy = $this->factory->create($request, $options);
// 为每个请求生成一个 ID
$id = (int) $easy->handle;
// 注册一个 promise,分别是调用执行和关闭方法
$promise = new Promise(
[$this, 'execute'],
// 依据 ID 来关闭请求
function () use ($id) { return $this->cancel($id); }
);
// 添加请求 底层是 curl_multi_add_handle 方法
$this->addRequest(['easy' => $easy, 'deferred' => $promise]);
return $promise;
}
Заводской классCurlFactory
Он был проанализирован выше и не будет повторяться здесь. Однако асинхронный запрос не инициирует окончательный запрос в это время.Сначала для каждого запроса генерируется идентификатор, а затем запрос добавляется в дескриптор пакетного ответа ( curl_multi_add_handle ), и, наконец, возвращается объект Promise, который зарегистрированexecute
функция иcancel
Функции, используемые для инициирования и последующего закрытия запросов.
Следует отметить, что запрос на отложенное выполнение устанавливается вaddRequest()
Метод лечения. За «результатом возврата» будет упомянут раздел обработки запроса задержки.
потоковая обработка
Если опция потока не пуста в конфигурации, она будет включена, если у вас нет cURL, вы можете только использовать ее.
public function __invoke(RequestInterface $request, array $options)
{
// 如果设置了延迟请求,会在这里阻塞一会
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
// 流处理本身信息较少,所以为了补全一些信息,这里记录处理开始时间
$startTime = isset($options['on_stats']) ? microtime(true) : null;
try {
// 不支持 expect header.
$request = $request->withoutHeader('Expect');
// 当内容为0的时候,依然添加一个头信息
if (0 === $request->getBody()->getSize()) {
$request = $request->withHeader('Content-Length', 0);
}
// 发起请求,然后回调 on_stats 函数
// 解析结果,同样返回一个满足状态的 promise
return $this->createResponse(
$request,
$options,
$this->createStream($request, $options),
$startTime
);
} catch (\InvalidArgumentException $e) {
throw $e;
} catch (\Exception $e) {
// Determine if the error was a networking error.
$message = $e->getMessage();
// This list can probably get more comprehensive.
if (strpos($message, 'getaddrinfo') // DNS lookup failed
|| strpos($message, 'Connection refused')
|| strpos($message, "couldn't connect to host") // error on HHVM
) {
$e = new ConnectException($e->getMessage(), $request, $e);
}
$e = RequestException::wrapException($request, $e);
$this->invokeStats($options, $request, $startTime, null, $e);
return \GuzzleHttp\Promise\rejection_for($e);
}
}
Что касается потоковой обработки, поскольку ее основная реализацияfopen()
функция, которая поддерживает множество протоколов, не только http, этоПоддерживаемые протоколы и протоколы инкапсуляции здесьКак видите, Guzzle выполняет специальную обработку для удовлетворения потребностей бизнеса.
вернуть результат
Из приведенного выше анализа мы уже знаем, чтоtransfer
Какой результат возвращает метод, а затем получить возвращенный результат.
синхронный запрос
синхронный запрос, потому что вtransfer
В методе фактический запрос был отправлен, и был получен необработанный исходный результат возврата.
public function send(RequestInterface $request, array $options = [])
{
// 我们注意到最后调用的 wait 方法
return $this->sendAsync($request, $options)->wait();
}
В методе синхронного запроса, вызываемом напрямуюwait()
метод, поэтому перейдите непосредственно к объекту Promisewait()
способ и регистрацияthen()
метод. Я до сих пор помню, что какое-то промежуточное ПО было зарегистрировано в предыдущемthen()
метод? Тут главное вызвать их, выполнить шаг "обработки возвращаемого результата" мидлвара, ну и конечно же прописать в логике обработки некоторыеthen()
метод, и примеры здесь не приводятся.
асинхронный запрос
Асинхронный запрос вtransfer
То, что возвращается в методе, это обещание, а фактический запрос не отправлен в это время. Мы используем официальный пример, чтобы проанализировать способ отправить запрос и получить возвращенный результат.
$promise = $client->requestAsync('GET', 'https://www.chengxiaobai.cn');
$promise->then(
function (ResponseInterface $res) {
echo $res->getStatusCode() . "\n";
},
function (RequestException $e) {
echo $e->getMessage() . "\n";
echo $e->getRequest()->getMethod();
}
);
Этот способ заключается в регистрации отдельно для каждого асинхронного запроса.then()
метод, указывающий, как обрабатывать запрос в случае успеха и как обрабатывать его в случае сбоя.
$client = new Client(['base_uri' => 'https://www.chengxiaobai.cn']);
// 注册多个异步请求,实现并发
$promises = [
'image' => $client->getAsync('/image'),
'png' => $client->getAsync('/image/png'),
'jpeg' => $client->getAsync('/image/jpeg'),
'webp' => $client->getAsync('/image/webp')
];
// 有一个失败就终止
$results = Promise\unwrap($promises);
// 忽略某些请求的异常,保证所有请求都发送出去
$results = Promise\settle($promises)->wait();
Это делается для того, чтобы установить несколько асинхронных запросов для достижения параллелизма и выбрать, следует ли игнорировать некоторые ошибки запроса или нет.
$client = new Client();
$requests = function ($total) use ($client) {
for ($i = 1; $i < $total; $i++) {
$uri = 'https://www.chengxiaobai.cn/page/' . $i;
// 这里用到了协程
yield function() use ($client, $uri) {
return $client->getAsync($uri.$i);
};
}
};
$pool = new Pool($client, $requests(10), [
// 并发数
'concurrency' => 5,
'fulfilled' => function ($response, $index) {
echo $res->getStatusCode() . "\n";
},
'rejected' => function ($reason, $index) {
echo $e->getMessage() . "\n";
},
]);
// 初始化 Promise
$promise = $pool->promise();
// 发起请求处理
$promise->wait();
Это делается для того, чтобы сделать пакетную обработку больших пакетных запросов аналогично концепции пула запросов, задать скорость экспорта (параллелизм) и использовать унифицированную логику обработки для обработки данных в пуле запросов.
Давайте проанализируем исходный код Пула, в основном конструктор.
public function __construct(
ClientInterface $client,
$requests,
array $config = []
) {
// 设定请求池大小
if (isset($config['pool_size'])) {
$config['concurrency'] = $config['pool_size'];
} elseif (!isset($config['concurrency'])) {
// 默认并发数 25
$config['concurrency'] = 25;
}
if (isset($config['options'])) {
$opts = $config['options'];
unset($config['options']);
} else {
$opts = [];
}
// 将请求列表转换为一个迭代器
$iterable = \GuzzleHttp\Promise\iter_for($requests);
$requests = function () use ($iterable, $client, $opts) {
// 遍历请求列表
foreach ($iterable as $key => $rfn) {
// 如果是一个 request 的实现,转换为一个异步请求
if ($rfn instanceof RequestInterface) {
yield $key => $client->sendAsync($rfn, $opts);
} elseif (is_callable($rfn)) {
// 如过是一个闭包,直接调用
yield $key => $rfn($opts);
} else {
throw new \InvalidArgumentException('...');
}
}
};
// 支持迭代的 Promise 对象
$this->each = new EachPromise($requests(), $config);
}
Мы видим, что в режиме пула все запросы настроены$opts
одинаковы, поэтому логика обработки каждого запроса одинакова. Если каждый запрос имеет индивидуальные требования, режим пула может не подойти. Конечно, вы можете использовать метод модификации исходного кода, но это не соответствует Режим «Бассейн» Первоначальный замысел дизайна.
Независимо от формы можно обнаружить, что окончательный вызов вызываетсяwait()
метод. Это связано со спецификацией Promise.
Давайте посмотрим, как асинхронно обрабатываются запросы.
Помните обещания, возвращаемые асинхронными запросами?
$promise = new Promise(
[$this, 'execute'],
// 依据 ID 来关闭请求
function () use ($id) { return $this->cancel($id); }
);
wait()
Вызов метода[$this, 'execute']
, давайте проанализируем его реализацию. Перед этим нам нужно указать запрос задержки.
задерживать
Для отложенных запросов синхронные запросы и потоковые запросы легко обрабатывать, и их можно заблокировать напрямую.Если в 20 асинхронных запросах есть 10 отложенных запросов, и каждое время задержки не одинаково, обработка отложенных запросов должна быть тщательно продумана. в это время вниз.
В главе «Обработка запросов» мы говорили, что отложенный запрос не сразу добавляется в дескриптор пакетного запроса, он временно сохраняется в$this->delays
в очереди. Пока вы не решите инициировать запрос, отложенный запрос не извлекается, чтобы вычислить, следует ли его добавить в дескриптор пакетного запроса. Логика расчета Давайте посмотрим, как посчитать время блокировки из исходного кода.
public function execute()
{
$queue = P\queue();
while ($this->handles || !$queue->isEmpty()) {
// 如果没有在进行的请求,并且延迟请求队列不为空,就开始阻塞
if (!$this->active && $this->delays) {
usleep($this->timeToNext());
}
$this->tick();
}
}
private function timeToNext()
{
$currentTime = microtime(true);
$nextTime = PHP_INT_MAX;
// 找出现有延迟请求队列中最小的延迟时间
foreach ($this->delays as $time) {
if ($time < $nextTime) {
$nextTime = $time;
}
}
return max(0, $nextTime - $currentTime) * 1000000;
}
execute
В основном называетсяtick()
Сюда.
public function tick()
{
// 如果延迟请求队列不为空,处理延迟请求
if ($this->delays) {
$currentTime = microtime(true);
// $this->delays[$id] = microtime(true) + ($easy->options['delay'] / 1000);
foreach ($this->delays as $id => $delay) {
// 延迟任务已经达到延迟预期时间,开始处理
if ($currentTime >= $delay) {
// 将它从延迟任务队列中删除
unset($this->delays[$id]);
// 添加到批量请求句柄中
curl_multi_add_handle(
$this->_mh,
$this->handles[$id]['easy']->handle
);
}
}
}
// 执行队列中的任务
P\queue()->run();
// 执行请求
if ($this->active &&
curl_multi_select($this->_mh, $this->selectTimeout) === -1
) {
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
while (curl_multi_exec($this->_mh, $this->active) === CURLM_CALL_MULTI_PERFORM);
// 获取请求结果信息,移除请求成功的请求
$this->processMessages();
}
Тогда асинхронный процесс очень ясен:
- Если очередь задержки запроса не является пустой и в настоящее время не находится в запросе за выполнением, минимальное время задержки перед блокированием, задержка очереди запроса, чтобы убедиться, что каждый запрос не менее одного потребления. Если запрос выполняется или очередь запроса задержки не пусто, 2 прямого выполнения.
- Сделайте пакетный запрос.
- Получить информацию о запросе и удалить успешные запросы.
- Если очередь запросов не пуста, перейдите к 1-3.
Из приведенного выше процесса мы можем проанализировать, что даже если ваш параллелизм превышает количество запросов, это не означает, что вы запрашиваете только один раз, и могут быть повторные попытки или отложенные запросы, приводящие к нескольким запросам. И согласно шагу 1 мы также можем знать, что неотложенные задачи также будут заблокированы вместе с ним.
Как и в случае с синхронными запросами, после обработки каждого запроса в рамках асинхронных запросов соответствующийthen()
Способ завершает обработку возвращенного результата.
Запрос трансляции
Поскольку потоковые запросы в основном основаны наfopen
Да, логика инициирования запроса относительно проста.
public function __invoke(RequestInterface $request, array $options)
{
// 延迟请求直接 delay 操作
if (isset($options['delay'])) {
usleep($options['delay'] * 1000);
}
// 重点1:解析返回值
return $this->createResponse(
$request,
$options,
// 重点2:发起请求
$this->createStream($request, $options),
$startTime
);
}
Давайте сначала рассмотрим, как инициировать запрос, сосредоточив внимание на обработке элементов конфигурации.
private function createStream(RequestInterface $request, array $options)
{
$params = [];
// 这里设置了默认请求参数
$context = $this->getDefaultContext($request);
// 这里方法主要是依据配置项调用了
// add_proxy,add_timeout,add_verify,add_cert,add_progress,add_debug
// 其实本质上就是用自定义配置覆盖默认请求参数
if (!empty($options)) {
foreach ($options as $key => $value) {
$method = "add_{$key}";
if (isset($methods[$method])) {
$this->{$method}($request, $context, $value, $params);
}
}
}
// 这里也是用自定义配置覆盖默认请求参数
if (isset($options['stream_context'])) {
if (!is_array($options['stream_context'])) {
throw new \InvalidArgumentException('stream_context must be an array');
}
$context = array_replace_recursive(
$context,
$options['stream_context']
);
}
// 解析 host ,支持强制 IP 解析,v4 和 v6 都支持
$uri = $this->resolveHost($request, $options);
$context = $this->createResource(
function () use ($context, $params) {
// 这里创建资源流
return stream_context_create($context, $params);
}
);
return $this->createResource(
function () use ($uri, &$http_response_header, $context, $options) {
// 这里发起请求
$resource = fopen((string) $uri, 'r', null, $context);
$this->lastHeaders = $http_response_header;
// 设置超时时间
if (isset($options['read_timeout'])) {
$readTimeout = $options['read_timeout'];
$sec = (int) $readTimeout;
$usec = ($readTimeout - $sec) * 100000;
stream_set_timeout($resource, $sec, $usec);
}
return $resource;
}
);
}
Из кода https включен по умолчанию.
Слияние пользовательской конфигурации и конфигурации по умолчанию здесь больше не является простой операцией слияния массивов, поскольку модификация определенной конфигурации может включать изменения в других элементах конфигурации, поэтому несколько основных опций(proxy,timeout,verify,cert,progress,debug)
в упаковке.
после всегоfopen
Целью этой мощной функции в начале ее разработки является управление ресурсами, поэтому ее элементы конфигурации также различаются в зависимости от ресурса.Его элементы конфигурации для http можно увидеть здесь.
Затем идет обработка возвращаемого значения.Если вы используете cURL, их можно легко обработать, но при потоковой обработке вам придется анализировать их самостоятельно, что эквивалентно выполнению части работы cURL самостоятельно.
private function createResponse(
RequestInterface $request,
array $options,
$stream,
$startTime
) {
$hdrs = $this->lastHeaders;
$this->lastHeaders = [];
$parts = explode(' ', array_shift($hdrs), 3);
$ver = explode('/', $parts[0])[1];
$status = $parts[1];
$reason = isset($parts[2]) ? $parts[2] : null;
// 解析 header
$headers = \GuzzleHttp\headers_from_lines($hdrs);
// 解析返回类型
list ($stream, $headers) = $this->checkDecode($options, $headers, $stream);
// 构建一个 Psr7\StreamInterface 的 Stream 对象
$stream = Psr7\stream_for($stream);
$sink = $stream;
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
// 回调 on_headers 函数
if (isset($options['on_headers'])) {
try {
$options['on_headers']($response);
} catch (\Exception $e) {
$msg = 'An error was encountered during the on_headers event';
$ex = new RequestException($msg, $request, $response, $e);
return \GuzzleHttp\Promise\rejection_for($ex);
}
}
// 回调 on_stats 函数
$this->invokeStats($options, $request, $startTime, $response, null);
return new FulfilledPromise($response);
}
Весь процесс анализирует данные, и содержимое ответа передается черезstream_get_contents
получил, вPsr7\StreamInterface
отражено в примере.
здесь одинon_headers
эта функция. После получения возвращаемого заголовка эта функция определяет, как реагировать на следующие операции на основе информации в возвращаемом заголовке.Когда возвращаемые данные относительно велики, их можно перехватить заранее, чтобы избежать пустой траты ресурсов.
Этот параметр действителен для всех методов запроса, но он имеет большее значение при использовании в потоковой обработке.
$client->request('GET', 'http://httpbin.org/stream/1024', [
'on_headers' => function (ResponseInterface $response) {
if ($response->getHeaderLine('Content-Length') > 1024) {
throw new \Exception('The file is too big!');
}
}
]);
пасхальные яйца
В процессе анализа исходного кода был обнаружен неиспользуемый классGuzzleHttp\Promise\Coroutine
, это тоже реализация Promise, но реализуется через итераторы.Будет ли сопрограммная версия Promise? Подождем - увидим.
Эта работаЧенг Сяобайсоздавать, использоватьCreative Commons Attribution-ShareAlike 4.0
Международное лицензионное соглашениеС разрешения можно свободно воспроизводить и цитировать, но при этом необходимо поставить подпись автора и указать источник статьи.
Оригинальный адрес:Woohoo, Ченг Сяобай, Talent/PHP/guzzle-…