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 的基礎上, 大家又多了各式形形色色的工具.

工具的目的是提供便利, 從編程方法學的角度考慮, 其實是增加抽象.

所以, 多度一些源碼吧, 既用來解決實際的業務問題, 也可以用來培養自己對 「編程方法」 的理解.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,993評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,559評論 25 708
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,554評論 0 17
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,257評論 4 61
  • 最近兩個月其實我挺焦躁。 雖然加入小灶有了些改變,但是我覺得我還是心里慌。 就是那種口袋沒錢的心慌。 腦袋沒貨的心...
    趙慧姿閱讀 211評論 12 4