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 請求工具:
- cURL: http://php.net/manual/en/book.curl.php
- 非常火的 composer package - Guzzle: http://docs.guzzlephp.org/en/stable/overview.html
- hyperframework 中的 webclient 工具: http://hyperframework.com/cn/manual/common/web_client_basics
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 的基礎上, 大家又多了各式形形色色的工具.
工具的目的是提供便利, 從編程方法學的角度考慮, 其實是增加抽象.
所以, 多度一些源碼吧, 既用來解決實際的業務問題, 也可以用來培養自己對 「編程方法」 的理解.