RPC框架的IDL与IDL-less

IDL,Interface description language,即接口描述语言。
IDL是一种很有用的工具,它提供了对接口的描述,约定了接口协议。使得通讯双方通讯时,无需再发送 scheme,有效提高了通讯数据的荷载比。
但对于RPC框架而言,IDL又不仅仅是一个接口描述语言。对于市面上绝大多数的RPC框架而言,IDL还是一个工具和一种使用过程,专指根据 IDL 描述文件,用指定的开发语言,生成对应的服务端接口模块,和客户端程序。这样的好处是,便于开发者快速开发。
初衷是好的,但是,问题伴随而来。
首先,依据 IDL 生成对应的客户端和接口模块,这个本质是编译。
但对于编译和语言的常规理解和认识,却导致了问题的复杂化:IDL成了一门“编译型”的语言,发展出了一套复杂的规则和语法。而且不同 RPC 框架的 IDL 语言不完全一样。每用一个新的框架,就有一套新的IDL语法和规则,就得重新学习一遍。
其次,市面上绝大多数的IDL语法,对于可选参数和可变类型参数,以及不定长参数存在着不同程度的支持问题。
早期的 IDL,所有的接口字段必须存在。即使是无用的,也需要赋予一个诸如 'nil' 一类的值。否则就是违背 IDL 的规范。随着技术的发展,之后的 IDL,出现了 optional 关键字,但仅仅是不得已的情况下,才被推荐使用。而此时,被强烈推荐使用的却是关键字 require 以及 required。比如 Thrift,ProtoBuf 1.0 都是典型的代表。直到技术不断发展,RPC 领域的经验积累越来越多,关键字 require 以及 required 才不再被继续推荐使用,而关键字 optional 成为 IDL 的新宠。可选参数,到目前为止还没有遇到什么问题。直到 IDL 开始编译,生成对应的客户端和接口代码时,新的问题出现。
接口代码的生成,的确方便了网络服务和接口调用的开发。但过度复杂的接口代码,直接导致了接口的强耦合:所有的业务都依赖于 IDL 生成的客户端和服务端的接口,如果一个变动,其余的需要全部跟随变动。如果一个接口被改动,与之关联的所有服务和客户端,必须全部重新编译,否则极有可能在 IDL 生成的代码中,出现不兼容的问题。
最典型的就是,加了一个新的参数,但不在协议数据的末尾,那绝大多数 RPC 框架原有的接口在处理新版接口数据时,便会出现兼容性错误。
当然,这在很长一段时间内被视为理所当然。
但视之“理所当然”,却不妨碍开发者们对其导致的问题视而不见,于是不少IDL为了绕过这个问题,发明了“序号/字段编号”这种本不应该存在的语言标识。
此外,传统的 IDL 一旦遇到参数的减少,或者参数类型的更改,IDL 所生成的 RPC 框架的接口代码,则往往需要两个独立的接口处理函数,才能同时处理新旧两个版本的数据。所以,为了更加“优雅”的处理参数的删除和类型的改变,部分 IDL 增强了对“序号/字段编号”这种本不应该存在的语言标识的依赖。字段编号不可重复,一旦确定了,便不能修改。于是字段编号的维护,便成了开发者历史包袱的一部分。
然后对于不定类型的参数,支持该特性的 IDL 会选用 Oneof 或 Union 等来实现,而剩下的 IDL 则直接弃疗。
至于不定长参数,类似于 C 语言 printf 那样的参数,这对几乎所有的 IDL 而言,均是噩梦般的存在。因此对于不定长参数的支持,几乎所有的 IDL 都是以弃疗结束。
在经过漫长的摸索和众多项目的历练后,我们发现,对 IDL 的使用,其实不必如此痛苦。
我们选择了 IDL-less的方案,让 IDL 回归到纯粹的接口描述。此时,IDL-less 的方案,不仅能轻易支持可选参数、可变类型参数,也能很好的支持参数的增减,和不定长参数。
在这个前提下,我们使用 IDL-less 设计了新的 RPC 框架。在这个新的框架下,我们发现,其实我们虽然也可能根据新框架的 IDL 生成客户端和服务器的框架代码,但在更换思路后,需要生成的代码其实异常的简单,甚至对于手写来说,也完全不是负担。此时,如果对于传统的 RPC 框架,IDL 已经默化为根据 IDL 文件,生成接口和框架代码的话,我们不仅做到了 IDL-less,甚至我们可以称为:无 IDL。
我们的新的 IDL-less 的 RPC 框架:FPNN:Fast Programmable Nexus Network
与动则生成上万行框架和接口代码的传统 RPC 框架和 IDL 而言,因为 FPNN 框架设计之初,便将极力简化使用复杂度作为核心设计目标之一,所以 FPNN 框架几乎没有什么代码需要根据 IDL 描述生成。
首先,是框架的代码。
参考 FPNN 服务端基础使用向导“1. 服务代码框架” 将发现,无论开发 TCP 服务还是 UDP 服务,算上括号与空行,标准框架一共也才 3 个文件总共 41 行代码。其中 “DemoServer.cpp” 24 行,“DemoServer.cpp” 15 行,“DemoQuestProcessor.cpp” 2 行。而参考 FPNN 客户端基础使用向导 就会发现,算上括号与空行,客户端完整框架一共就 8 行代码,而以下 17 行代码(算上括号、空行、注释)已经能直接运行,并向服务器发送请求,并接收应答:

#include #include "TCPClient.h" using namespace std; using namespace fpnn; int main() { std::shared_ptr client = TCPClient::createClient("demo.example.com", 6789); //-- 生成请求数据 FPQWriter qw(1, "echo"); qw.param("feedback", "Example string."); FPAnswerPtr answer = client->sendQuest(qw.take()); return 0; }

所以,对于 FPNN 框架,几乎没有什么代码是需要依据 IDL 描述文件生成的。
然后,是接口和对象的处理。
假设 IDL 中规定了一个接口 demoInterface 和一个结构体 DemoData,编译成 C++ 代码可能是以下形式:
struct DemoData { int demoInt; double demoDouble; std::string demoString; }; void demoInterface(int firstValue, const std::string& secondValue, const struct DemoData& demoData);

而在 FPNN 中,无论任何接口,接口均统一为以下形式:
FPAnswerPtr method_name(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);

【RPC框架的IDL与IDL-less】那对于基本类型参数的获取,FPNN 仅只是多了一步:
int firstValue = https://www.it610.com/article/args->getInt("firstValue");


std::string secondValue = https://www.it610.com/article/args->getString("secondValue");

而对于结构体,也是仅多一步(但从 FPNN 的理念上,结构体直接做为接口参数的这种设计,因为对结构扩展的兼容性极差,非常不推荐):
DemoData demoData = https://www.it610.com/article/args->get("demoData", Demodata());

而对于结构体成员的获取,使用 IDL 生成的框架代码:
double value = https://www.it610.com/article/demoData.demoValue();

直接使用 FPNN 框架:
demoData.demoValue;

没有本质性差异。
所以,对于 FPNN 这一 IDL-less 的 RPC 框架而言,虽然可以根据 IDL 文件生成对应的框架和接口代码,但几乎毫无意义。因此,FPNN 框架也不会提供根据 IDL 文件生成对应的框架和接口代码的工具。

    推荐阅读