php源码阅读----php5.3.27 mysqli扩展bug及修复

最近在做一个功能,需要设置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扩展就可以了。

你可能感兴趣的:(php)