【PHP】 phpredis 長連接實現原理

 前言;

         多年以前有個大佬問過一個問題,PHP的phpredis第三方擴展(客戶端)怎麼實現與redis服務端維持長連接,並且每個請求是怎麼複用這些連接的,今天才突然想一探究竟,便翻了翻一下源碼。PHP源碼版本是php-7.2.19, phpredis擴展版本是redis-5.0.2。

         首先在傳統的網絡通信中,普通的交互流程中,客戶端發起連接請求,三次握手與服務端建立連接,等客戶端做完對應的工作後,會主動關閉連接。

         還有一種是客戶端建立起連接,做完對應的工作後,不會主動關閉連接,這樣就形成了長連接。

         這種長連接應用於phpredis與redis服務端,但是由於一般都是在FPM-CGI模式下,每次請求的過程後,如果不保存已建立的連接資源的話,就不能讓下次請求複用這個長連接,否則還是每個請求都需要重新建立連接。

下面是一張PHP FPM模式下生命週期圖,在FPM模式下,每個CGI進程,PHP的各個模塊(包括第三方擴展模塊)模塊初始化都只會加載一次,並且常駐內存,而請求初始化,是每次請求都會執行一次。

所以在FPM-CGI模式下一些全局變量會一直隨進程的生命週期一直存在,只有在RINIT請求的過程中的一些變量才需要釋放和處理掉。全局的變量和PHP內核內部變量都是用系統內存分配malloc分配,RINIT請求的過程中的所需要分配的內存都是由PHP內存管理器所分配和管理。

下面就貼出一些實際的代碼

PHP測試的腳本代碼

$redis = new Redis();

$redis->pconnect('127.0.0.1', 6379);

$result = $redis->ping();

print_r($result);

exit();

PHP內核實現連接池,建立網絡連接資源都是存放在php_stream結構體包裝

1.EG(persistent_list) 一個全局變量哈希表, 主要是存放以persistent_id爲哈希鍵存儲的連接池 ConnectionPool

2.php_stream是存放在zend_llist list中,所以在FPM-CGI模式下EG(persistent_list) 會一直隨進程的生命週期一直存在,所以不同HTTP請求中,如果是長連接模式下,會從對應的哈希鍵存儲的連接池 ConnectionPool中的連接頭部取出可以複用的連接,直接使用

3.$redis對象銷燬或者主動$redis->close()時纔會把這個連接重新放到連接池鏈表的尾部,從而實現連接池複用連接,減少客戶端重複建立連接。

zend_string *persistent_id = strpprintf(0, "phpredis_%s:%d", ZSTR_VAL(redis_sock->host), redis_sock->port);

typedef struct {
    zend_llist list;
    int nb_active;
} ConnectionPool;


static ConnectionPool *
redis_sock_get_connection_pool(RedisSock *redis_sock)
{
    zend_string *persistent_id = strpprintf(0, "phpredis_%s:%d", ZSTR_VAL(redis_sock->host), redis_sock->port);
    zend_resource *le = zend_hash_find_ptr(&EG(persistent_list), persistent_id);
    if (!le) {
        ConnectionPool *p = pecalloc(1, sizeof(*p) + sizeof(*le), 1);
        zend_llist_init(&p->list, sizeof(php_stream *), NULL, 1);
        le = (zend_resource *)((char *)p + sizeof(*p));
        le->type = le_redis_pconnect;
        le->ptr = p;
        zend_hash_str_update_mem(&EG(persistent_list), ZSTR_VAL(persistent_id), ZSTR_LEN(persistent_id), le, sizeof(*le));
    }
    zend_string_release(persistent_id);
    return le->ptr;
}

phpredis擴展源碼,模塊初始化過程,主要做了註冊Redis配置,還有類,常量等工作

/**
 * PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(redis)
{
    struct timeval tv;

    zend_class_entry redis_class_entry;
    zend_class_entry redis_array_class_entry;
    zend_class_entry redis_cluster_class_entry;
    zend_class_entry redis_exception_class_entry;
    zend_class_entry redis_cluster_exception_class_entry;

    zend_class_entry *exception_ce = NULL;

    /* Seed random generator (for RedisCluster failover) */
    gettimeofday(&tv, NULL);
    srand(tv.tv_usec * tv.tv_sec);

    REGISTER_INI_ENTRIES();

    /* Redis class */
    INIT_CLASS_ENTRY(redis_class_entry, "Redis", redis_functions);
    redis_ce = zend_register_internal_class(&redis_class_entry);
    redis_ce->create_object = create_redis_object;

    /* RedisArray class */
    INIT_CLASS_ENTRY(redis_array_class_entry, "RedisArray", redis_array_functions);
    redis_array_ce = zend_register_internal_class(&redis_array_class_entry);
    redis_array_ce->create_object = create_redis_array_object;

    /* RedisCluster class */
    INIT_CLASS_ENTRY(redis_cluster_class_entry, "RedisCluster", redis_cluster_functions);
    redis_cluster_ce = zend_register_internal_class(&redis_cluster_class_entry);
    redis_cluster_ce->create_object = create_cluster_context;

    /* Register our cluster cache list item */
    le_cluster_slot_cache = zend_register_list_destructors_ex(NULL, cluster_cache_dtor,
                                                              "Redis cluster slot cache",
                                                              module_number);

    /* Base Exception class */
    exception_ce = zend_hash_str_find_ptr(CG(class_table), "RuntimeException", sizeof("RuntimeException") - 1);
    if (exception_ce == NULL) {
        exception_ce = zend_exception_get_default();
    }

    /* RedisException class */
    INIT_CLASS_ENTRY(redis_exception_class_entry, "RedisException", NULL);
    redis_exception_ce = zend_register_internal_class_ex(
        &redis_exception_class_entry,
        exception_ce);

    /* RedisClusterException class */
    INIT_CLASS_ENTRY(redis_cluster_exception_class_entry,
        "RedisClusterException", NULL);
    redis_cluster_exception_ce = zend_register_internal_class_ex(
        &redis_cluster_exception_class_entry, exception_ce);

    /* Add shared class constants to Redis and RedisCluster objects */
    add_class_constants(redis_ce, 0);
    add_class_constants(redis_cluster_ce, 1);

#ifdef PHP_SESSION
    php_session_register_module(&ps_mod_redis);
    php_session_register_module(&ps_mod_redis_cluster);
#endif

    /* Register resource destructors */
    le_redis_pconnect = zend_register_list_destructors_ex(NULL, redis_connections_pool_dtor,
        "phpredis persistent connections pool", module_number);

    return SUCCESS;
}



/* {{{ proto boolean Redis::pconnect(string host, int port [, double timeout])
 */
//void zim_Redis_pconnect(zend_execute_data *execute_data, zval *return_value)
PHP_METHOD(Redis, pconnect)
{
    if (redis_connect(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1) == FAILURE) {
        RETURN_FALSE;
    } else {
        RETURN_TRUE;
    }
}
/* }}} */

PHP_REDIS_API int
redis_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent)
{
    zval *object;
    char *host = NULL, *persistent_id = NULL;
    zend_long port = -1, retry_interval = 0;
    size_t host_len, persistent_id_len;
    double timeout = 0.0, read_timeout = 0.0;
    redis_object *redis;

#ifdef ZTS
    /* not sure how in threaded mode this works so disabled persistence at
     * first */
    persistent = 0;
#endif

    if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(),
                                     "Os|lds!ld", &object, redis_ce, &host,
                                     &host_len, &port, &timeout, &persistent_id,
                                     &persistent_id_len, &retry_interval,
                                     &read_timeout) == FAILURE)
    {
        return FAILURE;
    }

    /* Disregard persistent_id if we're not opening a persistent connection */
    if (!persistent) {
        persistent_id = NULL;
    }

    if (timeout < 0L || timeout > INT_MAX) {
        REDIS_THROW_EXCEPTION("Invalid connect timeout", 0);
        return FAILURE;
    }

    if (read_timeout < 0L || read_timeout > INT_MAX) {
        REDIS_THROW_EXCEPTION("Invalid read timeout", 0);
        return FAILURE;
    }

    if (retry_interval < 0L || retry_interval > INT_MAX) {
        REDIS_THROW_EXCEPTION("Invalid retry interval", 0);
        return FAILURE;
    }

    /* If it's not a unix socket, set to default */
    if(port == -1 && host_len && host[0] != '/') {
        port = 6379;
    }

    if (port < 0) {
        port = 0;
    }

    redis = PHPREDIS_GET_OBJECT(redis_object, object);
    /* if there is a redis sock already we have to remove it */
    if (redis->sock) {
        redis_sock_disconnect(redis->sock, 0);
        redis_free_socket(redis->sock);
    }

    redis->sock = redis_sock_create(host, host_len, port, timeout, read_timeout, persistent,
        persistent_id, retry_interval);

    if (redis_sock_server_open(redis->sock) < 0) {
        if (redis->sock->err) {
            REDIS_THROW_EXCEPTION(ZSTR_VAL(redis->sock->err), 0);
        }
        redis_free_socket(redis->sock);
        redis->sock = NULL;
        return FAILURE;
    }

    return SUCCESS;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章