php curl长连接的实现
php的curl库是对http协议的封装,使用起来非常方便,但是每次使用的过程都是:连接——传输——关闭。http连接是在建立于TCP连接之上, 而频繁的关闭和打开TCP连接的成本是非常高的,于是有优化curl的想法,即尽量做到对于同一地址的访问,可以多次复用一个连接。
以下代码是基于php-5.5.10来实现
为了和正常的curl_init函数区别,用curl_pinit函数来做长连接的处理,因为有的curl是临时性的,不需要做长连接的处理。
在ext/curl/php_curl.h中声明:
PHP_FUNCTION(curl_pinit);
在ext/curl/Interface.c 中增加本模块导出的函数:
const zend_function_entry curl_functions[] = {
PHP_FE(curl_init, arginfo_curl_init)
PHP_FE(curl_pinit, arginfo_curl_init)
……
}
在原来的curl句柄结构上增加是否长连接的选项,即在文件
ext/curl/php_curl.h 中修改以下内容
typedef struct {
struct _php_curl_error err;
struct _php_curl_free *to_free;
struct _php_curl_send_headers header;
void ***thread_ctx;
CURL *cp;
php_curl_handlers *handlers;
long id;
zend_bool in_callback;
zval *clone;
zend_bool safe_upload;
zend_bool is_persistent;
} php_curl;
其中is_persistent表示是否长连接
在curl模块初始化的时候注册长连接资源类型
在文件ext/curl/php_curl.h中声明curl长连接资源类型变量:
int le_curl, le_pcurl;
在ext/curl/Interface.c中模块初 始化函数注册le_pcurl长连接资源
PHP_MINIT_FUNCTION(curl)
{
le_curl = zend_register_list_destructors_ex(_php_curl_close, NULL, "curl", module_number);
le_pcurl = zend_register_list_destructors_ex(NULL, _php_curl_pclose, "curl persistent", module_number);
……
}
zend_register_list_destructors_ex函数为三个参数,一是正常连接释构函数,即资源释放的时候被调用的函数,第二个和第一个相同,只不过是长连接,这里指定为:_php_curl_pclose
接下来看下_php_curl_pclose的实现:
/* {{{ _php_curl_close()
List destructor for curl handles */
static void _php_curl_pclose(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
CURL *cp = (CURL *) rsrc->ptr;
curl_easy_setopt(cp, CURLOPT_HEADERFUNCTION, curl_write_nothing);
curl_easy_setopt(cp, CURLOPT_WRITEFUNCTION, curl_write_nothing);
curl_easy_cleanup(cp);
}
/* }}} */
实际上就是调用c的curl库,将头和写函数方法设置为空函数,然后调用curl_easy_cleanup关闭连接
在ext/curl/Interface.c实现 curl_pinit函数
其中粗体是表示连接池的实现
/* {{{ proto resource curl_pinit([string url])
Initialize a persistent cURL session */
PHP_FUNCTION(curl_pinit)
{
php_curl *ch;
CURL *cp;
zval *clone;
char *url = NULL;
int url_len = 0;
char* curl_hashed_details;
int curl_hashed_details_length;
php_url* resource;
long curl_keepidle;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &url, &url_len) == FAILURE) {
return;
}
if (!url) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "url param empty!");
RETURN_FALSE;
}
//解析url,得到shema, host, port
resource = php_url_parse_ex(url, url_len);
if (resource == NULL) {
/* @todo Find a method to determine why php_url_parse_ex() failed */
php_error_docref(NULL TSRMLS_CC, E_WARNING, "parse url error!");
RETURN_FALSE;
}
if (!resource->scheme){
php_error_docref(NULL TSRMLS_CC, E_WARNING, "url schema empty!");
RETURN_FALSE;
}
if (!resource->host){
php_error_docref(NULL TSRMLS_CC, E_WARNING, "url host empty!");
RETURN_FALSE;
}
if (!resource->port){
resource->port = 80;
}
//get from connect pool
zend_rsrc_list_entry *le;
//计算hash key,用协议+主机+端口作为key
curl_hashed_details_length = spprintf(&curl_hashed_details, 0,
"curl__%s_%s:%d", resource->scheme, resource->host,
resource->port);
/* try to find if we already have this link in our persistent list */
if (zend_hash_find(&EG(persistent_list), curl_hashed_details,
curl_hashed_details_length+1, (void **) &le)!=FAILURE) { /* we
don't */
//进到这里,从连接池中取到了长连接,要判断下
if (Z_TYPE_P(le) != le_pcurl) {
efree(curl_hashed_details);
RETURN_FALSE;
}
cp = (CURL *) le->ptr;
}else{
cp = curl_easy_init();
//set persis connect
if (!cp) {
efree(curl_hashed_details);
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Could not initialize a new cURL handle");
RETURN_FALSE;
}
//set persistent
curl_easy_setopt(cp, CURLOPT_FORBID_REUSE, 0L);
curl_easy_setopt(cp, CURLOPT_FRESH_CONNECT, 0L);
//以下代码是设置tcp连接保持的时间,是从php.ini中读取配置项curl.tcp_keepidle,如果没有,则默认为1800秒
#if LIBCURL_VERSION_NUM >= 0x071900 /* Available since 7.25.0 */
curl_easy_setopt(cp, CURLOPT_TCP_KEEPALIVE, 1L):
//从php.init中读取配置,默认为1200秒
curl_keepidle = zend_ini_long('curl.tcp_keepidle', 18, 1800);
if (curl_keepidle < 0){
curl_keepidle = 1800;
}
curl_easy_setopt(cp, CURLOPT_TCP_KEEPIDLE, curl_keepidle);
#endif
//save it to hash
zend_rsrc_list_entry new_le;
/* hash it up */
Z_TYPE(new_le) = le_pcurl;
new_le.ptr = cp;
//保存到连接池中
if (zend_hash_update(&EG(persistent_list), curl_hashed_details,
curl_hashed_details_length+1, (void *) &new_le,
sizeof(zend_rsrc_list_entry), NULL)==FAILURE) {
_php_curl_close_ex(ch TSRMLS_CC);
efree(curl_hashed_details);
RETURN_FALSE;
}
}
//allocate ch structure
alloc_curl_handle(&ch);
TSRMLS_SET_CTX(ch->thread_ctx);
ch->cp = cp;
//mark as persistent
//ch->is_persistent = true;
ch->is_persistent = 1;
ch->handlers->write->method = PHP_CURL_STDOUT;
ch->handlers->read->method = PHP_CURL_DIRECT;
ch->handlers->write_header->method = PHP_CURL_IGNORE;
MAKE_STD_ZVAL(clone);
ch->clone = clone;
_php_curl_set_default_options(ch);
if (!php_curl_option_url(ch, url, url_len TSRMLS_CC)) {
_php_curl_close_ex(ch TSRMLS_CC);
RETURN_FALSE;
}
ZEND_REGISTER_RESOURCE(return_value, ch, le_curl);
//clear resource
efree(curl_hashed_details);
ch->id = Z_LVAL_P(return_value);
}
/* }}} */
最后还要改造_php_curl_close_ex函数,如果是长连接,则不要关闭连接:
static void _php_curl_close_ex(php_curl *ch TSRMLS_DC)
{
#if PHP_CURL_DEBUG
fprintf(stderr, "DTOR CALLED, ch = %x\n", ch);
#endif
_php_curl_verify_handlers(ch, 0 TSRMLS_CC);
/*
* Libcurl is doing connection caching. When easy handle is cleaned up,
* if the handle was previously used by the curl_multi_api, the connection
* remains open un the curl multi handle is cleaned up. Some protocols are
* sending content like the FTP one, and libcurl try to use the
* WRITEFUNCTION or the HEADERFUNCTION. Since structures used in those
* callback are freed, we need to use an other callback to which avoid
* segfaults.
*
* Libcurl commit d021f2e8a00 fix this issue and should be part of 7.28.2
*/
if (!ch->is_persistent){
curl_easy_setopt(ch->cp, CURLOPT_HEADERFUNCTION, curl_write_nothing);
curl_easy_setopt(ch->cp, CURLOPT_WRITEFUNCTION, curl_write_nothing);
curl_easy_cleanup(ch->cp);
}
……
}
ok,curl长连接已经实现了,怎么测试呢:
1、在apache中,如用下方式启动:httpd -X
以上表示单例启动httpd
2、在nginx中,要修改php fpm的配置
pm.max_children=1
然后编写以下测试本, test.php
<?php
$url = 'http://shop.test.cn/';
$ch = curl_pinit($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = curl_exec($ch);
echo strlen($res);
return;
?>
然后不停的刷新这个页面,再用tcpdmp抓下包,看下是不是复用同一个连接,即每次向远程请求的时候,本地的src port是否相同,用netstat 命令查看,最多只有一个连接在使用。
下载地址:http://download.csdn.net/detail/programwithebay/7168115

浙公网安备 33010602011771号