Apache Thrift 的 Common Lisp 客户端/服务端开发完全指南:从 IDL 翻译到 RPC 实战
2026/9/24 16:39:50 网站建设 项目流程

Apache Thrift 的 Common Lisp 客户端/服务端开发完全指南:从 IDL 翻译到 RPC 实战

【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/gh_mirrors/thrift2/thrift

导读

本文基于 Apache Thrift 仓库中 lib/cl/README.md 官方文档,系统讲解如何在 Common Lisp 中消费 Thrift 协议:从理解.thriftIDL 文件被翻译为 Lisp 源码后生成的包(package)、类型、def-struct/def-service形态,到实现服务端逻辑、用 thrift 编译器生成 Lisp 接口、加载 ASDF 系统、启动服务端并用客户端远程调用。读完本文,你将掌握with-clientserve两大核心宏的用法,以及如何基于仓库自带的 tutorial/cl 教程代码跑通一个完整的 Calculator 示例,并理解生成代码与手写实现之间的分工边界。


一、Thrift Common Lisp 库概述

Thrift 是一套语言无关的进程间通信协议与库:合作的进程之间通过请求/响应消息进行通信,消息的结构在使用前通过一份共享的**接口定义(IDL,Interface Definition Language)**预先约定。在 Common Lisp 场景下,这份.thrift定义文件会被翻译成若干 Lisp 源文件,翻译产物包含以下几类定义:

  • 三个包(package):一个用于实现操作符的命名空间,另外两个分别用于请求操作符与响应操作符;
  • 各种类型定义:作为 Thrift 的typedefenum定义的 Lisp 实现;
  • def-structdef-exception形态:对应 Thrift 的structexception定义;
  • def-service形态:对应 Thrift 的service定义。

其中,def-service是最核心的产物。每个服务定义会展开为一组泛型函数(generic function)定义:对于服务中的每一个操作op,会生成两个函数:

函数使用方签名要点职责
op-request客户端接受一个额外的、位于首位的protocol参数充当客户端代理,通过 Thrift 编码的传输流与远程进程交互
op-response服务端只接受一个protocol参数解码请求消息、调用基函数op并传入消息参数、编码并回送结果、处理异常

需要特别指出的是:翻译生成的代码只负责消息的编解码与传输,真正执行op业务逻辑的函数需要由程序员实现,其命名空间与生成的请求/响应函数相隔离(详见下一节)。库本身不提供从服务实现反向生成 IDL 的能力,因此如果你要新建服务,必须自己编写 IDL 文件(参见官方流程说明中 "no facility to generate them from a service implementation" 的表述,见 lib/cl/README.md)。


二、客户端与服务端的两大核心接口

2.1 客户端接口:with-client

客户端侧唯一的操作符是一个宏:

(with-client (variable location) . body)

它的语义是:在动态上下文中建立一个连接,并在退出时关闭它。variable被绑定到一个客户端代理的流/协议实例上,该实例将底层的 I/O 流(socket、文件等)用实现 Thrift 协议与传输机制的算子包装起来。location则是形如#u"thrift://127.0.0.1:9091"的 Thrift URI(由puri库提供的 uri 类解析)。

典型用法如下(取自文档,原样可运行):

(in-package :cl-user) (macrolet ((show (form) `(format *trace-output* "~%~s =>~{ ~s~}" ',form (multiple-value-list (ignore-errors ,form))))) (with-client (protocol #u"thrift://127.0.0.1:9091") (show (tutorial.calculator:ping protocol)) (show (tutorial.calculator:add protocol 1 2)) (show (tutorial.calculator:add protocol 1 4)) (let ((task (make-instance 'tutorial:work :op operation.subtract :num1 15 :num2 10))) (show (tutorial.calculator:calculate protocol 1 task)) (setf (tutorial:work-op task) operation.divide (tutorial:work-num1 task) 1 (tutorial:work-num2 task) 0) (show (tutorial.calculator:calculate protocol 1 task))) (show (shared.shared-service:get-struct protocol 1)) (show (zip protocol))))

注意这里每个生成的请求函数都以protocol作为第一个参数,这正是文档所强调的"接受一个额外的初始protocol参数"的体现;macrolet中的show只是一个辅助打印宏,用于把返回的多个值与可能的异常一并显示。

2.2 服务端接口:serve

服务端接口组合了服务端对象(server)服务对象(service)

(serve (location service))

它会在指定端口上接受连接,并响应服务所声明操作的请求。service参数是def-service生成的一个全局变量所绑定的服务实例(见下文"运行服务端"小节)。


三、构建与加载库(Building)

Thrift Common Lisp 库以ASDF系统thrift的形式发布。要构建它,需要先把以下依赖系统注册给 ASDF:

依赖系统用途
puri提供 Thrift 的 uri 类(用于解析#u"thrift://..."这样的 location)
closer-mop提供类元数据(class metadata)支持
trivial-utf-8字符串编解码
usocketsocket 传输(transport)
ieee-floatsint 与 float 之间的转换
trivial-gray-streamsgray streams 的抽象层
alexandria常用工具函数

依赖已随库打包,用于本地构建测试与教程二进制,也可以直接使用这些打包好的依赖来加载库本身。注册完成后,执行:

(asdf:load-system :thrift)

这行代码会编译并加载四类内容:

  1. Thrift 定义文件的 Lisp 编译器(即生成器运行时支持);
  2. 传输(transport)与协议(protocol)实现
  3. 客户端与服务端的接口函数
  4. 若要实际使用,还需编写并加载远程服务的接口定义;如果是实现服务方,还需定义 Thrift 要代理调用的真实业务函数。

3.1 仓库中的加载脚本

仓库在 lib/cl/load-locally.lisp 中提供了一份"使用捆绑依赖加载库本身"的脚本,它是构建自测与跨语言测试二进制的基础:

(require "asdf") (load (merge-pathnames "externals/bundle.lisp" *load-truename*)) (asdf:load-asd (merge-pathnames "lib/de.setf.thrift-backport-update/thrift.asd" *load-truename*)) (asdf:load-system :thrift)

依赖是通过 lib/cl/ensure-externals.sh 脚本从 Quicklisp 打包下来的——它会下载 quicklisp、调用quicklisp:bundle-systemspuriusocketcloser-moptrivial-utf-8ieee-floatstrivial-gray-streamsalexandriabordeaux-threadscl-ppcrefiasconet.didierverna.clon等系统打包到externals/,再拉取de.setf.thriftbackport-update分支作为库本体的来源。教程目录下的 tutorial/cl/load-locally.lisp 与该脚本内容一致,保证测试与教程使用同一套加载路径。


四、按照 Thrift 教程实现一个服务

文档其余部分以 Thrift 官方教程为线索,演示完整流程的五步:

  1. 实现服务(业务逻辑);
  2. 翻译 Thrift IDL;
  3. 加载翻译后的 Lisp 服务接口;
  4. 运行服务端;
  5. 用客户端远程访问服务。

4.1 第一步:实现服务

教程服务包含若干函数:addpingzipcalculate。每一个被翻译的 IDL 文件都会为每个服务生成三个包。以教程文件为例,相关的包是:

  • tutorial.calculator—— 请求包(生成)
  • tutorial.calculator-implementation—— 实现包(由程序员填写)
  • tutorial.calculator-response—— 响应包(生成)

这种划分的目的是把 Thrift 方法的请求函数(生成)、**响应函数(生成)实现函数(程序员编写)**分离开。

文档建议在tutorial-implementation包中实现服务逻辑,因为它importcommon-lisp包,而服务专属的那些包不导入common-lisp——这是为了避免 Thrift 方法名与common-lisp中的函数名发生冲突。仓库里对应的实现文件是 tutorial/cl/tutorial-implementation.lisp。

文档给出的是"基函数"定义示例(注意函数名带包限定符):

;; define the base operations (in-package :tutorial-implementation) (defun tutorial.calculator-implementation:add (num1 num2) (format t "~&Asked to add ~A and ~A." num1 num2) (+ num1 num2)) (defun tutorial.calculator-implementation:ping () (print :ping)) (defun tutorial.calculator-implementation:zip () (print :zip)) (defun tutorial.calculator-implementation:calculate (logid task) (calculate-op (work-op task) (work-num1 task) (work-num2 task)))

其中calculate的实现借助了一个defgeneric分派,把operation枚举值分派到对应的算术实现上,并通过:around方法记录日志:

(defgeneric calculate-op (op arg1 arg2) (:method :around (op arg1 arg2) (let ((result (call-next-method))) (format t "~&Asked to calculate: ~d on ~A and ~A = ~d." op arg1 arg2 result) result)) (:method ((op (eql operation.add)) arg1 arg2) (+ arg1 arg2)) (:method ((op (eql operation.subtract)) arg1 arg2) (- arg1 arg2)) (:method ((op (eql operation.multiply)) arg1 arg2) (* arg1 arg2)) (:method ((op (eql operation.divide)) arg1 arg2) (/ arg1 arg2)))

仓库中的实际实现 tutorial/cl/tutorial-implementation.lisp 还展示了更完整的边界处理:calculatehandler-case捕获division-by-zero,并抛出 Thrift 异常tutorial:invalidoperation(带有:why:what-op关键字参数),这样客户端就能收到结构化的远程异常。shared服务的实现见 tutorial/cl/shared-implementation.lisp:它用一个 hash table 作为"日志存储",get-struct负责按键取值,add-log负责把计算结果以shared:sharedstruct实例的形式存入。

4.2 第二步:翻译 Thrift IDL

IDL 文件使用.thrift扩展名。教程场景有两个文件需要翻译:

  • tutorial.thrift
  • shared.thrift

由于前者include了后者,只需用前者即可生成全部接口:

$THRIFT/bin/thrift -r --gen cl $THRIFT/tutorial/tutorial.thrift
  • -r表示递归翻译被 include 的文件;
  • --gen cl指定目标语言为 Common Lisp(cl是 thrift 编译器注册的语言名)。

从编译器源码 compiler/cpp/src/thrift/generate/t_cl_generator.cc 可以看到,CL 生成器会按 IDL 的namespace cl声明来决定输出的包前缀(第 164 行program->get_namespace("cl")),并据此输出三类文件。

4.3 第三步:加载 Lisp 翻译后的服务接口

翻译器为每个 IDL 文件生成三个文件(生成器源码第 129/130/142 行印证了这一点):

  • tutorial-types.lisp—— 类型定义(def-structdef-exception、枚举与 typedef 的 Lisp 实现);
  • tutorial-vars.lisp—— 变量与常量定义;
  • 一个.asd文件(ASDF 系统定义,形如thrift-gen-tutorial.asd),用来加载以上两者,并把其他 include(如教程中的shared)作为依赖拉进来。

仓库教程里的 tutorial/cl/make-tutorial-server.lisp 演示了完整加载序列:加载本地依赖、加载 CLON 命令行框架、依次asdf:load-asd两个生成系统(gen-cl/shared/thrift-gen-shared.asdgen-cl/tutorial/thrift-gen-tutorial.asd),最后加载手写实现所在的 tutorial/cl/thrift-tutorial.asd。这个教程级 ASDF 系统把shared-implementationtutorial-implementation两个手写文件串行编译进thrift-tutorial系统,依赖thrift-gen-tutorial(即生成代码)。

4.4 第四步:运行服务端

服务在def-service形态中声明的实际名称(教程里是calculator)。每个服务定义都会定义一个以服务名命名的全局变量,并将其绑定到一个描述该服务操作的服务实例上。

启动服务只需指定一个 location 与服务实例:

(in-package :tutorial) (serve #u"thrift://127.0.0.1:9091" calculator)

仓库教程服务端入口 tutorial/cl/make-tutorial-server.lisp 使用thrift:serve绑定#u"thrift://127.0.0.1:9090",并通过clon:dump "TutorialServer" main生成可执行二进制。

4.5 第五步:用客户端远程访问

另一个进程中运行客户端(前文的with-client代码段即此环节),即可依次发起pingaddcalculate(含触发除零异常)与shared.shared-service:get-struct等调用。仓库客户端入口 tutorial/cl/make-tutorial-client.lisp 展示了异常路径的预期行为:调用5 / 0时会捕获tutorial:invalidoperation并打印异常信息,随后调用15 - 10正常返回,最后读取服务端日志中的sharedstruct


五、已知问题与设计取舍(Issues)

文档明确列出了该库当前的一些实现限制与设计讨论,理解它们有助于规避陷阱:

5.1 optional 字段的歧义

当 IDL 把某个字段声明为optional时,生成的def-struct形态不会为该 slot 提供 initform,编码算子也会跳过未绑定的 slot。这会在布尔(bool)字段上留下歧义:无法区分"字段未设置"与"字段显式设置为 NIL"。

5.2 实例化协议

  • struct类是标准类(standard class),exception类则由具体实现决定;
  • 解码器对 initargs 列表应用make-struct来构造实例;
  • 文档指出,在服务端一侧,复用(resourcing)struct 并直接对 slot-value 做副作用式解码是有优势的——这暗示未来可能的优化方向。

5.3 map 的表示方式

map 目前用hash table表示。由于通过 call/reply 接口传输的数据全部是静态类型的,对象本身无需自描述编码形式,用association list(alist)其实就已足够。文档还论证了为什么 property list 并不更优:虽然 key 类型任意,但getf使用eq比较,若要支持任意 key 就得新建访问接口,且 plist 无法直接用于函数应用(如作为泛型函数分派依据)。


六、测试与回归

仓库为 CL 库提供了自动化测试框架:lib/cl/Makefile.am 中的run-tests目标调用 SBCL 执行 lib/cl/test/make-test-binary.lisp 生成测试二进制,再由check-local运行./run-tests。构建产物(run-tests、quicklisp、依赖目录等)可通过clean-local清理。此外 lib/cl/READMES/readme-cassandra.lisp 还附带了一个 Cassandra 场景的补充说明,可作为扩展阅读。


总结

Apache Thrift 的 Common Lisp 支持遵循与其他语言一致的"先定义 IDL、再生成骨架、后填写实现"的开发范式,但又有鲜明的 Lisp 特色:

  • 命名空间三分:请求包 / 响应包 / 实现包相互隔离,从机制上避免 Thrift 方法名污染common-lisp
  • 泛型函数驱动:每个操作展开为一对op-request /op-response 泛型函数,配合with-clientserve两个顶层算子,客户端与服务端代码极度精简;
  • 声明式传输#u"thrift://host:port"这样的 URI 即完整描述了连接目标,底层 socket、协议编解码全部由库封装。

若要在自己的项目中使用,完整的落地路径是:编写.thrift文件 → 用thrift -r --gen cl翻译 → 用 ASDF 加载生成系统与手写实现系统 → 服务端调用thrift:serve、客户端在thrift:with-client中发起调用。需要注意 optional 字段、map 表示等已知限制,并在实现服务时始终把业务函数写在*-implementation包中。

【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/gh_mirrors/thrift2/thrift

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询