前言;
多年以前有个大佬问过一个问题,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;
}