前言
今天这篇文章,主要介绍 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, ¶ms) == 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 的亮点之一,其实现相对同步调用来说,会稍微复杂那么一丢丢。