PHP-CURL连接复用内核原理
0.写在前面
PHP是一个时代的产物,它的底层支持是C语言,因此它在CPU密集型计算或者系统内核调用上有天生的优势,Zend引擎把PHP执行生命期分成了五个阶段1,这五个阶段并不是全部都能常驻进程,这种模式下,对于很多使用场景会造成不好的影响,比如网络IO.
对于网络IO中的HTTP请求 , 很多工程师使用 php-curl 系列函数 . 所以这篇文章将从内核角度讲解php如何支持curl请求的连接复用(这里的连接复用也是指在一个RINIT2–>RSHUTDOWN3周期内复用).
1. PHP引擎借力CURL库函数
PHP需要使用curl组件进行HTTP系列通信,因此它在底层需要curl有相关的支撑,所以curl首先需要在系统环境中被部署或者被编译,并对外部提供动态链接库文件,PHP通过调用curl相关的动态链接库函数来进行自己内核函数的实现过程.
多说一句,PHP并不一定需要curl才能完成http请求,因为php引擎中已经包含了socket完善的函数库,所以有些php扩展包支持curl和原生stream_socket(tcp)两种模式,例如:guzzle
2. PHP-CURL基础数据结构(php_curl结构体)
1147 typedef struct {
2148 php_curl_write *write;
3149 php_curl_write *write_header;
4150 php_curl_read *read;
5151 zval std_err;
6152 php_curl_progress *progress;
7153 #if LIBCURL_VERSION_NUM >= 0x071500 /* Available since 7.21.0 */
8154 php_curl_fnmatch *fnmatch;
9155 #endif
10156 } php_curl_handlers;
11
12173 typedef struct {
13174 CURL *cp; //curl库 实体结构体
14175 php_curl_handlers *handlers; //header 头部
15176 zend_resource *res; //引擎资源指针
16177 struct _php_curl_free *to_free;
17178 struct _php_curl_send_headers header;
18179 struct _php_curl_error err; //错误码
19180 zend_bool in_callback;
20181 uint32_t* clone;
21182 } php_curl;
3. 透析CURL初始化阶段(curl_init函数)
curl_init是php调用curl的开端,之所以要分析这个函数,因为这个函数中就包括PHP如何调用curl库函数,如何对curl进行参数设置, 代码的解析通过注释的方式 , 内核源码如下:
137 #include <curl/curl.h> //引入curl库头文件
238 #include <curl/easy.h>
3
4/* {{{ proto resource curl_init([string url])
5 Initialize a cURL session */
62019 PHP_FUNCTION(curl_init)
72020 {
82021 php_curl *ch;
92022 CURL *cp;
102023 zend_string *url = NULL;
112024 //解析php函数参数 ,不是必须参数,如果存在则丰富url zend_string(php内核字符串类型)类型指针
122025 ZEND_PARSE_PARAMETERS_START(0,1)
132026 Z_PARAM_OPTIONAL
142027 Z_PARAM_STR(url)
152028 ZEND_PARSE_PARAMETERS_END();
162029 / *
17 * curl_easy_init()是curl库对外提供的alloc,setup和init的外部函数
18 * struct Curl_easy *curl_easy_init(void); /lib/easy.c :381
19 * 返回简单的句柄。如果出现任何问题,则返回NULL。
20 */
212030 cp = curl_easy_init();
222031 if (!cp) { //如果curl初始化失败 则 返回false
232032 php_error_docref(NULL, E_WARNING, "Could not initialize a new cURL handle");
242033 RETURN_FALSE;
252034 }
262035 //这个是核心内存构造函数,主要构造curl句柄的结构指针内存,
272036 ch = alloc_curl_handle();
282037
292038 ch->cp = cp;
302039
312040 ch->handlers->write->method = PHP_CURL_STDOUT;
322041 ch->handlers->read->method = PHP_CURL_DIRECT;
332042 ch->handlers->write_header->method = PHP_CURL_IGNORE;
342043
352044 _php_curl_set_default_options(ch); //初始化CURL对象的默认参数
36
372045 //如果url指针不为null,则把url参数设置好
382046 if (url) {
39 /*php_curl_option_url包含:
40 1.url解析(php_url_parse_ex) 在(LIBCURL_VERSION_NUM > 0x073800) 支持file://
41 2.php_curl_option_str --> curl_easy_setopt(ch->cp, option, str);
42 函数:设置curl句柄的字符串类型参数,在libcurl 7.17.0之后进行url字符串拷贝.
43 copystr = estrndup(url, len);
44 error = curl_easy_setopt(ch->cp, option, copystr);
45 */
462047 if (php_curl_option_url(ch, ZSTR_VAL(url), ZSTR_LEN(url)) == FAILURE) {
472048 _php_curl_close_ex(ch);
482049 RETURN_FALSE;
492050 }
502051 }
512052 /*zval *return_value,我们在函数内部修改这个指针,函数执行完成后,内核将把这个指针指向的zval
52 返回给用户端的函数调用者。这种结果值的设计很少见,可以达到很多意想不到的效果,就像这个函数,设置
53 完返回值,又可以继续进行相关操作.
54 注册ch资源 并 把相关资源指针初始化到返回值内存内.
55 */
56 //ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type);
572053 ZVAL_RES(return_value, zend_register_resource(ch, le_curl));
582054 ch->res = Z_RES_P(return_value); //提取资源的指针(在php引擎中是资源id)
592055 }
602056 /* }}} */
4. 透析CURL参数设置(curl_setopt函数)
php在初始化curl阶段之后,如果初始化成功则会能到一个有效的curl句柄,随后需要对这个curl句柄中的参数进行丰富性设置,下面我们就来看一下内核这部分源代码:
13014 /* {{{ proto bool curl_setopt(resource ch, int option, mixed value)
23015 Set an option for a cURL transfer */
33016 PHP_FUNCTION(curl_setopt)
43017 {
53018 zval *zid, *zvalue;
63019 zend_long options;
73020 php_curl *ch;
83021 /* 解析参数
9 * 1.resource : zid
10 * 2.long: options
11 * 3.zval: zvalue
12 */
133022 ZEND_PARSE_PARAMETERS_START(3, 3)
143023 Z_PARAM_RESOURCE(zid)
153024 Z_PARAM_LONG(options)
163025 Z_PARAM_ZVAL(zvalue)
173026 ZEND_PARSE_PARAMETERS_END();
18 /*获取资源地址
19 * zend_fetch_resource : if(resource_type == res->type){return res->ptr;}
20 * 把void* 指针转换为 php_curl* 指针类型
21 */
223027 // #define le_curl_name "cURL handle"
233028 if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
243029 RETURN_FALSE;
253030 }
263031 //检查参数
27 //PHP7 删除了CURLOPT_SAFE_UPLOAD选项, 必须使用 CURLFile interface 来上传文件
283032 if (options <= 0 && options != CURLOPT_SAFE_UPLOAD) {
293033 php_error_docref(NULL, E_WARNING, "Invalid curl configuration option");
303034 RETURN_FALSE;
313035 }
323036 //对ch句柄设置选项对应的参数
333037 if (_php_curl_setopt(ch, options, zvalue) == SUCCESS) {
343038 RETURN_TRUE;
353039 } else {
363040 RETURN_FALSE;
373041 }
383042 }
393043 /* }}} */
403044
1//这个选项在第五部分会设计,所以单独拿出来, 设置CURL的结果输出类型
22899 case CURLOPT_RETURNTRANSFER:
32900 if (zend_is_true(zvalue)) {
42901 ch->handlers->write->method = PHP_CURL_RETURN;
52902 } else {
62903 ch->handlers->write->method = PHP_CURL_STDOUT;
72904 }
82905 break;
5. 透析CURL的执行过程(curl_exec函数)
当设置好curl参数之后 , 就可以执行curl_exec函数,发出用户请求.
13094 /* {{{ proto bool curl_exec(resource ch)
23095 Perform a cURL session */
33096 PHP_FUNCTION(curl_exec)
43097 {
53098 CURLcode error;
63099 zval *zid;
73100 php_curl *ch;
83101
93102 ZEND_PARSE_PARAMETERS_START(1, 1)
103103 Z_PARAM_RESOURCE(zid)
113104 ZEND_PARSE_PARAMETERS_END();
123105 //解析并获取php_curl句柄
133106 if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
143107 RETURN_FALSE;
153108 }
163109 /*
17 检验资源所对应的句柄们,ch属于php_curl类型,类型中包含一些读写资源指针,需要对这些资源进行可
18 用性判断,根据具体情况更新ch所对应的相应参数.
19 */
203110 _php_curl_verify_handlers(ch, 1);
213111 /*
22 由于ch对象能够被复用,所以这部分是对ch进行数据复位工作,主要包括相关缓冲区清空,错误码归置
23 */
243112 _php_curl_cleanup_handle(ch);
253113 /*调用curl库函数进行请求发送
26 * curl内核调用关系:curl_easy_perform -->easy_perform-->easy_transfer
27 */
283114 error = curl_easy_perform(ch->cp);
293115 SAVE_CURL_ERROR(ch, error);
303116 /* CURLE_PARTIAL_FILE is returned by HEAD requests */
313117 if (error != CURLE_OK && error != CURLE_PARTIAL_FILE) {
323118 smart_str_free(&ch->handlers->write->buf);
333119 RETURN_FALSE;
343120 }
353121
363122 if (!Z_ISUNDEF(ch->handlers->std_err)) {
373123 php_stream *stream;
383124 stream = (php_stream*)zend_fetch_resource2_ex(&ch->handlers->std_err, NULL, php_file_le_stream(), php_file_le_pstream());
393125 if (stream) {
403126 php_stream_flush(stream);
413127 }
423128 }
433129 /*下面的这部分 就是判断curl的结果该往哪地方输出,有stdout,file,php_var
44 */
453130 if (ch->handlers->write->method == PHP_CURL_RETURN && ch->handlers->write->buf.s) {
463131 smart_str_0(&ch->handlers->write->buf);
473132 RETURN_STR_COPY(ch->handlers->write->buf.s);
483133 }
493134
503135 /* flush the file handle, so any remaining data is synched to disk */
513136 if (ch->handlers->write->method == PHP_CURL_FILE && ch->handlers->write->fp) {
523137 fflush(ch->handlers->write->fp);
533138 }
543139 if (ch->handlers->write_header->method == PHP_CURL_FILE && ch->handlers->write_header->fp) {
553140 fflush(ch->handlers->write_header->fp);
563141 }
573142
583143 if (ch->handlers->write->method == PHP_CURL_RETURN) {
593144 RETURN_EMPTY_STRING();
603145 } else {
613146 RETURN_TRUE;
623147 }
633148 }
643149 /* }}} */
6. 透析CURL的关闭过程
PHP-CURL的过程化解析,最后一部分是关于CURL的关闭阶段,我来分析一下这部分的内核源代码.
13481 /* {{{ proto void curl_close(resource ch)
23482 Close a cURL session */
33483 PHP_FUNCTION(curl_close)
43484 {
53485 zval *zid;
63486 php_curl *ch;
73487 //解析引擎中资源id
83488 ZEND_PARSE_PARAMETERS_START(1, 1)
93489 Z_PARAM_RESOURCE(zid)
103490 ZEND_PARSE_PARAMETERS_END();
113491 //把void*资源指针转换为 php_curl*指针,如果失败则返回false
123492 if ((ch = (php_curl*)zend_fetch_resource(Z_RES_P(zid), le_curl_name, le_curl)) == NULL) {
133493 RETURN_FALSE;
143494 }
153495 //调用回调函数
163496 if (ch->in_callback) {
173497 php_error_docref(NULL, E_WARNING, "Attempt to close cURL handle from a callback");
183498 return;
193499 }
203500 //销毁资源
213501 zend_list_close(Z_RES_P(zid));
223502 }
233503 /* }}} */
243504
25
26 //销毁zend链表内存,销毁res内存资源
2782 ZEND_API int ZEND_FASTCALL zend_list_close(zend_resource *res)
2883 {
2984 if (GC_REFCOUNT(res) <= 0) {
3085 return zend_list_free(res);
3186 } else if (res->type >= 0) {
3287 zend_resource_dtor(res);
3388 }
3489 return SUCCESS;
3590 }
7.CURL内核分析
1//这部分承载着curl的简单模式下的HTTP请求,curl_exec最终会到这里,当然curl会继续往下走一层,到multi的请求与监控层.
2//curl内核调用关系:curl_easy_perform -->easy_perform-->easy_transfer
3static CURLcode easy_perform(struct Curl_easy *data, bool events)
4{
5 struct Curl_multi *multi;
6 CURLMcode mcode;
7 CURLcode result = CURLE_OK;
8 SIGPIPE_VARIABLE(pipe_st);
9
10 if(!data)
11 return CURLE_BAD_FUNCTION_ARGUMENT;
12 //初始化data的错误buffer
13 if(data->set.errorbuffer)
14 /* clear this as early as possible */
15 data->set.errorbuffer[0] = 0;
16
17 if(data->multi) {
18 failf(data, "easy handle already used in multi handle");
19 return CURLE_FAILED_INIT;
20 }
21
22 if(data->multi_easy)
23 multi = data->multi_easy;
24 else {
25 /* this multi handle will only ever have a single easy handled attached
26 to it, so make it use minimal hashes */
27 multi = Curl_multi_handle(1, 3);
28 if(!multi)
29 return CURLE_OUT_OF_MEMORY;
30 data->multi_easy = multi;
31 }
32
33 if(multi->in_callback)
34 return CURLE_RECURSIVE_API_CALL;
35
36 /* Copy the MAXCONNECTS option to the multi handle */
37 curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, data->set.maxconnects);
38
39 mcode = curl_multi_add_handle(multi, data);
40 if(mcode) {
41 curl_multi_cleanup(multi);
42 if(mcode == CURLM_OUT_OF_MEMORY)
43 return CURLE_OUT_OF_MEMORY;
44 return CURLE_FAILED_INIT;
45 }
46
47 sigpipe_ignore(data, &pipe_st);
48
49 /* assign this after curl_multi_add_handle() since that function checks for
50 it and rejects this handle otherwise */
51 data->multi = multi;
52
53 /* run the transfer */
54 result = events ? easy_events(multi) : easy_transfer(multi);
55
56 /* ignoring the return code isn't nice, but atm we can't really handle
57 a failure here, room for future improvement! */
58 (void)curl_multi_remove_handle(multi, data);
59
60 sigpipe_restore(&pipe_st);
61
62 /* The multi handle is kept alive, owned by the easy handle */
63 return result;
64}
8.PHP-HTTP 请求连接复用
PHP的HTTP请求如果想连接复用,这里讨论的连接复用是在一个一般有两种途径:
这里的连接复用也是指在一个RINIT2–>RSHUTDOWN3周期内复用:
借助外部库,复用外部库所创建的实例(类似于PHP的单体), 外部库可以借助curl
借助php内核的stream_socket_client的STREAM_CLIENT_PERSISTENT
(可以参考predis源码的src/Connection/StreamConnection.php : 169)
9. PHP-CURL组合的HTTP连接复用
通过上面的源码分析,我们可以看出:
- curl_init 阶段会调用curl库创建相关内存区域,
- curl_close阶段会销毁资源
所以 , 在PHP调用curl阶段,我们如果想复用HTTP连接,就必须要把ch改造为单体,不要出现覆盖性创建,也要避免使用curl_close,避免相关资源的销毁,这样复用ch就会复用CURL
虽然复用了CURL,并不能代表能够复用HTTP,因为从源码分析中,我们可以看出http请求是curl帮我们承载的,所以对于连接的管理是curl帮我们做的,根据远端web服务器的http协议,curl会自动判断并选择性的复用连接.
10.PHP-Guzzle组合的HTTP复用
对于很多公司使用的是Guzzle功能包丰富和承载HTTP请求,所以我需要对Guzzle的源码做一些解读.
这里我只解析Guzzle的HTTP的handle选择过程.
10.1. **Guzzle的Client创建流程
1Client ::__construct()-->HandlerStack::create()-->HandlerStack::choose_handler()
10.2. choose_handler()函数解析
1function choose_handler()
2{
3 $handler = null;
4 if (function_exists('curl_multi_exec') && function_exists('curl_exec')) {
5 $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler());
6 } elseif (function_exists('curl_exec')) {
7 $handler = new CurlHandler();
8 } elseif (function_exists('curl_multi_exec')) {
9 $handler = new CurlMultiHandler();
10 }
11
12 if (ini_get('allow_url_fopen')) {
13 $handler = $handler
14 ? Proxy::wrapStreaming($handler, new StreamHandler())
15 : new StreamHandler();
16 } elseif (!$handler) {
17 throw new \RuntimeException('GuzzleHttp requires cURL, the '
18 . 'allow_url_fopen ini setting, or a custom HTTP handler.');
19 }
20
21 return $handler;
22}
choose_handler函数选择stack中的起始handler,选择策略为:
- 扩展自带curl_multi_exec和curl_exec函数则根据$options中的synchronous选项决定,empty(synchronous)为false则使用CurlHandler,否则使用CurlMultiHandler
- 扩展只有curl_exec函数则使用CurlHandler
- 扩展只有curl_multi_exec函数则使用CurlMultiHandler
通过分析这部分源代码,我们可以了解Guzzle也可以通过静态变量的方式来做到复用curl资源,来达到连接复用的目的.
10.3. Guzzle连接复用的样例代码
1//为缩减版代码
2protected static $guzzleClientConnection = null;
3
4 protected function getGuzzleClient($baseUrl, $persistent = true)
5 {
6 if (!$persistent || !self::$guzzleClientConnection) {
7 self::$guzzleClientConnection = new Client(['base_uri' => $baseUrl]);
8 }
9
10 return self::$guzzleClientConnection;
11 }
12
13//获取Client静态变量,复用curl单体
14 $client = $this->getGuzzleClient($base_uri);
15 $headers = [
16 'token' => '',
17 'Content-Type' => 'application/json',
18 'Accept-Encoding' => 'gzip',
19 ];
20
21 $responseBody = '';
22 $httpCode = '';
23 $error = '';
24
25 try {
26 $response = $client->request('POST', $func, [
27 'headers' => $headers,
28 'body' => \GuzzleHttp\json_encode($body),
29 'timeout' => $timeout,
30 'connect_timeout' => $connectTimeout,
31 ]);
32 $httpCode = $response->getStatusCode();
33 $responseBody = $response->getBody();
34 } catch (\Exception $e) {
35 $error = $e->getMessage();
36 }
10.4. 上面的guzzle样例是否复用了CURL对象?
这个问题很关键, 因为我们想借助curl的TCP复用,那就必须要成功的在PHP层复用CURL内核对象.
我们通过上面的源码分析可以知道PHP内核对于curl内核的创建返回使用的是资源类型进行存储,在PHP内核中,资源有个资源ID进行区分 . 因此我们要想验证上述代码是否成功的复用CURL对象,就需要把CURL对象打印出来.
验证上述理论,首先我需要分析一个Guzzle的复用ch对象部分源码:
1 //文件: CurlFactory.php
2 public function create(RequestInterface $request, array $options)
3 {
4 if (isset($options['curl']['body_as_string'])) {
5 $options['_body_as_string'] = $options['curl']['body_as_string'];
6 unset($options['curl']['body_as_string']);
7 }
8
9 $easy = new EasyHandle;
10 $easy->request = $request;
11 $easy->options = $options;
12 $conf = $this->getDefaultConf($easy);
13 $this->applyMethod($easy, $conf);
14 $this->applyHandlerOptions($easy, $conf);
15 $this->applyHeaders($easy, $conf);
16 unset($conf['_headers']);
17
18 // Add handler options from the request configuration options
19 if (isset($options['curl'])) {
20 $conf = array_replace($conf, $options['curl']);
21 }
22
23 $conf[CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
24 //这部分是对handle进行存在性判断,我们在这里加上一行var_dump
25 var_dump($this->handles);
26 $easy->handle = $this->handles
27 ? array_pop($this->handles)
28 : curl_init();
29 curl_setopt_array($easy->handle, $conf);
30
31 return $easy;
32 }
我们调整完guzzle代码后,还需要写一份测试代码,测试代码如下:
1//GuzzleClient.php
2use \GuzzleHttp\Client;
3class GuzzleClient
4{
5 protected static $guzzleClientConnection = null;
6
7 public static function getGuzzleClient($baseUrl, $persistent = true)
8 {
9 if (!$persistent || !self::$guzzleClientConnection) {
10 self::$guzzleClientConnection = new Client(['base_uri' => $baseUrl]);
11 }
12
13 return self::$guzzleClientConnection;
14 }
15
16}
17
18//get_loop_simple.php 内部循环调用多次
19for ($i=0;$i<=10;$i++){
20 try {
21 //获取Client静态变量,复用curl单体
22 $client = GuzzleClient::getGuzzleClient("http://127.0.0.1");
23 $response = $client->request('GET', '/test.php');
24 // var_dump($response->getBody()->getContents());
25 } catch (\Exception $e) {
26 $error = $e->getMessage();
27 var_dump($error);
28 }
29}
我们通过执行上述测试代码可以看到如下的运行结果:
1array(0) {
2}
3array(1) {
4 [0]=>
5 resource(44) of type (curl)
6}
7array(1) {
8 [0]=>
9 resource(44) of type (curl)
10}
11array(1) {
12 [0]=>
13 resource(44) of type (curl)
14}
15array(1) {
16 [0]=>
17 resource(44) of type (curl)
18}
19array(1) {
20 [0]=>
21 resource(44) of type (curl)
22}
23array(1) {
24 [0]=>
25 resource(44) of type (curl)
26}
27array(1) {
28 [0]=>
29 resource(44) of type (curl)
30}
31array(1) {
32 [0]=>
33 resource(44) of type (curl)
34}
35array(1) {
36 [0]=>
37 resource(44) of type (curl)
38}
39array(1) {
40 [0]=>
41 resource(44) of type (curl)
42}
通过上面的运行结果,我们可以除了第一次handle为空,其余每次create过程均没有再次调用curl_init内核函数,而是复用了资源id为44的curl类别资源.