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请求的连接复用(这里的连接复用也是指在一个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         * 返回简单的句柄。如果出现任何问题,则返回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;
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周期内复用:

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

  2. 借助php内核的stream_socket_clientSTREAM_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类别资源.


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

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

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