PHP的curl扩展是在PHP Web应用中常见的网络服务调用方式,支持http、https、ftp等协议,编译的过程中是依赖LibCurl
包,libcurl 同时支持 HTTPS 证书、HTTP POST、HTTP PUT、 FTP 上传(也能通过 PHP 的 FTP 扩展完成)、HTTP 基于表单的上传、代理、cookies、用户名+密码的认证。
另外,目前另外一种调用网络常用方式是基于基于GuzzleHttp包
,可以不依赖cURL来发送HTTP请求(基于PHP流包装程序发送HTTP请求)
本文主要是基于curl
的扩展,在使用curl
扩展在开发过程中遇到的一些问题整理。
针对单HTTP请求
curl_exec($ch)
在失败后,会重新创建套接字,不会复用之前的连接句柄;curl_exec
执行,在底层是调用了libcurl库的curl_easy_perform
方法 ,curl_easy_perform
以阻塞的方式执行整个请求,并在完成或失败时返回,,curl_multi_perform
支持非阻塞的poll/select
模式;curl_setopt
设置的属性:CURLOPT_CONNECTTIMEOUT
:连接等待的秒数,不包含在CURLOPT_TIMEOUT
或者CURLOPT_TIMEOUT_MS
中CURLOPT_TIMEOUT_MS
和CURLOPT_TIMEOUT
只针对单次curl_exec($ch)
有用,在循环中会重新计算
- 在下面的示例中,while循环是否会对服务器性能有影响,是依赖于curl请求的服务(不要一看到while循环就主观认为会很消耗cpu),实际上消耗没有想象的大,尤其是在请求的web服务存不是很快响应的时候,此时curl_exec底层通过调用
curl_easy_perform
同步阻塞,导致while循环在等待IO响应(poll执行的过程,每秒一次)。若服务端很快响应,比如10ms内,同时while的业务也执行很快,这内容情况就比较容易导致快速循环迭代,不断请求服务,快速处理,在请求服务的过程,导致CPU的使用率会有一定上升,若我们又不希望这样(因为可能服务每次都是重复的信息,比如重复的探测),这类情况可以适当增加延迟处理来延缓循环迭代的速度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <?php
$ch = curl_init("http://localhost:2334/curl/req_time/".time());
while(true) {
// 设置 URL 和相应的选项,其中TIMEOUT是在每次执行后,会重新计算
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// 另外底层会以poll/select模式检测文件描述符状态(同步阻塞)
$rs = curl_exec($ch);
if ($rs == false) {
echo curl_error($ch)."\n";
} else {
echo $rs;
}
// 传输信息
$info = curl_getinfo($ch);
printf("LocalAddress: %s:%s\n", $info["local_ip"], $info["local_port"]);
}
// 关闭 cURL 资源,并且释放系统资源
curl_close($ch);
|
针对多HTTP请求
- PHP中的curl_multi_中每个ch句柄都是一个连接套接字/句柄,curl_multi是建立在多TCP通路上发送数据(其实类似TCP/1x的多TCP并行加速下载优化,HTTP/2采用了连接复用,基于流+帧来优化,开销更小)
- curl_multi_perform函数以非阻塞方式处理所有需要注意的添加的句柄上的传输。
- PHP中的
curl_multi_select
底层是基于libcurl的curl_multi_wait
,轮询多句柄中的所有简单句柄,curl_multi_wait
轮询给定多句柄集中包含的curl easy句柄使用的所有文件描述符。同时它将阻塞,直到在至少一个句柄上检测到活动或timeout_ms超时。在PHP的扩展中,在curl版本大于7.28.0
会采用该poll模式,同时我们也发现wait的timeout是1s.
curl_multi_exec的PHP源码
CURLMcode curl_multi_perform(CURLM *multi_handle, int *running_handles);
curl_multi_exec
内部是基于libcurl库函数curl_multi_perform
执行,该函数以非阻塞方式处理所有需要注意的添加的句柄上的传输。
curl_multi_perform在读取/写入完成后立即返回,当应用程序发现有可用于multi_handle的数据或超时时,应用程序应调用此函数以读取/写入当前要读取或写入的内容,同时在在第二个参数的整数指针中写入仍在传输数据的句柄数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| /* {{{ proto int curl_multi_exec(resource mh, int &still_running)
Run the sub-connections of the current cURL handle */
PHP_FUNCTION(curl_multi_exec)
{
zval *z_mh;
zval *z_still_running;
php_curlm *mh;
int still_running;
CURLMcode error = CURLM_OK;
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_RESOURCE(z_mh)
Z_PARAM_ZVAL(z_still_running)
ZEND_PARSE_PARAMETERS_END();
if ((mh = (php_curlm *)zend_fetch_resource(Z_RES_P(z_mh), le_curl_multi_handle_name, le_curl_multi_handle)) == NULL) {
return;
}
{
zend_llist_position pos;
php_curl *ch;
zval *pz_ch;
// 遍历每一个之前添加的easyhandler句柄,检测句柄
for (pz_ch = (zval *)zend_llist_get_first_ex(&mh->easyh, &pos); pz_ch;
pz_ch = (zval *)zend_llist_get_next_ex(&mh->easyh, &pos)) {
if ((ch = (php_curl *)zend_fetch_resource(Z_RES_P(pz_ch), le_curl_name, le_curl)) == NULL) {
return;
}
// 检验句柄是否有错(1表示通过`php_error_docref`报告警告错误)
_php_curl_verify_handlers(ch, 1);
}
}
still_running = zval_get_long(z_still_running);
// 基于curl_multi_perform库函数执行
error = curl_multi_perform(mh->multi, &still_running);
ZEND_TRY_ASSIGN_REF_LONG(z_still_running, still_running);
SAVE_CURLM_ERROR(mh, error);
RETURN_LONG((zend_long) error);
}
/* }}} */
|
若执行curl的进程设置了TIMEOUT
超时时间,同时服务器的对应的响应服务也很慢,此时curl_easy_perform
内部会进行poll轮询,检测可读事件或者直至设置的TIMEOUT
超时过期;该类情况通过strace可以跟踪到具体进程的内容(strace -TCvf -s 1024 -e all -p $(ps -ef|grep cur[l]|awk '{print $2}')
):
1
2
3
4
5
6
7
8
9
| poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout) <0.000019>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 1000) = 0 (Timeout) <1.000522>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout) <0.000008>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 1000) = 0 (Timeout) <1.001108>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout) <0.000014>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 1000) = 0 (Timeout) <1.001173>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout) <0.000013>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 1000) = 0 (Timeout) <1.000411>
poll([{fd=3, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout) <0.000013>
|
curl_multi_select的PHP源码
curl_multi_wait
基于poll模式,轮询给定多句柄集中包含的curl easy句柄使用的所有文件描述符,它将阻塞,直到在至少一个句柄上检测到活动或timeout_ms已经过去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| /* {{{ proto int curl_multi_select(resource mh[, double timeout])
Get all the sockets associated with the cURL extension, which can then be "selected" */
PHP_FUNCTION(curl_multi_select)
{
zval *z_mh;
php_curlm *mh;
double timeout = 1.0;
#if LIBCURL_VERSION_NUM >= 0x071c00 /* Available since 7.28.0 */
int numfds = 0;
#else
fd_set readfds;
fd_set writefds;
fd_set exceptfds;
int maxfd;
struct timeval to;
#endif
CURLMcode error = CURLM_OK;
ZEND_PARSE_PARAMETERS_START(1,2)
Z_PARAM_RESOURCE(z_mh)
Z_PARAM_OPTIONAL
Z_PARAM_DOUBLE(timeout)
ZEND_PARSE_PARAMETERS_END();
if ((mh = (php_curlm *)zend_fetch_resource(Z_RES_P(z_mh), le_curl_multi_handle_name, le_curl_multi_handle)) == NULL) {
return;
}
#if LIBCURL_VERSION_NUM >= 0x071c00 /* Available since 7.28.0 */
// 7.28之后的LibCurl支持`curl_multi_wait`,poll轮询多句柄中的所有简单句柄,阻塞直至任意简单句柄事件发生或超时,优于select模式1024文件描述符限制
error = curl_multi_wait(mh->multi, NULL, 0, (unsigned long) (timeout * 1000.0), &numfds);
if (CURLM_OK != error) {
SAVE_CURLM_ERROR(mh, error);
RETURN_LONG(-1);
}
RETURN_LONG(numfds);
#else
_make_timeval_struct(&to, timeout);
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
// 从多句柄中提取文件描述符信息,添加至select函数(read_fd_set!=null:读就绪、write_fd_set!=null:写就绪、exc_fd_set!=null:异常错误)
error = curl_multi_fdset(mh->multi, &readfds, &writefds, &exceptfds, &maxfd);
SAVE_CURLM_ERROR(mh, error);
if (maxfd == -1) {
RETURN_LONG(-1);
}
// 基于select模式,有最大maxfd监听限制,当读、写、异常错误在任一事件产生时候返回
RETURN_LONG(select(maxfd + 1, &readfds, &writefds, &exceptfds, &to));
#endif
}
/* }}} */
|
CURL MULTI代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| <?php
function showCurlInfo($chs)
{
// 传输信息
echo "LocalAddress ";
foreach ($chs as $k => $ch) {
$info = curl_getinfo($ch);
printf("(%s:%s) ", $info["local_ip"], $info["local_port"]);
}
echo "\n";
}
// create both cURL resources
$ch1 = curl_init();
$ch2 = curl_init();
// set URL and other appropriate options
curl_setopt_array($ch1, [
CURLOPT_URL => "http://10.37.5.249:2334/multi01",
CURLOPT_HEADER => 0,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_TIMEOUT_MS => 500,
]);
curl_setopt($ch2, CURLOPT_URL, "http://10.37.5.249:2334/multi02");
curl_setopt($ch2, CURLOPT_HEADER, 0);
//create the multiple cURL handle
$mh = curl_multi_init();
//add the two handles
$rs = curl_multi_add_handle($mh, $ch1);
$rs = curl_multi_add_handle($mh, $ch2);
// 执行批处理句柄,curl_multi_exec该函数仅返回关于整个批处理栈相关的错误。即使返回 CURLM_OK 时单个传输仍可能有问题。
// Before version 7.20.0: If you receive CURLM_CALL_MULTI_PERFORM, this basically means that you should
// call curl_multi_perform again, before you select() on more actions
$active = null;
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
// 仍在执行的标识的引用有在运行
while ($active && $mrc == CURLM_OK) {
echo date_format(date_create(),"H:i:s u")."\n";
// select等待事件OK, 成功时返回描述符集合中描述符的数量。
if (curl_multi_select($mh) != -1) {
do {
$mrc = curl_multi_exec($mh, $active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
}
// showCurlInfo([$ch1, $ch2]);
}
$errors = curl_multi_errno($mh);
var_dump("multi error:",$errors);
// get content
foreach ([$ch1, $ch2] as $ch) {
var_dump(curl_multi_getcontent($ch));
curl_multi_remove_handle($mh, $ch);
}
//close the handles
curl_multi_close($mh);
|
Http Web服务调试
缓冲推送在IDE中看不到效果(可以直接通过curl -i
请求可以查看到推送10次内容)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"time"
)
// 用于调试PHP curl_multi_select
func main() {
// 可以调整time.sleep查看不同差异
http.HandleFunc("/curl/", func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
time.Sleep(100*time.Millisecond)
//time.Sleep(*time.Second)
fmt.Fprintf(w,"URI:%s,TIME ESAPSED:%s\n", r.RequestURI, time.Since(now))
})
// 基于flush推
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
f, _ := w.(http.Flusher)
//w.Header().Set("x-request-id", "ok")
for i := 0; i < 10; i++ {
log.Printf("URI:%s, RemoteAddr:%v\n", r.RequestURI, r.RemoteAddr)
now := time.Now()
time.Sleep(time.Duration(rand.Intn(10)+1) * 300 * time.Millisecond)
fmt.Fprintf(w,"URI:%s,TIME ESAPSED:%s\n", r.RequestURI, time.Since(now))
f.Flush()
}
})
log.Fatal(http.ListenAndServe(":2334", nil))
}
|
相关服务调试和观测命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 查看推流
curl -i http://10.37.5.249:2334/multi01
// 执行php命令
php curl01.php
php curl_multi01.php
// strace跟踪php执行的curl代码进程
strace -ttfCv -s 1024 -p $(ps -ef|grep cur[l]|awk '{print $2}')
// watch查看命令TCP连接状态变化
watch -n 0.5 'netstat -ntp|grep 2334'
// 资源使用查看(pidstat、vmstat)
pidstat -p $(ps -ef|grep cur[l]|awk '{print $2}') 1
// 另外,在中间意外网络问题,通过tcpdump导出分析
tcpdump 'tcp port 2334' -w /tmp/tcpdump
tcpdump -r /tmp/tcpdump -nnn|tail
|
小结
基于对php的curl扩展以及libcurl包的简单使用和分析调试,在针对单独curl_exec
执行时候,调用的进程会同步阻塞,直至curl_setopt
设置的TIMEOUT
到期或者有接收到数据;curl_multi_exec
在使用时候,通过将简单句柄加入进来,同时基于pool或select
的非阻塞IO模式进行事件监听,每个ch句柄都是一个单独的tcp连接;curl_exec即使是在while死循环中,也不会一定会引起系统负的高负载问题;