前言

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

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

什么是编码/解码

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
class Person 
{
public $name;

public $age;
}

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

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

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

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

1
2
3
4
5
6
7
8
$encoded = file_get_contents('person.txt');
var_dump($encoded);

$decodedPerson = unserialize($encoded);

var_dump($decodedPerson);
var_dump($decodedPerson->name);
var_dump($decodedPerson->age);
1
2
3
4
5
6
7
8
9
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。

1
2
3
4
5
6
7
// 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。

1
2
3
4
5
const yar_packager_t yar_packager_json = {
"JSON",
php_yar_packager_json_pack,
php_yar_packager_json_unpack
};

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

1
2
3
4
5
6
7
8
9
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 常量,比如版本号、客户端相关的选项。紧接着开始初始化模块,除了编码模块以外,还会初始化传输、客户端、服务端及异常等模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 是一个宏,定义如下:

1
2
3
4
#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) 展开之后是这样的:

1
zm_startup_yar_packager(type, module_number)

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

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

1
2
3
4
5
6
7
8
9
#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 常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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,用于存储所有可用的编码方式。

1
2
3
4
5
6
7
// 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 进行初始化、扩容及注册编码方式等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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) 函数设置本次请求使用的编码方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 函数只需要传入编码方式的名称和需要编码的数据,就可以使用对应的编码方式对数据进行编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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 字节得到编码方式的名称,然后通过编码方式的名称获取编码方式,最后调用真正的解码函数对数据进行解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 使用的内存就可以了。

1
2
3
4
5
6
7
8
// source:yar_packager.c

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

总结

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

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

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

:)