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

posted @ 2015-06-21 20:51  szphper  阅读(3417)  评论(0)    收藏  举报