最近在做一个功能,需要设置mysql查询超时。于是就使用mysqli的options(MYSQL_OPT_READ_TIMEOUT,5)或者options(11,5),但结果却出人意料。
首先,我写了如下的测试代码:
php
$mysqli = mysqli_init();
$r = $mysqli->options(11, 1);
var_dump($r);
$mysqli->real_connect('10.0.3.25', 'test', 'test123', 'test');
$res = $mysqli->query("select sleep(5)");
if (!$res) {
echo "query error: ". $mysqli->error ."\n";
} else {
echo "Query: query success\n";
}
然后在两台机器上执行上面的代码,结果如下:
机器a,php版本5.3.27
机器b,php版本5.2.17
在机器b上,是成功的执行了超时设置,但机器a上并没有成功。于是我便怀疑是php5.3.27的mysqli扩展源码有问题。
于是怀着一颗打破砂锅问到底的心,我打开了php5.3.27的源码。并找到mysqli_options代码(在src/ext/mysqli/mysqli_api.c),代码如下:
PHP_FUNCTION(mysqli_options)
{
MY_MYSQL *mysql;
zval *mysql_link = NULL;
zval **mysql_value;
long mysql_option;
unsigned int l_value;
long ret;
int expected_type;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "OlZ", &mysql_link, mysqli_link_class_entry, &mysql_option, &mysql_value) == FAILURE) {
return;
}
MYSQLI_FETCH_RESOURCE_CONN(mysql, &mysql_link, MYSQLI_STATUS_INITIALIZED);
#if PHP_API_VERSION < 20100412
if ((PG(open_basedir) && PG(open_basedir)[0] != '\0') || PG(safe_mode)) {
#else
if (PG(open_basedir) && PG(open_basedir)[0] != '\0') {
#endif
if(mysql_option == MYSQL_OPT_LOCAL_INFILE) {
RETURN_FALSE;
}
}
expected_type = mysqli_options_get_option_zval_type(mysql_option);
if (expected_type != Z_TYPE_PP(mysql_value)) {
switch (expected_type) {
case IS_STRING:
convert_to_string_ex(mysql_value);
break;
case IS_LONG:
convert_to_long_ex(mysql_value);
break;
default:
break;
}
}
switch (expected_type) {
case IS_STRING:
ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value));
break;
case IS_LONG:
l_value = Z_LVAL_PP(mysql_value);
ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value);
break;
default:
ret = 1;
break;
}
RETURN_BOOL(!ret);
}
问题就出在这块代码:
expected_type = mysqli_options_get_option_zval_type(mysql_option);
其中mysql_option就是我们php代码里面向mysqil_options传的第一个参数。这段代码的作用是通过mysqli_options的第一个参数,得到第二个参数的类型。我们在看一下这个函数mysqli_options_get_option_zval_type(在src/ext/mysqli/mysqli_api.c),代码如下:
static int mysqli_options_get_option_zval_type(int option)
{
switch (option) {
#ifdef MYSQLI_USE_MYSQLND
#if PHP_MAJOR_VERSION >= 6
case MYSQLND_OPT_NUMERIC_AND_DATETIME_AS_UNICODE:
#endif
case MYSQLND_OPT_NET_CMD_BUFFER_SIZE:
case MYSQLND_OPT_NET_READ_BUFFER_SIZE:
#ifdef MYSQLND_STRING_TO_INT_CONVERSION
case MYSQLND_OPT_INT_AND_FLOAT_NATIVE:
#endif
#endif /* MYSQLI_USE_MYSQLND */
case MYSQL_OPT_CONNECT_TIMEOUT:
#ifdef MYSQL_REPORT_DATA_TRUNCATION
case MYSQL_REPORT_DATA_TRUNCATION:
#endif
case MYSQL_OPT_LOCAL_INFILE:
case MYSQL_OPT_NAMED_PIPE:
#ifdef MYSQL_OPT_PROTOCOL
case MYSQL_OPT_PROTOCOL:
#endif /* MySQL 4.1.0 */
#ifdef MYSQL_OPT_READ_TIMEOUT
case MYSQL_OPT_READ_TIMEOUT:
case MYSQL_OPT_WRITE_TIMEOUT:
case MYSQL_OPT_GUESS_CONNECTION:
case MYSQL_OPT_USE_EMBEDDED_CONNECTION:
case MYSQL_OPT_USE_REMOTE_CONNECTION:
case MYSQL_SECURE_AUTH:
#endif /* MySQL 4.1.1 */
#ifdef MYSQL_OPT_RECONNECT
case MYSQL_OPT_RECONNECT:
#endif /* MySQL 5.0.13 */
#ifdef MYSQL_OPT_SSL_VERIFY_SERVER_CERT
case MYSQL_OPT_SSL_VERIFY_SERVER_CERT:
#endif /* MySQL 5.0.23 */
#ifdef MYSQL_OPT_COMPRESS
case MYSQL_OPT_COMPRESS:
#endif /* mysqlnd @ PHP 5.3.2 */
#ifdef MYSQL_OPT_SSL_VERIFY_SERVER_CERT
REGISTER_LONG_CONSTANT("MYSQLI_OPT_SSL_VERIFY_SERVER_CERT", MYSQL_OPT_SSL_VERIFY_SERVER_CERT, CONST_CS | CONST_PERSISTENT);
#endif /* MySQL 5.1.1., mysqlnd @ PHP 5.3.3 */
return IS_LONG;
#ifdef MYSQL_SHARED_MEMORY_BASE_NAME
case MYSQL_SHARED_MEMORY_BASE_NAME:
#endif /* MySQL 4.1.0 */
#ifdef MYSQL_SET_CLIENT_IP
case MYSQL_SET_CLIENT_IP:
#endif /* MySQL 4.1.1 */
case MYSQL_READ_DEFAULT_FILE:
case MYSQL_READ_DEFAULT_GROUP:
case MYSQL_INIT_COMMAND:
case MYSQL_SET_CHARSET_NAME:
case MYSQL_SET_CHARSET_DIR:
return IS_STRING;
default:
return IS_NULL;
}
}
可以看到,这个函数其实挺简单的,就是一个switch case判断传进来的参数,然后返回相应的值。但问题出现了,有这么一段代码:
#ifdef MYSQL_OPT_READ_TIMEOUT
case MYSQL_OPT_READ_TIMEOUT:
case MYSQL_OPT_WRITE_TIMEOUT:
case MYSQL_OPT_GUESS_CONNECTION:
case MYSQL_OPT_USE_EMBEDDED_CONNECTION:
case MYSQL_OPT_USE_REMOTE_CONNECTION:
case MYSQL_SECURE_AUTH:
#endif /* MySQL 4.1.1 */
当我们传入的参数是MYSQL_OPT_READ_TIMEOUT (或11)时,需要定义MYSQL_OPT_READ_TIMEOUT这个宏,才会返回IS_LONG,不然只会执行default,返回IS_NULL。如果返回IS_NULL,mysqli_options函数之后的执行逻辑如下:
expected_type = mysqli_options_get_option_zval_type(mysql_option);
// expected_type的值是IS_NULL,所以下面的switch执行default。
if (expected_type != Z_TYPE_PP(mysql_value)) {
switch (expected_type) {
case IS_STRING:
convert_to_string_ex(mysql_value);
break;
case IS_LONG:
convert_to_long_ex(mysql_value);
break;
default:
break;
}
}
// 到这里expected_type的值还是IS_NULL,所以还是执行default,所以并没有执行mysql_options这个函数
switch (expected_type) {
case IS_STRING:
ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value));
break;
case IS_LONG:
l_value = Z_LVAL_PP(mysql_value);
ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value);
break;
default:
ret = 1;
break;
}
RETURN_BOOL(!ret);
解决方案,定义MYSQL_OPT_READ_TIMEOUT这个宏。另一个方案:使用php5.2.17的做法,不对这个参数已经判断。这个做法就会导致使用mysqli_options时,不能乱传参数,因为没有做过滤。
我这边使用方案二,修改后的代码如下:
PHP_FUNCTION(mysqli_options)
{
MY_MYSQL *mysql;
zval *mysql_link = NULL;
zval **mysql_value;
long mysql_option;
unsigned int l_value;
long ret;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "OlZ", &mysql_link, mysqli_link_class_entry, &mysql_option, &mysql_value) == FAILURE) {
return;
}
MYSQLI_FETCH_RESOURCE(mysql, MY_MYSQL *, &mysql_link, "mysqli_link", MYSQLI_STATUS_INITIALIZED);
if ((PG(open_basedir) && PG(open_basedir)[0] != '\0') || PG(safe_mode)) {
if(mysql_option == MYSQL_OPT_LOCAL_INFILE) {
RETURN_FALSE;
}
}
switch (Z_TYPE_PP(mysql_value)) {
case IS_STRING:
ret = mysql_options(mysql->mysql, mysql_option, Z_STRVAL_PP(mysql_value));
break;
default:
convert_to_long_ex(mysql_value);
l_value = Z_LVAL_PP(mysql_value);
ret = mysql_options(mysql->mysql, mysql_option, (char *)&l_value);
break;
}
RETURN_BOOL(!ret);
}
修改完,如果mysqli在编译php的时候,是编译进php的,那需要重新编译一下php。如果时候通过so这种外部链接的,那就只需要重新编译一下mysqli扩展就可以了。