5.3 异步Http Client
最后更新于:2022-03-31 23:47:42
# 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行为。