【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;
}

 

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