惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
雷峰网
雷峰网
Recent Announcements
Recent Announcements
月光博客
月光博客
G
Google Developers Blog
腾讯CDC
S
Secure Thoughts
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
云风的 BLOG
云风的 BLOG
W
WeLiveSecurity
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 聂微东
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
MyScale Blog
MyScale Blog
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
aimingoo的专栏
aimingoo的专栏
I
Intezer
Vercel News
Vercel News
小众软件
小众软件
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
Latest news
Latest news
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tor Project blog
S
Security Affairs
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
美团技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
L
LINUX DO - 热门话题
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
D
Docker
L
Lohrmann on Cybersecurity
F
Full Disclosure

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 浅析 Redlock 分布式锁实现原理 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
Yar 源码阅读笔记:客户端的同步调用
她和她的猫 · 2022-01-07 · via 文章列表 - 她和她的猫

前言

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