Yar 源码阅读笔记:消息编码模块

2021-12-23
8分钟阅读时长

前言

上一篇文章 中,我们知道了 Yar 通信协议的格式及作用,还提到了在 Yar 客户端发送请求前和收到响应后,需要先对数据进行编码与解码才能继续进一步操作。

今天我们来了解一下,Yar 中的消息编码模块是怎样实现的。

什么是编码/解码

在介绍 Yar 编码模块之前,我们先来了解一下,什么是编码与解码?

编码(encode)和解码(decode)也有人叫作序列化(serialization)和反序列化(deserialization),其实都是一个意思。

为了统一描述,在后文中都称为编码和解码。

编码是指将数据结构或对象转换成可以存储或传输的数据格式(字节流)。

解码是编码的反向操作,将字节流转换为数据数据结构或对象。

举个例子,假如要将 Person 对象写入到文件中,然后再从文件中还原该对象,应该怎么处理?

class Person 
{
    public $name;
    
    public $age;
}

$person = new Person();
$person->name = 'her-cat';
$person->age = 18;

PHP 提供了两个函数: serialize(编码)和 unserialize(解码)。

serialize 函数可以将除了 resource 类型以外的数据编码为字符串(字节流)。

unserialize 函数可以将编码后的字符串转换为 PHP 的值。

$encoded = file_get_contents('person.txt');
var_dump($encoded);

$decodedPerson = unserialize($encoded);

var_dump($decodedPerson);
var_dump($decodedPerson->name);
var_dump($decodedPerson->age);
string(57) "O:6:"Person":2:{s:4:"name";s:7:"her-cat";s:3:"age";i:18;}"
object(Person)#2 (2) {
  ["name"]=>
  string(7) "her-cat"
  ["age"]=>
  int(18)
}
string(7) "her-cat"
int(18)

为什么需要编码/解码

目的就是为了在不同的编程语言或不同的载体之间交换数据。

上面将对象写入到文件中就是不同载体之间交换数据的例子,这里再举一个不同语言之间的例子。

就拿 PHP 和 JavaScript 来说,我们用 PHP 写了一个 HTTP 接口给 JavaScript 调用,然后 JavaScript 解析 PHP 响应的内容。

PHP 和 JavaScript 是两种不同的编程语言(废话),两者各种数据结构的实现是不一样的,在内存上的组织方式也不一样,所以它们不能直接使用内存中的数据进行数据交换。

就好比两个人在交流,一个人讲蒙古语,另一个人讲粤语,最后两人都不知道对方讲的啥玩意。

所以,我们需要一个中间人帮忙翻译,或者使用两个人都会的一种语言进行交流,比如普通话。

将 “普通话” 代入到上面的例子中,实际上就是引用一种通用的数据编码格式,让 PHP 与 JavaScript 能够进行“交流”。

假设我们使用 JSON,PHP 将数据发送到前端之前,先调用 json_encode 将数据转换成 JSON 字符串(这个过程就是编码), 前端页面收到 PHP 返回的 JSON 字符串后,将其解析成前端的 JavaScript 的数组/对象(这个过程就是解码)。

Yar 编码模块简介

Yar 提供了三种编码方式:分别是 PHP、JSON、Msgpack。

PHP 是 PHP 内置的一种对数据结构/对象编码的方式,实际上就是使用 serialize 和 unserialize 这对函数,可以参考上面的例子。

JSON 是目前比较通用且流行的一种数据编码格式,由于其简单、易读的特点,所以被广泛地用于 API 接口、JSON-RPC、数据存储等地方。

但是,JSON 也不是一点儿缺点都没有的,JSON 为了保证其可读性,需要多使用一些内存来保存相关信息,所以在数据传输的过程中,占用的内存就会比 Msgpack 这类编码格式要大一些。

Msgpack 一种高效的二进制编码格式,有点儿类似于 JSON,但是比 JSON 占用内存更小、效率更高。

前两种方式在 Yar 中已经内置支持了(直接调用相关的 PHP 函数),Msgpack 则需要自己手动安装扩展,并在编译时指定。

编码模块结构体

Yar 定义了编码模块结构体:yar_packager_t。

// source:yar_packager.h

typedef struct _yar_packager {
    const char *name;
    int  (*pack) (const struct _yar_packager *self, zval *pzval, smart_str *buf, char **msg);
    zval * (*unpack) (const struct _yar_packager *self, char *content, size_t len, char **msg, zval *ret);
} yar_packager_t;

结构体中字段说明:

  • name:编码方式的名称,比如 PHP、JSON、Msgpack。
  • pack:函数指针,对数据进行编码操作。
  • unpack:函数指针,对数据进行解码操作。

yar_packager_t 结构体有点类似于 OOP 中的抽象类,为不同的编码方式提供了统一的接口,从而实现了多态。

如果我们想要扩展一种编码方式,只需要设置编码方式名称并实现 pack 和 unpack 函数即可。

JSON 编码方式

Yar 支持的编码方式都在 packagers 目录下,这里我们用 JSON 方式进行举例。

在 packagers/json.c 文件中定义了一个类型为 yar_packager_t 的常量:yar_packager_json。

const yar_packager_t yar_packager_json = {
    "JSON",
    php_yar_packager_json_pack,
    php_yar_packager_json_unpack
};

首先是编码方式名称 JSON,然后是编码和解码对应的函数,这两个函数其实是对 json_encode 和 json_decode 函数的一层封装。

int php_yar_packager_json_pack(const yar_packager_t *self, zval *pzval, smart_str *buf, char **msg) {
    php_json_encode(buf, pzval, 0);
    return 1;
}

zval * php_yar_packager_json_unpack(const yar_packager_t *self, char *content, size_t len, char **msg, zval *ret) {
    php_json_decode(ret, content, len, 1, 512);
    return ret;
}

其它两种编码方式的实现跟 JSON 差不多,这里就不赘述了。

编码模块的生命周期

Yar 作为 PHP 的扩展,其编码模块的生命周期肯定也是在 PHP 生命周期中的,这里我画了一张图,方便大家能够更直观的了解 Yar 编码模块的生命周期。

接下来看看编码模块在每个阶段中都做了些什么。

启动编码模块

在 PHP 模块初始化阶段,Yar 会先注册一些 Yar 相关的 PHP 常量,比如版本号、客户端相关的选项。紧接着开始初始化模块,除了编码模块以外,还会初始化传输、客户端、服务端及异常等模块。

// source:yar.c

PHP_MINIT_FUNCTION(yar)
{
    // 注册 PHP 常量
    REGISTER_INI_ENTRIES();
    REGISTER_STRINGL_CONSTANT("YAR_VERSION", PHP_YAR_VERSION, sizeof(PHP_YAR_VERSION)-1, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_PACKAGER", YAR_OPT_PACKAGER, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_PERSISTENT", YAR_OPT_PERSISTENT, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_TIMEOUT", YAR_OPT_TIMEOUT, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_CONNECT_TIMEOUT", YAR_OPT_CONNECT_TIMEOUT, CONST_CS|CONST_PERSISTENT);

    REGISTER_LONG_CONSTANT("YAR_OPT_HEADER", YAR_OPT_HEADER, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_RESOLVE", YAR_OPT_RESOLVE, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("YAR_OPT_PROXY", YAR_OPT_PROXY, CONST_CS|CONST_PERSISTENT);

    // 初始化模块
    YAR_STARTUP(service);
    YAR_STARTUP(client);
    YAR_STARTUP(packager);
    YAR_STARTUP(transport);
    YAR_STARTUP(exception);

    return SUCCESS;
}

今天我们主要研究编码模块,其它几个模块放到后面的文章中。

YAR_STARTUP 是一个宏,定义如下:

#define YAR_STARTUP(module)    ZEND_MODULE_STARTUP_N(yar_##module)(INIT_FUNC_ARGS_PASSTHRU)

/* Name macros */
#define ZEND_MODULE_STARTUP_N(module)    zm_startup_##module

YAR_STARTUP(packager) 展开之后是这样的:

zm_startup_yar_packager(type, module_number)

如果用 zm_startup_yar_packager 在 Yar 源码中是搜不到结果的,这是因为宏只有在编译的时候才会展开,那么为什么用 YAR_STARTUP(packager) 也搜不到呢?

因为在定义函数名的时候用的不是 YAR_STARTUP,而是 YAR_STARTUP_FUNCTION,先来看看定义:

#define YAR_STARTUP_FUNCTION(module)    ZEND_MINIT_FUNCTION(yar_##module)

#define ZEND_MINIT_FUNCTION    ZEND_MODULE_STARTUP_D

/* Declaration macros */
#define ZEND_MODULE_STARTUP_D(module)    zend_result ZEND_MODULE_STARTUP_N(module)(INIT_FUNC_ARGS)

/* Name macros */
#define ZEND_MODULE_STARTUP_N(module)    zm_startup_##module

可以看到,最终使用的都是 ZEND_MODULE_STARTUP_N,它还有一个相似的宏:ZEND_MODULE_STARTUP_D,不同的是,前者是用于命名的宏,无参数,而后者是用于声明/定义的宏,有参数。

在 YAR_STARTUP_FUNCTION(packager) 函数中,先调用 php_yar_packager_register 函数往 Yar 编码模块中注册编码方式,然后往 PHP 中注册编码模块相关的 PHP 常量。

// source:yar_packager.c

YAR_STARTUP_FUNCTION(packager) {
  // 注册编码方式
#ifdef ENABLE_MSGPACK
  php_yar_packager_register(&yar_packager_msgpack);
#endif
  php_yar_packager_register(&yar_packager_php);
  php_yar_packager_register(&yar_packager_json);

  // 注册编码模块相关的 PHP 常量
  REGISTER_STRINGL_CONSTANT("YAR_PACKAGER_PHP", YAR_PACKAGER_PHP, sizeof(YAR_PACKAGER_PHP)-1, CONST_CS|CONST_PERSISTENT);
  REGISTER_STRINGL_CONSTANT("YAR_PACKAGER_JSON", YAR_PACKAGER_JSON, sizeof(YAR_PACKAGER_JSON)-1, CONST_CS|CONST_PERSISTENT);
#ifdef ENABLE_MSGPACK
  REGISTER_STRINGL_CONSTANT("YAR_PACKAGER_MSGPACK", YAR_PACKAGER_MSGPACK, sizeof(YAR_PACKAGER_MSGPACK)-1, CONST_CS|CONST_PERSISTENT);
#endif

  return SUCCESS;
}

接着我们来了解一下注册编码方式的逻辑,Yar 在编码模块中定义了一个结构体 yar_packagers_list,用于存储所有可用的编码方式。

// source:yar_packager.c

struct _yar_packagers_list {
    unsigned int size;
    unsigned int num;
    const yar_packager_t **packagers;
} yar_packagers_list;

结构体字段说明:

  • size:packagers 数组的大小。
  • num:packagers 数组中编码方式的数量。
  • packagers:编码方式的指针数组。

在 php_yar_packager_register 函数中,对 yar_packagers_list 进行初始化、扩容及注册编码方式等操作。

// source:yar_packager.c

PHP_YAR_API int php_yar_packager_register(const yar_packager_t *packager) {

    if (!yar_packagers_list.size) {
       // size 为 0,说明未初始化
       // 设置 size 初始值为 5,并为 packagers 分配相应的内存
       yar_packagers_list.size = 5;
       yar_packagers_list.packagers = (const yar_packager_t **)malloc(sizeof(yar_packager_t *) * yar_packagers_list.size);
    } else if (yar_packagers_list.num == yar_packagers_list.size) {
       // num 等于 size,说明数组已经满了,需要进行扩容
       yar_packagers_list.size += 5;
       yar_packagers_list.packagers = (const yar_packager_t **)realloc(yar_packagers_list.packagers, sizeof(yar_packager_t *) * yar_packagers_list.size);
    }
    
    // 将编码方式存储到 packagers 数组下标为 num 处
    yar_packagers_list.packagers[yar_packagers_list.num] = packager;

    // 返回存储编码方式的索引位置并加一
    return yar_packagers_list.num++;
}

激活编码模块

每次请求进来到达请求初始化阶段时,就会调用 YAR_ACTIVATE_FUNCTION(packager) 函数设置本次请求使用的编码方式。

// source:yar_packager.c

YAR_ACTIVATE_FUNCTION(packager) {
    // 获取默认的编码方式
    const yar_packager_t *packager = php_yar_packager_get(YAR_G(default_packager), strlen(YAR_G(default_packager)));

    if (packager) {
        // 获取成功则直接设置为默认编码方式,并返回
        YAR_G(packager) = packager;
        return SUCCESS;
    }

    // 如果没有默认编码方式,则使用 PHP 作为默认的编码方式,并输出警告信息
    YAR_G(packager) = &yar_packager_php;
    php_error(E_WARNING, "unable to find package '%s', use php instead", YAR_G(default_packager));

    return SUCCESS;
}

调用编码模块

前面两个阶段都属于初始化的阶段,只有执行脚本的阶段才会去调用编码、解码函数,那么什么时候会调用呢?

上一篇文章中有提到 Yar 支持两种数据传输方式:HTTP、TCP。无论使用哪种传输方式,都会在发送请求前收到响应后调用编码模块。

为此,Yar 提供了两个通用函数:php_yar_packager_pack、php_yar_packager_unpack,用于对数据进行编码或解码操作。

php_yar_packager_pack

php_yar_packager_pack 函数只需要传入编码方式的名称和需要编码的数据,就可以使用对应的编码方式对数据进行编码。

// source:yar_packager.c

zend_string *php_yar_packager_pack(char *packager_name, zval *pzval, char **msg) {
    char header[8];
    smart_str buf = {0};
    
    // 获取编码方式,如果传入的名称为 null,则使用默认的编码方式
    const yar_packager_t *packager = packager_name ?
        php_yar_packager_get(packager_name, strlen(packager_name)) : YAR_G(packager);

    if (!packager) {
        php_error_docref(NULL, E_ERROR, "unsupported packager %s", packager_name);
        return 0;
    }
    
    // 将编码方式的名称写入前 8 个字节中
    memcpy(header, packager->name, 8);
    smart_str_alloc(&buf, YAR_PACKAGER_BUFFER_SIZE /* 1M */, 0);
    smart_str_appendl(&buf, header, 8);
    
    // 调用具体的编码函数
    packager->pack(packager, pzval, &buf, msg); 

    if (buf.s) {
        // 如果编码成功,则在末尾加上 \0
        smart_str_0(&buf);
        return buf.s;
    }

    // 编码失败则释放 buf 并返回 null
    smart_str_free(&buf);

    return NULL;
}

如果使用 JSON 编码方式,那么 packager->pack 实际上调用的是 php_yar_packager_json_pack 函数,将数据编码为 JSON 字符串。

php_yar_packager_unpack

php_yar_packager_unpack 函数不像 php_yar_packager_pack 函数直接传入编码方式的名称,而是传入需要解码的数据及数据的长度,通过取数据前 8 字节得到编码方式的名称,然后通过编码方式的名称获取编码方式,最后调用真正的解码函数对数据进行解码。

// source:yar_packager.c

zval * php_yar_packager_unpack(char *content, size_t len, char **msg, zval *ret) {
    char *pack_info = content; /* 4 bytes, last byte is version */
    const yar_packager_t *packager;

    // 数据往后移动 8 字节,就是之前提到的 payload
    content = content + 8;
    len -= 8;
    
    // 编码方式的名称虽然占用了 8 字节,实际上只用了 7 字节,第 8 个字节存储 \0
    *(pack_info + 7) = '\0';

    // 获取编码方式
    packager = php_yar_packager_get(pack_info, 7);

    if (!packager) {
        spprintf(msg, 0, "unsupported packager '%s'", pack_info);
        return NULL;
    }
    
    // 调用具体的解码函数
    return packager->unpack(packager, content, len, msg, ret);

}

如果使用 JSON 编码方式,那么 packager->unpack 实际上调用的是 php_yar_packager_json_unpack 函数,将数据(JSON 字符串)解码并存储到 ret 中。

关闭编码模块

当执行完 PHP 脚本后,进入请求关闭阶段清理执行脚本过程中使用到的资源。在此之后,如果是 PHP-FPM 模式下,则会进入请求初始化阶段等待下一次请求到来,停止 PHP-FPM 时才会进入模块关闭阶段;如果是 PHP-Cli 模式会直接进入到模块关闭阶段。

关闭编码模块的逻辑很简单,只需要释放 yar_packagers_list 中 packagers 使用的内存就可以了。

// source:yar_packager.c

YAR_SHUTDOWN_FUNCTION(packager) {
    if (yar_packagers_list.size) {
        free(yar_packagers_list.packagers);
    }
    return SUCCESS;
}

总结

在本文中,基于 PHP 生命周期介绍了各个阶段中编码模块都做了些什么,在下一篇文章中将会介绍 Yar 的传输模块,通过该模块,我们就可以知道编码后的数据是怎样被发送出去的。

这篇文章断断续续花费了一个月的时间,期间经历了被裁员、离职、面试等等,心情也是跌宕起伏。之前换工作都是无缝衔接,中间也没休息过,所以这次就当放了个假,休息休息。这几天在家做做饭、搞搞卫生,感觉也还不错,在家待了几天后,想着抓紧把 Yar 系列写完,好准备下一个系列的文章。

至于工作,这段时间也面试了几家,如果没有合适的话,那就明年再战。

:)

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