添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
PHP内核

PHP-CURL-Guzzle-HTTP-连接复用内核原理

Posted by LB on Fri, Feb 22, 2019

PHP-CURL连接复用内核原理

0.写在前面

PHP是一个时代的产物,它的底层支持是C语言,因此它在CPU密集型计算或者系统内核调用上有天生的优势,Zend引擎把PHP执行生命期分成了五个阶段 1 ,这五个阶段并不是全部都能常驻进程,这种模式下,对于很多使用场景会造成不好的影响,比如网络IO.

对于网络IO中的HTTP请求 , 很多工程师使用 php-curl 系列函数 . 所以这篇文章将从内核角度讲解php如何支持curl请求的连接复用(这里的连接复用也是指在一个RINIT 2 –>RSHUTDOWN 3 周期内复用).

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结构体)

3. 透析CURL初始化阶段(curl_init函数)

curl_init是php调用curl的开端,之所以要分析这个函数,因为这个函数中就包括PHP如何调用curl库函数,如何对curl进行参数设置, 代码的解析通过注释的方式 , 内核源码如下:

 137  #include <curl/curl.h>  //引入curl库头文件
 238  #include <curl/easy.h>
 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         * 返回简单的句柄。如果出现任何问题,则返回NULL20         */
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;
30




    
2039  
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对象的默认参数
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句柄中的参数进行丰富性设置,下面我们就来看一下内核这部分源代码:

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的关闭阶段,我来分析一下这部分的内核源代码.

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)
 5




    
  struct Curl_multi *multi;
 6  CURLMcode mcode;
 7  CURLcode result = CURLE_OK;
 8  SIGPIPE_VARIABLE(pipe_st);
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;
17  if(data->multi) {
18    failf(data, "easy handle already used in multi handle");
19    return CURLE_FAILED_INIT;
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;
33  if(multi->in_callback)
34    return CURLE_RECURSIVE_API_CALL;
36  /* Copy the MAXCONNECTS option to the multi handle */
37  curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, data->set.maxconnects);
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;
47  sigpipe_ignore(data, &pipe_st);
49  /* assign this after curl_multi_add_handle() since that function checks for
50     it and rejects this handle otherwise */
51  data->multi = multi;
53  /* run the transfer */
54  result = events ? easy_events(multi) : easy_transfer(multi);
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);
60  sigpipe_restore(&pipe_st);
62  /* The multi handle is kept alive, owned by the easy handle */
63  return result;

8.PHP-HTTP 请求连接复用

PHP的HTTP请求如果想连接复用,这里讨论的连接复用是在一个一般有两种途径:

这里的连接复用也是指在一个RINIT 2 –>RSHUTDOWN 3 周期内复用:

  1. 借助外部库,复用外部库所创建的实例(类似于PHP的单体), 外部库可以借助curl

  2. 借助php内核的 stream_socket_client STREAM_CLIENT_PERSISTENT

    (可以参考predis源码的src/Connection/StreamConnection.php : 169)

9. PHP-CURL组合的HTTP连接复用

通过上面的源码分析,我们可以看出:

所以 , 在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创建流程

10.2. choose_handler()函数解析

choose_handler函数选择stack中的起始handler,选择策略为:

通过分析这部分源代码,我们可以了解 Guzzle也可以通过静态变量的方式来做到复用curl资源,来达到连接复用的目的 .

10.3. Guzzle连接复用的样例代码

10.4. 上面的guzzle样例是否复用了CURL对象?

​ 这个问题很关键, 因为我们想借助curl的TCP复用,那就必须要成功的在PHP层复用CURL内核对象.

​ 我们通过上面的源码分析可以知道PHP内核对于curl内核的创建返回使用的是资源类型进行存储,在PHP内核中,资源有个资源ID进行区分 . 因此我们要想验证上述代码是否成功的复用CURL对象,就需要把CURL对象打印出来.

​ 验证上述理论,首先我需要分析一个Guzzle的复用ch对象部分源码:

​ 我们调整完guzzle代码后,还需要写一份测试代码,测试代码如下:

​ 我们通过执行上述测试代码可以看到如下的运行结果:

通过上面的运行结果,我们可以除了第一次handle为空,其余每次create过程均没有再次调用curl_init内核函数,而是复用了资源id为44的curl类别资源.


  1. PHP生命期包括: MINIT , RINIT , PHP_EXECUTE_SCRIPT, RSHUTDOWN , MSHUTDOWN ↩︎

  2. RINIT代表 PHP引擎中的request startup阶段,指请求初始化阶段. ↩︎ ↩︎

  3. RSHUTDOWN代表PHP引擎中的request shutdown阶段,指请求关闭阶段. ↩︎ ↩︎