本文适合寻找 PHP HTTP 客户端库,或者对于 Guzzle 的使用和实现原理比较感兴趣的同学阅读,需要具备一定的 PHP 基础知识。

一、背景

在 PHP 后台开发过程中,经常会遇到模块间需要通过 HTTP 通信的情形。PHP 语言本身只提供了 socket 操作的接口,并未提供 HTTP 相关操作的接口。许多现有的实现采用 curl 扩展充当 HTTP Client 与 HTTP Server 通信,但仍需自己封装 curl 的接口。有鉴于此,本文介绍一款流行的 PHP HTTP Client 客户端 —-Guzzle(https://github.com/guzzle/guzzle/)的用法,深入分析其底层实现原理。

二、Guzzle 用法

例如使用 Guzzle 访问http://www.baidu.com 的代码:

  1. <?php
  2. $client = new \GuzzleHttp\Client();
  3. $response = $client->request('GET', 'http://www.baidu.com', [
  4. "timeout" => 3000
  5. ]);
  6. echo $response->getStatusCode(), "\n";
  7. echo $response->getBody();

接口封装是不是十分简单?只需要关心请求方法,目标 url 和请求的选项即可快速上手。同时,Guzzle 还支持异步请求方式:

<?php

use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;

$client = new \GuzzleHttp\Client();
$promise = $client->requestAsync('GET', 'http://www.baidu.com');
$promise->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
        echo $res->getBody();
        return $res;
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
)->wait();

基于异步请求,Guzzle 还实现了并发请求,关于 Guzzle 的具体使用方法可以参考其中文文档http://guzzle-cn.readthedocs.io/zh_CN/latest/index.html。

三、Guzzle 实现原理

1.client 构造

GuzzleHttp\Client 类构造函数声明为:

public function __construct(array $config = [])

$config 配置使得用户可以根据需要配置一切可以配置的选项,包括 allow_redirects、auth、connect_timeout、proxy 等。除此之外,还可以自定义请求的处理函数 handler,方便应用程序扩展,handler 接口规范为:

function handler($request, array $options);

处理成功时,接口返回 Psr\Http\Message\ResponseInterface;失败时返回 GuzzleHttp\Exception\RequestException 异常。

默认情形下,GuzzleHttp\HandlerStack::create 会创建请求处理函数

public static function create(callable $handler = null)
{
    $stack = new self($handler ?: choose_handler());
    $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;
}

create 函数以堆栈的形式创建了一系列的处理函数,包括 http 异常、重定向、cookie 和 prepare_body。处理函数返回的函数闭包为:

return function (callable $handler) {
    return function ($request, array $options) use ($handler) {
        ...
    };
};

函数入参为 handler,返回一个新的 handler,这样可以将所有的处理函数链接在一起,最终生成一个符合 handler 接口规范的函数.

choose_handler 函数选择 stack 中的起始 handler,选择策略为:

  • 扩展自带 curl_multi_exec 和 curl_exec 函数则根据 $options 中的 synchronous 选项决定,empty(synchronous) 为 false 则使用 CurlHandler,否则使用 CurlMultiHandler
  • 扩展只有 curl_exec 函数则使用 CurlHandler
  • 扩展只有 curl_multi_exec 函数则使用 CurlMultiHandler

最后,如果 php.ini 中开启了 allow_url_fopen,则根据 $options 中的 stream 选项决定,empty(stream) 为 false 则使用 StreamHandler。

2.client 调用 request 方法

request 方法实现为:

public function request($method, $uri = '', array $options = [])
{
    $options[RequestOptions::SYNCHRONOUS] = true;
    return $this->requestAsync($method, $uri, $options)->wait();
}

由此可见,request 事实上是采用了 requestAsync 异步方法 + wait 来完成的,也就是异步转同步。

2.1 requestAsync

requestAsync 将请求信息包装成 Psr7\Request 对象,然后调用

transfer(RequestInterface $request, array $options)

transfer 函数最终返回 Promise\promise_for(PHP HTTP客户端-Guzzle原理解析 - SegmentFault 思否 - 图1request, $options)); 其中 $handler 即为构造函数中所设置的 stack,stack 中存放一系列的请求处理函数。 HandlerStack 的处理函数为:

public function __invoke(RequestInterface $request, array $options)
{
    $handler = $this->resolve();

    return $handler($request, $options);
}

resolve 方法解析整个 stack,返回一个包装后的 handler,包装策略为按照出栈顺序包装,也就是

foreach (array_reverse($this->stack) as $fn) {
    $prev = $fn[0]($prev);
}

PHP HTTP客户端-Guzzle原理解析 - SegmentFault 思否 - 图2

典型的中间件模型,所有的处理函数串接在一起了。请求经由 http_errors、allow_redirects 等处理之后到达 Curl,执行真正的网络交互。

  • 对于同步的 handler 如 CurlHandler,在此处会执行 curl_exec 发起请求,最终返回的是 FulfilledPromise 对象或 RejectedPromise 对象,代表请求已经处理完毕。
  • 对于异步的 handler 比如 CurlMultiHandler,在此处并不会执行 curl_multi_exec,而是返回一个 promise 对象,里面注册了需要等待执行的 curl_multi_exec。

Curl Handler 处理完成之后,往上回溯,在 Allow_redirects 和 Http_errors 部分会进入 then 方法,最终返回的结果都是 promise 对象。

2.2 wait

请求发送完毕,进入 promise 的 wait 操作,最终会执行 promise 的 $waitFn 函数。

  • 对于 CurlMultiHandler,$waitFn 即执行 curlmultiexec 进行网络交互,然后调用 resolve 方法将 response 对象传递到 then 方法的 $onFulfilled 函数。
  • 对于 CurlHandler,直接利用 resolve 将 response 对象传递到 $onFulfilled 函数。

这样,异步的 then 方法设置的回调就可以接收到 response 了。 then 方法最终返回 response,这个对象又可以作为返回值返回,这样同步的 wait 就可以通过返回值来获取 response 对象了。

四、总结

本文重点介绍了 Guzzle 同步和异步请求的实现原理,除此之外,Guzzle 还提供了并行请求,请求 pool 等实现,读者可以在此基础上继续深入。

想要获取最新技术文章?欢迎订阅微信公众号 —— 软件编程之路

PHP HTTP客户端-Guzzle原理解析 - SegmentFault 思否 - 图3
https://segmentfault.com/p/1210000010203531/read