⚔️ Saber, PHP 同步协程 HTTP 客户端 | PHP Coroutine HTTP client - Swoole 人性化库
⚔️ Saber, PHP 同步协程 HTTP 客户端 | PHP Coroutine HTTP client - Swoole 人性化库
HTTP军刀(呆毛王), Swoole人性化组件库之PHP高性能HTTP客户端, 基于Swoole原生协程, 支持多种风格操作, 底层提供高性能解决方案, 让开发者专注于功能开发, 从传统同步阻塞且配置繁琐的Curl中解放.
最好的安装方法是通过 Composer 包管理器 :
composer require swlib/saber
Swoole底层实现协程调度, 业务层无需感知, 开发者可以无感知的用同步的代码编写方式达到异步IO的效果和超高性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护.
需要在onRequet, onReceive, onConnect等事件回调函数中使用, 或是使用go关键字包裹 (swoole.use_shortname默认开启).
go(function () {
echo SaberGM::get('http://httpbin.org/get');
})
数据自动打包: 传入的data会自动转换成content-type所指定的类型格式
默认为
x-www-form-urlencoded, 也支持json等其它格式
SaberGM := Saber Global Manager, 如果觉得类名有点长, 可以使用class_alias自己取别名, 推荐服务中使用生成实例的方式使用, 而把SaberGM作为快捷方式.
SaberGM::get('http://httpbin.org/get');
SaberGM::delete('http://httpbin.org/delete');
SaberGM::post('http://httpbin.org/post', ['foo' => 'bar']);
SaberGM::put('http://httpbin.org/put', ['foo' => 'bar']);
SaberGM::patch('http://httpbin.org/patch', ['foo' => 'bar']);
适用API代理服务
$saber = Saber::create([
'base_uri' => 'http://httpbin.org',
'headers' => [
'Accept-Language' => 'en,zh-CN;q=0.9,zh;q=0.8',
'Content-Type' => ContentType::JSON,
'DNT' => '1',
'User-Agent' => null
]
]);
echo $saber->get('/get');
echo $saber->delete('/delete');
echo $saber->post('/post', ['foo' => 'bar']);
echo $saber->patch('/patch', ['foo' => 'bar']);
echo $saber->put('/put', ['foo' => 'bar']);
Session会自动保存cookie信息, 其实现是浏览器级别完备的
$session = Saber::session([
'base_uri' => 'http://httpbin.org',
'redirect' => 0
]);
$session->get('/cookies/set?foo=bar&k=v&apple=banana');
$session->get('/cookies/delete?k');
echo $session->get('/cookies')->body;
注意: 此处使用了并发重定向优化方案, 多个重定向总是依旧并发的而不会退化为队列的单个请求
$responses = SaberGM::requests([
['uri' => 'http://github.com/'],
['uri' => 'http://github.com/'],
['uri' => 'https://github.com/']
]);
echo "multi-requests [ {$responses->success_num} ok, {$responses->error_num} error ]:\n" ."consuming-time: {$responses->time}s\n";
// multi-requests [ 3 ok, 0 error ]:
// consuming-time: 0.79090881347656s
// 别名机制可以省略参数书写参数名
$saber = Saber::create(['base_uri' => 'http://httpbin.org']);
echo $saber->requests([
['get','/get'],
['post','/post'],
['patch','/patch'],
['put','/put'],
['delete','/delete']
]);
目前支持json,xml,html,url-query四种格式的数据快速解析
[$json, $xml, $html] = SaberGM::list([
'uri' => [
'http://httpbin.org/get',
'http://www.w3school.com.cn/example/xmle/note.xml',
'http://httpbin.org/html'
]
]);
var_dump($json->getParsedJsonArray());
var_dump($json->getParsedJsonObject());
var_dump($xml->getParsedXmlArray());
var_dump($xml->getParsedXmlObject(true));
var_dump($html->getParsedDomObject()->getElementsByTagName('h1')->item(0)->textContent);
支持HTTP和SOCKS5代理
$uri = 'http://myip.ipip.net/';
echo SaberGM::get($uri, ['proxy' => 'http://127.0.0.1:1087'])->body;
echo SaberGM::get($uri, ['proxy' => 'socks5://127.0.0.1:1086'])->body;
底层自动协程调度, 可支持异步发送超大文件, 断点续传
同时上传三个文件(三种参数风格
string|array|object)
$file1 = __DIR__ . '/black.png';
$file2 = [
'path' => __DIR__ . '/black.png',
'name' => 'white.png',
'type' => ContentType::MAP['png'],
'offset' => null, //re-upload from break
'size' => null //upload a part of the file
];
$file3 = new SwUploadFile(
__DIR__ . '/black.png',
'white.png',
ContentType::MAP['png']
);
echo SaberGM::post('http://httpbin.org/post', null, [
'files' => [
'image1' => $file1,
'image2' => $file2,
'image3' => $file3
]
]
);
Download收到数据后会直接异步写入到磁盘, 而不是在内存中对HttpBody进行拼接. 因此download仅使用小量内存, 就可以完成超大文件的下载. 且支持断点续传, 通过设置offset参数来进行断点下载.
异步下载Saber壁纸
$download_dir = '/tmp/saber.jpg';
$response = SaberGM::download(
'https://ws1.sinaimg.cn/large/006DQdzWly1fsr8jt2botj31hc0wxqfs.jpg',
$download_dir
);
if ($response->success) {
exec('open ' . $download_dir);
}
在爬虫项目中, 请求失败自动重试是非常常见的需求, 比如会话过期后重新登录.
而Saber内置了此功能, 并可使用拦截器来强化它.
如未设置retry_time而设置了retry拦截器, 则retry_time会置为1, 如retry拦截器的回调方法返回了false, 无论retry_time是多少, 都会在返回false时终止重试.
$uri = 'http://eu.httpbin.org/basic-auth/foo/bar';
$res = SaberGM::get(
$uri, [
'exception_report' => 0,
'retry_time' => 3,
'retry' => function (Saber\Request $request) {
echo "retry...\n";
$request->withBasicAuth('foo', 'bar'); //发现失败后添加验证信息
if ('i don not want to retry again') {
return false; // shutdown
}
}
]
);
echo $res;
有时候HTTP资源并不会总是变更, 我们可以学习浏览器缓存不会变动的资源, 来加快请求效率, 由Saber自动化地完成且不必自己维护缓存逻辑(CURD或文件读写), 协程的调度使得其不论如何都不会阻塞服务器, Saber没有使用中间件机制因为它和Swoole是强相关的, 但是缓存可以使用 内存/文件/数据库 等多种方式, 所以虽然它尚未实现, 但它将会列入Saber的后续路线图中.
$bufferStream = new BufferStream();
$bufferStream->write(json_encode(['foo' => 'bar']));
$response = SaberGM::psr()
->withMethod('POST')
->withUri(new Uri('http://httpbin.org/post?foo=bar'))
->withQueryParams(['foo' => 'option is higher-level than uri'])
->withHeader('content-type', ContentType::JSON)
->withBody($bufferStream)
->exec()->recv();
echo $response->getBody();
可以通过websocketFrame数据帧的__toString方法直接打印返回数据字符串
$websocket = SaberGM::websocket('ws://127.0.0.1:9999');
while (true) {
echo $websocket->recv(1) . "\n";
$websocket->push("hello");
co::sleep(1);
}
测试机器为最低配MacBookPro, 请求服务器为本地echo服务器
0.9秒完成6666个请求, 成功率100%.
co::set(['max_coroutine' => 8191]);
go(function () {
$requests = [];
for ($i = 6666; $i--;) {
$requests[] = ['uri' => 'http://127.0.0.1'];
}
$res = SaberGM::requests($requests);
echo "use {$res->time}s\n";
echo "success: $res->success_num, error: $res->error_num";
});
// on MacOS
// use 0.91531705856323s
// success: 6666, error: 0
在实际项目中, 经常会存在使用URL列表来配置请求的情况, 因此提供了list方法来方便使用:
echo SaberGM::list([
'uri' => [
'https://www.qq.com/',
'https://www.baidu.com/',
'https://www.swoole.com/',
'http://httpbin.org/'
]
]);
在实际爬虫项目中, 我们往往要限制单次并发请求数量以防被服务器防火墙屏蔽, 而一个max_co参数就可以轻松地解决这个问题, max_co会将请求根据上限量分批将请求压入队列并执行收包.
// max_co is the max number of concurrency request once, it's very useful to prevent server-waf limit.
$requests = array_fill(0, 10, ['uri' => 'https://www.qq.com/']);
echo SaberGM::requests($requests, ['max_co' => 5])->time."\n";
echo SaberGM::requests($requests, ['max_co' => 1])->time."\n";
在常驻内存的服务器中使用时, 一定要手动开启连接池选项:
$swoole = Saber::create([
'base_uri' => 'https://www.swoole.com/',
'use_pool' => true
]);
在通过该实例使用时, 就会启用连接池特性, 即底层与www.swoole.com网站的连接客户端将会用一个全局连接池存取, 避免了每次使用创建/连接的开销.
在参数为true时, 该网站的连接池容量是无限的, 一般情况下没有问题, 且无限容量的连接池性能更好.
但如果你使用其作为爬虫代理服务, 遭遇大量请求时, 连接池中的客户端数量就会不可控制地快速上升, 甚至超出你所请求的源网站的最大允许连接数, 这时候你就需要将use_pool设置为一个理想数值(int), 此时, 底层会使用Channel作为连接池, 在连接池创建的客户端超出数量且不够取用时, 挂起需要取用客户端的协程, 并等待正在使用客户端的协程归还客户端, 协程等待和切换几乎没有多大的性能消耗, 是一种非常先进的解决方式.
需要注意的是, 连接池是绑定服务器IP+端口的, 即如果你有多个实例面向的是同一个服务器IP+端口, 他们之间使用的连接池也是同一个.
所以你在重复创建服务器IP+端口的实例时, 新创建的实例指定的use_pool是允许覆盖之前数值的, 即连接池底层是自动变容的, 容量增加时底层会重新创建新的连接池并转移客户端, 容量减少时也会销毁在连接池内的多余的客户端.
除了一定要记得配备连接池以外, 异常处理的方式也需要注意是符合你的编程习惯的, Saber默认的异常处理是最主流且严谨的抛出异常, 但Saber也支持静默地使用错误码和状态位, 可能更符合很多人的口味.
SaberGM::exceptionReport(0); // 关闭抛出异常报告, 在业务代码之前注册即可全局生效
$saber->exceptionReport(0); //也可以单独设置某个实例
同理, 你所希望的配置都可以在业务代码之前如onWorkerStart甚至是swoole_server启动之前预先配置.
SaberGM::default([
'exception_report' => 0
'use_pool' => true
]);
像这样配置你所期望的选项可以让你获得更好的使用体验!
go(function(){
// your code with pool...
saber_pool_release(); // and this script will exit
});
如果你在一次性脚本中使用的连接池, 由于协程客户端是存在池中的, 引用计数为1无法释放, 就会导致swoole一直处于事件循环中, 脚本就无法退出, 你需要手动调用saber_pool_release或saber_exit或swoole_event_exit来正常退出, 也可以使用exit强制退出当前脚本(不要在server中使用exit).
|符号分割多种可选值
| key | type | introduction | example | remark |
|---|---|---|---|---|
| protocol_version | string | HTTP协议版本 | 1.1 | HTTP2还在规划中 |
| base_uri | string | 基础路径 | http://httpbin.org |
将会与uri按照rfc3986合并 |
| uri | string | 资源标识符 | http://httpbin.org/get | /get | get |
可以使用绝对路径和相对路径 |
| uri_query | string|array | 请求信息 | ['foo' => 'bar'] |
非字符串会自动转换 |
| method | string | 请求方法 | get | post | head | patch | put | delete |
底层自动转换为大写 |
| headers | array | 请求报头 | ['DNT' => '1'] | ['accept' => ['text/html'], ['application/xml']] |
字段名不区分大小写, 但会保留设定时的原始大小写规则, 底层每个字段值会根据PSR-7自动分割为数组 |
| cookies | array|string |
['foo '=> 'bar'] | 'foo=bar; foz=baz' |
底层自动转化为Cookies对象, 并设置其domain为当前的uri, 具有浏览器级别的完备属性. | |
| useragent | string | 用户代理 | curl-1.0 |
默认为macos平台的chrome |
| referer | string | 来源地址 | https://www.google.com |
默认为空 |
| redirect | int | 最大重定向次数 | 5 | 默认为3, 为0时不重定向. |
| keep_alive | bool |
暂无开放 Issues,或尚未同步最近议题。