Yar 源码阅读笔记:客户端的同步调用

2022-01-07
4分钟阅读时长

前言

今天这篇文章,主要介绍 Yar 客户端是如何实现远程调用的,进一步了解各个模块在远程调用的过程中都做了些什么。

客户端介绍

Yar 客户端的远程调用分为同步调用和并行调用,同步调用是指调用多个远程方法时,必须按照调用顺序一个个地执行,上一个调用没有执行完时,后面的调用必须等待前面的执行完毕,这期间啥也不能干,效率比较低。

这时候就有了并行调用,从名字就可以看出来,并行调用支持同时调用多个远程方法。它会先将所有的调用请发送出去,让服务端开始处理这些请求,然后客户开始端监听每个请求的响应结果,当这些请求中有任何一个请求有响应结果时,执行该请求的回调函数处理相应的业务逻辑。

这篇文章主要介绍同步调用的实现,所以在本文中提到 “远程调用” 都是指同步调用。

我们先来看看开篇中客户端同步调用的例子。

// 实例化客户端
$client = new Yar_Client("http://127.0.0.1:3000/server.php");

// 设置超时时间
$client->setOpt(YAR_OPT_CONNECT_TIMEOUT, 1000);

// 调用 login 方法
$result = $client->login("her-cat", "123456");

var_dump($result);

通过上面的例子可以知道,客户端中有三个主要方法:

  • 构造函数:用于传入服务端的地址以及可选配置。
  • setOpt:设置配置项,比如连接超时时间等。
  • 远程方法:调用服务端定义好的方法,这里是 login。

当然客户端类还有一些的方法以及属性,可以在定义客户端类的地方找到:

// source:yar_client.c

// 客户端方法列表
zend_function_entry yar_client_methods[] = {
    PHP_ME(yar_client, __construct, arginfo_client___construct, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR|ZEND_ACC_FINAL)
    PHP_ME(yar_client, call, arginfo_client___call, ZEND_ACC_PUBLIC)
    PHP_ME(yar_client, __call, arginfo_client___call, ZEND_ACC_PUBLIC)
    PHP_ME(yar_client, getOpt, arginfo_client_getopt, ZEND_ACC_PUBLIC)
    PHP_ME(yar_client, setOpt, arginfo_client_setopt, ZEND_ACC_PUBLIC)
    PHP_FE_END
};

YAR_STARTUP_FUNCTION(client) /* {{{ */ {
    zend_class_entry ce;

    INIT_CLASS_ENTRY(ce, "Yar_Client", yar_client_methods);
    yar_client_ce = zend_register_internal_class(&ce);

    // 声明客户端的属性
    zend_declare_property_long(yar_client_ce, ZEND_STRL("_protocol"), YAR_CLIENT_PROTOCOL_HTTP, ZEND_ACC_PROTECTED);
    zend_declare_property_null(yar_client_ce, ZEND_STRL("_uri"), ZEND_ACC_PROTECTED);
    zend_declare_property_null(yar_client_ce, ZEND_STRL("_options"),  ZEND_ACC_PROTECTED);
    zend_declare_property_null(yar_client_ce, ZEND_STRL("_running"),  ZEND_ACC_PROTECTED);

    // 注册协议常量
    REGISTER_LONG_CONSTANT("YAR_CLIENT_PROTOCOL_HTTP", YAR_CLIENT_PROTOCOL_HTTP, CONST_PERSISTENT | CONST_CS);
    REGISTER_LONG_CONSTANT("YAR_CLIENT_PROTOCOL_TCP", YAR_CLIENT_PROTOCOL_TCP, CONST_PERSISTENT | CONST_CS);
    REGISTER_LONG_CONSTANT("YAR_CLIENT_PROTOCOL_UNIX", YAR_CLIENT_PROTOCOL_UNIX, CONST_PERSISTENT | CONST_CS);

    return SUCCESS;
}

下面我们重点来看看客户端远程调用的实现,客户端中的其它方法比较简单,这里就不赘述了。

同步调用

熟悉 PHP 的应该都知道,当我们调用对象中未定义或不可访问的方法时,PHP 会尝试调用魔术方法 __call,当 __call 方法也未定义时,将会抛出调用未定义方法的异常。

class A 
{
    public function __call($name, $arguments)
    {
        echo "调用 A 的 '{$name}' 方法,参数是:" . implode(',', $arguments) . "\n";
    }
}

$a = new A();
$a->login("her-cat", "123456");

// 输出:调用 A 的 login 方法,参数是:her-cat,123456

Yar 就是通过 __call 实现了远程调用。

虽然在代码中看起来调用的是客户端的方法,但实际上是通过 __call 魔术方法,将调用的请求转发到服务端上,让服务端执行对应的方法,最后将执行的结果返回,完成了整个远程调用。

下面是客户端执行同步调用时的调用栈。

Client::__call

在上面的客户端介绍中的客户端方法列表中,可以看到 Yar 实现了 __call 和 call 方法。

// source:yar_client.c

/* {{{ proto Yar_Client::call($method, $parameters = NULL) */
PHP_METHOD(yar_client, call) {
    PHP_MN(yar_client___call)(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
/* }}} */

call 函数实际上是 __call 函数的一层包装,可以理解为别名方法。

// source:yar_client.c

PHP_METHOD(yar_client, __call) {
    zval *params, *protocol, rv;
    zval *this_ptr = getThis();

    // 解析参数,被调用的方法名称和参数数组
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "Sa", &method, &params) == FAILURE) {
        return;
    }

    // 读取使用的协议
    protocol = zend_read_property(yar_client_ce, this_ptr, ZEND_STRL("_protocol"), 0, &rv);

    // 根据协议调用对应的方法
    switch (Z_LVAL_P(protocol)) {
        case YAR_CLIENT_PROTOCOL_TCP:
        case YAR_CLIENT_PROTOCOL_UNIX:
        case YAR_CLIENT_PROTOCOL_HTTP:
            if ((php_yar_client_handle(Z_LVAL_P(protocol), getThis(), method, params, return_value))) {
                return;
            }
            break;
        default:
            php_error_docref(NULL, E_WARNING, "unsupported protocol %ld", Z_LVAL_P(protocol));
            break;
    }

    RETURN_FALSE;
}

__call 函数的内容比较简单,先解析参数,得到被调用的方法名称和参数数组,然后从客户端对象中读取协议,然后根据协议调用对应的函数,当遇到不支持的协议时,提示错误信息并返回 false。

php_yar_client_handle

php_yar_client_handle 函数包含了整个调用的过程,它就是客户端实现远程调用的核心所在。

// source:yar_client.c

static int php_yar_client_handle(int protocol, zval *client, zend_string *method, zval *params, zval *retval) {
    char *msg;
    zval *uri, *options;
    zval rv;
    const yar_transport_t *factory;
    yar_transport_interface_t *transport;
    yar_request_t *request;
    yar_response_t *response;
    int flags = 0;
    zval *zobj = client;

    // 读取服务端的地址
    uri = zend_read_property(yar_client_ce, zobj, ZEND_STRL("_uri"), 0, &rv);

    // 通过协议获取对应的传输方式
    if (protocol == YAR_CLIENT_PROTOCOL_HTTP) {
        factory = php_yar_transport_get(ZEND_STRL("curl"));
    } else if (protocol == YAR_CLIENT_PROTOCOL_TCP || protocol == YAR_CLIENT_PROTOCOL_UNIX) {
        factory = php_yar_transport_get(ZEND_STRL("sock"));
    } else {
        return 0;
    }

    // 初始化传输方式
    transport = factory->init();

    options = zend_read_property(yar_client_ce, zobj, ZEND_STRL("_options"), 1, &rv);

    if (IS_ARRAY != Z_TYPE_P(options)) {
        options = NULL;
    }
    
    // 根据被调用的方法名称、参数等信息组装请求数据
    if (UNEXPECTED(!(request = php_yar_request_instance(method, params, options)))) {
        transport->close(transport);
        factory->destroy(transport);
        return 0;
    }

    if (options) {
        zval *flag = php_yar_client_get_opt(options, YAR_OPT_PERSISTENT);
        if (flag && (Z_TYPE_P(flag) == IS_TRUE || (Z_TYPE_P(flag) == IS_LONG && Z_LVAL_P(flag)))) {
            flags |= YAR_PROTOCOL_PERSISTENT;
        }
    }


    msg = (char*)options;
    // 打开/创建一个连接
    if (UNEXPECTED(!transport->open(transport, Z_STR_P(uri), flags, &msg))) {
        php_yar_client_trigger_error(1, YAR_ERR_TRANSPORT, msg);
        php_yar_request_destroy(request);
        ZEND_ASSERT(msg != (char*)options);
        efree(msg);
        transport->close(transport);
        factory->destroy(transport);
        return 0;
    }

    // 对请求数据进行编码、组装等操作,并将处理后的数据保存到连接中
    if (UNEXPECTED(!transport->send(transport, request, &msg))) {
        php_yar_client_trigger_error(1, YAR_ERR_TRANSPORT, msg);
        php_yar_request_destroy(request);
        efree(msg);
        transport->close(transport);
        factory->destroy(transport);
        return 0;
    }

    // 执行请求
    response = transport->exec(transport, request);

    if (UNEXPECTED(response->status != YAR_ERR_OKEY)) {
        // 调用失败则返回异常响应
        php_yar_client_handle_error(1, response);
        php_yar_request_destroy(request);
        php_yar_response_destroy(response);
        transport->close(transport);
        factory->destroy(transport);
        return 0;
    } else {
        if (response->out && ZSTR_LEN(response->out)) {
            PHPWRITE(ZSTR_VAL(response->out), ZSTR_LEN(response->out));
        }
        // 调用成功则将响应值拷贝到返回值中。
        ZVAL_COPY(retval, &response->retval);
        php_yar_request_destroy(request);
        php_yar_response_destroy(response);
        transport->close(transport);
        factory->destroy(transport);
        return 1;
    }
}

php_yar_client_handle 函数的主要逻辑都写上注释了,transport 调用的那些函数,就是上一篇文章中的那些 curl 函数,我们可以使用 gdb 打印 transport 的值进行验证。

(gdb) p *transport
$8 = {data = 0x7ffff5481000, open = 0x7ffff56ecd4b <php_yar_curl_open>, send = 0x7ffff56ee293 <php_yar_curl_send>,
  exec = 0x7ffff56eddc0 <php_yar_curl_exec>, setopt = 0x7ffff56ee78f <php_yar_curl_setopt>,
  calldata = 0x7ffff56ee761 <php_yar_curl_set_calldata>, close = 0x7ffff56edaa1 <php_yar_curl_close>}

总结

下一篇文章将会介绍客户端并行调用的实现,并行调用也是 Yar 的亮点之一,其实现相对同步调用来说,会稍微复杂那么一丢丢。

本文作者:她和她的猫
本文地址https://her-cat.com/posts/2022/01/07/yar-internals-client-sync-call/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!