hyperframework WebClient 源码解读

date: 2017-11-10 11:44:33
title: hyperframework WebClient 源码解读

先说句题外话, 我在每篇 blog 上都会先加上 date, 然后一直把 blog 放到编辑器中, 之后不断做类似「提纲」类的记录, 最后找一个大段的时间书写.

这篇 blog 的起源, 来自于上周一个工作任务过程中的「坎坷」 -- 接某支付的 sdk, 返回一直报参数错误. 因为自己也接过不少支付的 sdk, 所以一直怀疑是 「签名错误」. 直到详细阅读 sdk 的源码和 hyperframework webclient 的源码, 才解开这个谜题. 应该有很多程序员大大和我一样, 会经常和 http 打交道, 希望这篇文章能有所帮助.

php 中 3 种 http 请求工具对比

这里对比的 3种 http 请求工具:

get 请求:

$url = 'www.example.com/curl.php?option=test';

// cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
// 可以合并成: $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);

// Guzzle
$client = new \GuzzleHttp\Client();
$response = $client->get($url);

// hyperframework webclient
$c = new \Hyperframework\Common\WebClient();
$r = $c->get($url);

post 请求:

$url = 'http://httpbin.org/post';

// cURL
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
echo $output;

// Guzzle
$response = $client->request('POST', $url, [
    'form_params' => [
        'field_name' => 'abc',
        'other_field' => '123',
        'nested_field' => [
            'nested' => 'hello'
        ]
    ]
]);

// hyperframework webclient
$c = new \Hyperframework\Common\WebClient();
$r = $c->post($url, [
    'field_name' => 'abc',
    'other_field' => '123',
]);

通过对比, 希望你能从 3 种「风格」中感受到工具各自的设计思想:

  • 都采用 「对象」 来完成一次 http 请求, 为什么? 因为从一次 http 请求的生命周期来看, 非常适合使用对象这个概念来处理
  • 抽象程度依次增加, cURL 需要你设置更多(意味着你需要知道更多细节), 对比 Guzzle 和 hyperframework webclient 可以发现 get 相差无几, 但是 post 上, Guzzle 多了一层 key 值来设置, 而 webclient 则把这层隐藏掉了

而我这次踩到的坑, 就和 post 的这层隐藏有关.

PS: 关于我说 cURL 也是「对象」的方式, 大家可以参考 swoole 的源码: 2层目录, 面对对象风格写 c (from swoole wiki)

hyperframework webclient 源码解读

源码在此: https://github.com/hyperframework/hyperframework/blob/master/lib/Common/WebClient.php

hyperframework webclient 源码解读起来非常容易, 也推荐大家也读一读看看, 可以帮助你看到一些 http 请求的细节.

解读源码, 尤其是 webclient 这样的单个类文件, 可以从 「生命周期」 的角度来试试, 会简单很多:

  • $c = new WebClien(); 实例化一个 WebClient 对象, __construct() 方法中可以设置初始化 $options
  • $r = $c->post($url, $data, $option); 执行一次 http 请求, 实际操作的方法 sendHttpRequest() -> send() -> initializeRequest(), 而这些方法本质做的是同一件事: 设置 $requestOptions
  • 执行 curl_setopt_array() 来使用 $requestOptions 中的值, 然后执行 curl_exec

这里有 2 个概念要明确: $options 属于实例级别, $requestOptions 属于请求级别. 多这样一层抽象出来, 就是为了方便对象的复用.

大家着重看一下 processDataRequestOption() 方法的代码, 这里有 post 请求的一些细节. 这里先说一下我踩到的坑:

private function processDataRequestOption() {
    if ($this->hasRequestOption(self::OPT_DATA) === false) {
        return;
    }
    $data = $this->getRequestOption(self::OPT_DATA);
    $defaultType = 'application/json';
    ...
}

可以看到, 这里默认会设置 Content-Type: application/json, 而我对接的某支付 sdk, 服务器那边必须要使用 application/x-www-form-urlencoded 才可以. 而由于我之前对接支付 sdk 的经历, 我一直纠结在签名错误上, 导致处理这个问题花了很久. 因为 Content-Type 设置错误, 导致服务器接受到的数据解析出错, 那当然会验签失败.

继续阅读 processDataRequestOption() 的源码, 下面会处理不同的 Content-Type, 而本质上, 就是在 处理字符串 而已.(处理字符串也是基本功呀.)

private function processDataRequestOption() {
    if ($this->hasRequestOption(self::OPT_DATA) === false) {
        return;
    }
    $data = $this->getRequestOption(self::OPT_DATA);
    $defaultType = 'application/json';
    $type = $this->hasRequestOption(self::OPT_DATA_TYPE) ?
        $this->getRequestOption(self::OPT_DATA_TYPE) : $defaultType;
    $typeSuffix = null;
    $position = strpos($type, ';');
    if ($position !== false) {
        $typeSuffix = substr($type, $position);
        $type = substr($type, 0, $position);
    }
    $lowercaseType = strtolower(trim($type));
    if (is_string($data)) {
        $this->addRequestHeader('Content-Type: ' . $type . $typeSuffix);
        $this->initializeCurlPostFieldOptions();
        $this->setRequestOption(CURLOPT_POSTFIELDS, $data);
        return;
    }
    if ($lowercaseType === 'multipart/form-data') {
        ...
    } elseif ($lowercaseType === 'application/x-www-form-urlencoded') {
        ...
    } elseif ($lowercaseType === $defaultType) {
        ...
    } else {
        throw new WebClientException(
            "Data type '$type' is not supported."
        );
    }
}

继续聊聊 Content-Type

Http Header里的Content-Type: https://www.cnblogs.com/52fhy/p/5436673.html

推荐大家读一下上面这篇博客, 结合 postman 实操来讲解 http header 中的 Content-Type, 理论 + 实践.

常用的 Content-Type 只有几种, 可以参照上面的源码解读:

  • application/json 对应 postman 中的 raw -> JSON, 随着 「大前端」 时代的到来以及文档型存储数据库的兴盛, json 格式普及率越来越高
  • multipart/form-data 对应 postman 中的 form-data, 格式最复杂, 可以用来上传文件
  • application/x-www-form-urlencoded 对应 postman 中的 x-www-form-urlencoded, 默认值, html form 表单提交默认就是这种格式
  • 还有 text/plain text/xml text/html 等几种, 对用 postman 中的 raw ->, 纯文本 / xml / html 都是常见的格式

通过源码细节可以知道, 不同的 Content-Type, 字符串格式是不一样的, 其中 multipart/form-data 最复杂, x-www-form-urlencoded 其实使用 php 中的 htp_build_query() 函数来格式化数据.

PS: 有没有和我一样, 一直以为 htp_build_query() 函数只是用来拼接 get 请求参数的, 所以还是要多读一些源码.

稍等, 到这里还没完, 这里只完成了 你按照一定格式组装好数据, 而对方接收到数据, 还需要 按照格式解析数据.

对 php 熟悉的小伙伴, 应该知道 php 中有 3 种方式接收 post 来的数据:

  • $_POST 数组, 也是最常见的方式, 不过大家使用框架的过程中, 会发现框架都会提供 Request::get('xxx') 这样类似的方法
  • file_get_contents('php://input'), 需要读取一些 原始 数据的时候, 通常是 $_POST 无法解析的数据
  • $HTTP_RAW_POST_DATA, 读过 php manual 就知道这是个 方法, 推荐使用 $_POST 来代替

之所以推荐框架中封装的 Request::get('xxx') 这样类似的方法, 是因为 $_POST 并不是每次都能处理好数据解析, 比如 json 数据. 而框架多了这一层抽象, 其中之一就是为了处理这种问题.

之前也写过比较上面三者的 blog, 当时给出了尽量使用 file_get_contents('php://input') 的结论. 这里着重说一下, 千万不要迷信.

迷信, 其实来自无知.

推荐阅读下面这篇 blog, php://input 是解析不了 form-data 格式的数据的, 这个问题让我使用 postman 测试 + php://input 设置断点 时一直返回为空时郁闷了很久

深入理解 php://input : http://www.nowamagic.net/academy/detail/12220520

数一数踩过的坑吧

其实在上面也列举了一部分, 这里总结一下, 方便大家查阅:

  • 对接某支付 sdk, 由于 Content-Type 错误, 导致签名一直失败, 最后通过阅读支付sdk 提供的 demo 以及 hyperframework WebClient 的源码解决
  • 之前维护过一个 nodejs 的项目, 出现端(IOS/Android)发起的请求均失败, 通过日志发现端这边使用的 form-data 格式提交的数据, 而 nodejs 这边使用的 koa2 框架, 默认解析 post 请求是支持 x-www-form-urlencoded 的. 事情到这里还没完, 为了支持 form-data 格式, 需要 npm 安装一个包, 但是当时在十九大期间, 连淘宝镜像源都无法安装这个包, 而因为深夜执行 npm 操作, 导致整个项目的包管理挂了, 最终服务器宕了. 最后通过以前备份的服务器镜像, 复制项目目录替换解决. 注意: 一定要小心包管理
  • 以前对接支付 sdk 的时候, 经常遇到异步回调没有正常处理的情况. 通过几种方式打日志, 最终发现 php://input 最靠谱, 虽然需要自己 json_decode() 一下来格式化数据
  • 对接某大行的支付 sdk 的时候, 通过打的日志发现, 异步回调的数据格式有 json_encode()http_build_query() 2种形式, 但是这个问题在测试过程中(接近 10 笔订单)过程中没有出现过, 而且由于这个渠道订单量一直很小, 所以问题也是过了一段时间才发现. 提醒一下: 一定要打好日志; 在敏感数据处理的时候, 没有获取到预期的数据, 最好加一下预警

还有一些年代可能有点久远了, 或者是当时实在太 sb, 犯了一些低级错误, 就不赘述了.

写在最后

关于 http 的学习, 其实我已经在之前的 blog - alipay ILLEGAL_SIGN 错误解决 有提到. 当时的问题, 抽丝剥茧一层层下来, 最后到 http 协议这一层, 竟然是非常非常的简单.

所以继续推荐这本书:

<http 权威指南> - 图灵社区: http://www.ituring.com.cn/book/844

这里给一点阅读的建议:

  • 这是一本很纯粹的工具书. 工具书的特点其实和字典非常类似, 你用字典的时候, 只要知道查词的方法(具体的方法就是大名鼎鼎的 「二分查找法」, 可以配合 网易公开课 - 斯坦福大学公开课:编程方法学, 第一集举的就是这个例子)就好了, 并不需要记住所有细节.
  • http 其实是 tcp/ip 4层网络体系下一种应用层的协议. 从协议的角度来看待, 它由哪些部分组成, 这些部分之间如何协同, 就是学习 http 需要掌握的方法, 比如我们常说到的 header body method 等概念, 都是 http 协议的组成部分.

当然, 光看一本书是不能完全解决问题的, 毕竟基于 http 的基础上, 大家又多了各式形形色色的工具.

工具的目的是提供便利, 从编程方法学的角度考虑, 其实是增加抽象.

所以, 多度一些源码吧, 既用来解决实际的业务问题, 也可以用来培养自己对 「编程方法」 的理解.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,199评论 0 17
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,609评论 4 59
  • 最近两个月其实我挺焦躁。 虽然加入小灶有了些改变,但是我觉得我还是心里慌。 就是那种口袋没钱的心慌。 脑袋没货的心...
    赵慧姿阅读 180评论 12 4