# 5.3 異步Http Client
支持協程的異步Http Client很關鍵,在微服務系統框架中,服務與服務的交互大多是通過Http接口來實現,即使有封裝RPC,也大多在Http Client的基礎上,當然我們也可以選擇自定義的Tcp文本或者二進制協議,這里我們主要介紹MSF框架中的Http Client的實現與使用。本節中的示例代碼:[https://github.com/pinguo/php-msf-demo/app/Controllers/Http.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/Http.php)
## 實現
框架對http client的支持是基于swoole_http_client,同時在此基礎上封裝了dns查詢、dns緩存、keep-alive、簡單快捷操作、多個請求并行的各種方法。
## 基本用法
```php
<?php
/**
* 異步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
/**
* 獲取百度首頁,手工進行DNS解析和數據拉取
*/
public function actionBaiduIndexWithOutDNS()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
yield $client->goDnsLookup('http://www.baidu.com');
$sendGet = $client->goGet('/');
$result = yield $sendGet;
$this->outputView(['html' => $result['body']]);
}
}
```
這種用法將一個Http請求方法分成為兩步: 第一步,DNS查詢;第二步,Get請求。可能大家會很奇怪,不就一個Http請求嘛,還分兩步?其實我們原來使用CURL擴展的時候,也是這兩個步驟,只是CURL內部把我們完成了DNS查詢。
另外,由于DNS查詢是一次UDP的請求,PHP內置函數`string gethostbyname ( string $hostname )`是同步阻塞模式,如果使用這個函數,將使我們的Sever退化為同步Server,MSF框架進行DNS查詢使用了`swoole_async_dns_lookup()`進行異步DNS解析。
一個http請求,開發代碼進行兩次yield,開發效率不高,但是性能是最好的;同時我們也提供一些快捷的方法,在只有一次接口請求的開發中性能和效率均可得到提升。
## 快捷POST/GET
```php
<?php
/**
* 異步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
/**
* 獲取百度首頁,自動進行DNS,自動通過Get拉取數據
*/
public function actionBaiduIndexGet()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$result = yield $client->goSingleGet('http://www.baidu.com/');
$this->outputView(['html' => $result['body']]);
}
/**
* 獲取百度首頁,自動進行DNS,自動通過Post拉取數據
*/
public function actionBaiduIndexPost()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$result = yield $client->goSinglePost('http://www.baidu.com/');
$this->outputView(['html' => $result['body']]);
}
}
```
`\PG\MSF\Client\Http\Client::goSingleGet()`,`\PG\MSF\Client\Http\Client::goSinglePost()`兩個方法可以快捷的自動完成DNS和請求的發送,直接返回響應內容。
## 響應數據
```
[
'errCode' => 0
'sock' => 17
'host' => '180.97.33.107'
'port' => 80
'headers' => [
'content-type' => 'text/html'
'content-encoding' => 'gzip'
'cache-control' => 'no-cache'
'pragma' => 'no-cache'
'content-length' => '363'
'set-cookie' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
]
'type' => 1025
'requestHeaders' => [
'Host' => 'www.baidu.com'
'X-Ngx-LogId' => '59496e12c3474d040f41fda2'
]
'requestBody' => null
'cookies' => [
'bai' => '16.'
]
'set_cookie_headers' => [
'bai' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
]
'body' => '<body></body><script type=\"text/javascript\">u=\"https://www.baidu.com/?tn=93817326_hao_pg\";d=document;/webkit/i.test(navigator.userAgent)?(f=d.createElement(\'iframe\'),f.style.width=1,f.style.height=1,f.frameBorder=0,d.body.appendChild(f).src=\'javascript:\"<script>top.location.replace(\\\'\'+u+\'\\\')<\\/script>\"\'):(d.open(),d.write([\'<meta http-equiv=\"refresh\"content=\"0;url=\',\'\"/>\'].join(u)),d.close());function g(k){return v=eval(\"/\"+k+\"=(.*?)(&|$)/i.exec(location.href)\"),v?v[1]:\"\"}</script>'
'statusCode' => 200
]
```
其中:
errCode的具體含義:[附錄:Linux錯誤信息(errno)列表](https://wiki.swoole.com/wiki/page/172.html)
statusCode為Http響應的狀態碼:
[維基百科HTTP狀態碼](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81)
[OSCHINA HTTP狀態碼](http://tool.oschina.net/commons?type=5)
body為響應正文
## 并行請求
Http請求分成了DNS查詢和發送數據兩個異步部分,從而在多個內部接口請求中會寫大量的冗余代碼的,故框架封裝了簡單實用的并行的Http Client,大大的簡化了發送并行請求。
```php
<?php
/**
* 異步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
// 略
/**
* 并行多次獲取百度首頁,自動進行DNS,自動通過Get或者Post拉取數據
*/
public function actionConcurrentBaiduIndex()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$requests = [
'http://www.baidu.com/',
[
'url' => 'http://www.baidu.com/',
'method' => 'POST'
],
];
$results = yield $client->goConcurrent($requests);
$this->outputView(['html' => $results[0]['body'] . $results[0]['body']]);
}
}
```
`\PG\MSF\Client\Http\Client::goConcurrent($requests)`是我們封裝的快捷并行請求方式,`$requests`的數據結構如:
```php
[
'http://www.baidu.com/xxx',
[
// 必須為全路徑URL
'url' => 'http://www.baidu.com/xxx',
'method' => 'GET',
'dns_timeout' => 1000, // 默認為30s
'timeout' => 3000, // 默認不超時
'headers' => [], // 默認為空
'data' => ['a' => 'b'] // 發送數據
],
[
'url' => 'http://www.baidu.com/xxx',
'method' => 'POST',
'timeout' => 3000,
'headers' => [],
'data' => ['a' => 'b'] // 發送數據
],
[
'url' => 'http://www.baidu.com/xxx',
'method' => 'POST',
'timeout' => 3000,
'headers' => [],
'data' => ['a' => 'b'] // 發送數據
],
]
```
## DNS緩存
HTTP Client的DNS查詢為提升性能,默認情況下,已經開啟緩存,緩存策略為:
1. DNS緩存有效時間默認為60s
2. 已解析DNS使用次數上限為10000次
只要判斷有效時間,如果已過有效期即緩存失效;如果在有效期內,使用次數超過10000次,則重新進行DNS解析
## DNS配置
默認開啟了DNS緩存,并有相應的策略,我們也提供了配置項來修改緩存策略,
```php
$config['http']['dns'] = [
// 有效時間,單位秒
'expire' => 30,
// 使用次數上限
'times' => 1000,
];
```
## Keep-Alive
HTTP持久連接(HTTP persistent connection,也稱作HTTP keep-alive或HTTP connection reuse)
是使用同一個TCP連接來發送和接收多個HTTP請求/應答,而不是為每一個新的請求/應答打開新的連接的方法。
如果客戶端支持 keep-alive,它會在請求的包頭中添加:
```
Connection: Keep-Alive
```
然后當服務器收到請求,作出回應的時候,它也添加一個頭在響應中:
```
Connection: Keep-Alive
```
這樣做,連接就不會中斷,而是保持連接。當客戶端發送另一個請求時,它會使用同一個連接。
這一直繼續到客戶端或服務器端認為會話已經結束,其中一方中斷連接。
#### 優勢
- 較少的CPU和內存的使用(由于同時打開的連接的減少了)
- 允許請求和應答的HTTP管線化
- 降低擁塞控制 (TCP連接減少了)
- 減少了后續請求的延遲(無需再進行握手)
- 報告錯誤無需關閉TCP連接
## Keep-Alive 配置
由上面的描述可知,Keep-Alive需要客戶端和服務端都支持才可以。
假如我們的請求鏈路是:瀏覽器->Nginx->php-msf->后端API服務
那么:瀏覽器是純客戶端,后端API服務是純服務端,Nginx和php-msf既是服務端又是客戶端。
#### 保持和client的長連接
Nginx作為http服務端,默認情況下,已經自動開啟了對client連接的keep-alive支持。
一般場景可以直接使用,但是對于一些比較特殊的場景,還是有必要調整個別參數。
需要修改Nginx的配置文件(在nginx安裝目錄下的conf/nginx.conf):
```
http {
keepalive_timeout 120s 120s;
keepalive_requests 10000;
}
```
keepalive_timeout指令的語法:
```
Syntax: keepalive_timeout timeout [header_timeout];
Default: keepalive_timeout 75s;
Context: http, server, location
```
第一個參數設置keep-alive客戶端連接在服務器端保持開啟的超時值。值為0會禁用keep-alive客戶端連接。
可選的第二個參數在響應的header域中設置一個值“Keep-Alive: timeout=time”。這兩個參數可以不一樣。
注:默認75s一般情況下也夠用,對于一些請求比較大的內部服務器通訊的場景,適當加大為120s或者300s。第二個參數通常可以不用設置。
keepalive_requests指令用于設置一個keep-alive連接上可以服務的請求的最大數量。當最大請求數量達到時,連接被關閉。默認是100。
這個參數的真實含義,是指一個keep alive建立之后,nginx就會為這個連接設置一個計數器,記錄這個keep alive的長連接上已經接收并處理的客戶端請求的數量。如果達到這個參數設置的最大值時,則nginx會強行關閉這個長連接,逼迫客戶端不得不重新建立新的長連接。
這個參數往往被大多數人忽略,因為大多數情況下當QPS(每秒請求數)不是很高時,默認值100湊合夠用。但是,對于一些QPS比較高(比如超過10000QPS,甚至達到30000,50000甚至更高) 的場景,默認的100就顯得太低。
簡單計算一下,QPS=10000時,客戶端每秒發送10000個請求(通常建立有多個長連接),每個連接只能最多跑100次請求,意味著平均每秒鐘就會有100個長連接因此被nginx關閉。同樣意味著為了保持QPS,客戶端不得不每秒中重新新建100個連接。因此,如果用netstat命令看客戶端機器,就會發現有大量的TIME_WAIT的socket連接(即使此時keep alive已經在client和nginx之間生效)。
因此對于QPS較高的場景,非常有必要加大這個參數,以避免出現大量連接被生成再拋棄的情況,減少TIME_WAIT。
#### 保持和php-msf server的長連接
為了讓nginx和php-msf server(nginx稱為upstream)之間保持長連接,典型設置如下:
```
http {
upstream MSF_BACKEND {
server 127.0.0.1:8000;
keepalive 300; // 這個很重要!設置每個worker進程在緩沖中保持的到upstream服務器的空閑keepalive連接的最大數量.當這個數量被突破時,最近使用最少的連接將被關閉。
}
server {
listen 80 default_server;
server_name "";
location / {
proxy_pass http://MSF_BACKEND;
proxy_set_header Host $Host;
proxy_set_header x-forwarded-for $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
add_header Cache-Control no-store;
add_header Pragma no-cache;
proxy_http_version 1.1; // 這兩個最好也設置
proxy_set_header Connection "";
client_max_body_size 3072k;
client_body_buffer_size 128k;
}
}
}
```
## MSF長連接
#### 保持和客戶端的長連接
和Nginx一樣,只要客戶端支持長連接,msf就會默認支持長連接,不需要任何配置。
#### 保持和后端接口服務的長連接
當我們的服務需要請求其他的服務的時候,那么這個場景長連接就是需要的了,php-msf默認開啟的長連接,默認的配置如下:
```
http.keepAlive.expire = 120 //每個長連接有效期為20s
http.keepAlive.times = 10000 //每個長連接在有效期內最多處理10000個請求
```
也可用通過
```
$config['http']['keepAlive'] = [
'expire' => 60, // 為0時表示關閉 keep-alive
'times' => 1000
]
```
來控制 http-client 的keep-alive行為。
- 0 文檔說明
- 1 為什么研發新框架
- 1.1 傳統php-fpm工作模式的問題
- 1.2 壓測數據對比
- 1.3 小結
- 2 微服務框架研發概覽
- 2.1 通信框架技術選型
- 2.2 swoole
- 2.3 協程原理
- 2.4 異步、并發
- 2.5 小結
- 3 框架運行環境
- 3.1 環境變量
- 3.2 運行代碼
- 3.3 docker
- 3.4 小結
- 4 框架結構
- 4.1 結構概述
- 4.2 控制器
- 4.3 模型
- 4.4 視圖
- 4.5 同步任務
- 4.6 配置
- 4.7 路由
- 4.8 小結
- 5 框架組件
- 5.1 協程
- 5.2 類的加載
- 5.3 異步Http Client
- 5.4 請求上下文
- 5.5 連接池
- 5.6 對象池
- 5.7 RPC
- 5.8 公共庫
- 5.9 RESTful
- 5.10 多語言
- 5.11 雜項
- 5.12 小結
- 6 常見問題
- 7 附錄