前言;
多年以前有個大佬問過一個問題,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;
}