Apache Thrift 的 Common Lisp 客户端与服务端开发指南:从 IDL 翻译到 with-client / serve 全流程实战
2026/9/15 17:06:09 网站建设 项目流程

Apache Thrift 的 Common Lisp 客户端与服务端开发指南:从 IDL 翻译到 with-client / serve 全流程实战

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

Apache Thrift 是跨语言的 RPC 框架,而 Common Lisp(CL)是其众多目标语言中的一员。本指南以 lib/cl/README.md 为骨架,系统讲解如何用 Thrift 官方编译器生成 Common Lisp 代码、如何在 CL 中实现远程服务、如何用with-client编写客户端、如何用serve启动服务端,并深入仓库源码与教程工程验证每一步的底层实现。读完本文,你将掌握在 Common Lisp 中完整落地一个 Thrift 服务的端到端流程。

1. 总览:Thrift 在 Common Lisp 中如何工作

Thrift 是一套协议与库,用于在相互协作的进程之间进行与语言无关的通信。通信以请求/响应消息的形式进行,其格式通过共享的接口定义(IDL 文件)预先规定。一个 Thrift 定义文件(.thrift)会被编译器翻译成 Lisp 源文件,翻译结果包含以下几类定义:

  • 三个包(package):一个用于实现操作符的命名空间(implementation),另两个分别用于请求(request)和响应(response)操作符;
  • 各种类型定义:作为 Thrifttypedefenum定义的实现;
  • DEF-STRUCTDEF-EXCEPTION形式:对应 Thrift 的structexception定义;
  • DEF-SERVICE形式:对应 Thrift 的service定义。

三包分离是理解整个模型的关键:请求函数由编译器生成(客户端用),响应函数也由编译器生成(服务端用),而实现函数则留给程序员填写。这样设计是为了避免 Thrift 方法名与common-lisp包中的既有函数名产生冲突。

2. 服务定义展开:请求函数与响应函数

每个def-service会展开为一组 generic function 定义。对于服务定义中的每一个op,都会定义两个函数:

函数使用方签名与职责
op-request客户端额外接受一个初始的protocol参数,作为操作的客户端代理,通过 Thrift 编码的传输流与远程进程交互
op-response服务端只接受一个protocol参数。服务端用它解码请求消息、以消息参数调用底层的op函数、把结果编码成响应发回,并处理异常

也就是说,ping-request负责在客户端把调用参数按 Thrift 协议写出去,ping-response负责在服务端把参数读回来、回调真实的ping实现、再把返回值写回。这一生成逻辑在官方编译器的 CL 后端 compiler/cpp/src/thrift/generate/t_cl_generator.cc 中实现,该文件负责输出上述def-servicedef-struct、包定义等 Lisp 代码。

2.1 客户端接口:with-client

客户端只暴露一个操作符:

(with-client (variable location) . body)

它在动态上下文中建立一条连接,并在退出时关闭连接。variable被绑定到一个客户端代理流/协议实例,该实例把底层 I/O 流(socket、文件等)用实现 Thrift 协议与传输机制的算子包装起来。也就是说,with-client内部完成了「建立 socket → 套上传输层(如 framing)→ 套上协议层(如 binary)→ 绑定到变量」的全过程。

2.2 服务端接口:serve

服务端接口把服务对象组合起来:

(serve (location service))

它在指定端口上接受连接,并响应服务各操作的请求。location采用 Thrift URI 的形式,例如#u"thrift://127.0.0.1:9091"service则是def-service生成的全局服务实例。

3. 构建:ASDF 系统thrift及其依赖

Thrift Common Lisp 库被打包为 ASDF 系统thrift,依赖以下系统:

  • puri:提供 thrift URI 类(用于#u"thrift://..."这样的字面量);
  • closer-mop:提供类元数据支持;
  • trivial-utf-8:提供字符串编解码;
  • usocket:提供 socket 传输;
  • ieee-floats:提供整数与浮点数之间的转换;
  • trivial-gray-streams:Gray streams 的抽象层;
  • alexandria:常用实用工具。

在仓库中,这些依赖为本地构建测试和教程二进制而打包(bundle):可以用这些包来加载库本身。构建方式是把这些系统注册给 ASDF 后求值:

(asdf:load-system :thrift)

这会编译并加载:Thrift 定义文件的 Lisp 编译器、传输与协议实现、客户端与服务端接口函数。加载细节见 lib/cl/load-locally.lisp,它依次加载externals/bundle.lisp、注册lib/de.setf.thrift-backport-update/thrift.asd,最后(asdf:load-system :thrift)

3.1 自动化拉取依赖:ensure-externals.sh

仓库提供了 lib/cl/ensure-externals.sh,它用 Quicklisp 把上述依赖(外加bordeaux-threadscl-ppcrefiasconet.didierverna.clon等测试与命令行辅助库)bundle 到externals/目录,并从上游仓库拉取de.setf.thriftbackport-update分支解压到lib/。其核心命令:

sbcl --load quicklisp.lisp \ --eval "(quicklisp:bundle-systems '(#:puri #:usocket #:closer-mop #:trivial-utf-8 #:ieee-floats #:trivial-gray-streams #:alexandria #:bordeaux-threads #:cl-ppcre #:fiasco #:net.didierverna.clon) :to \"externals/\")" \ --no-userinit

3.2 测试接入:make check

lib/cl/Makefile.am 展示了库如何接入 Thrift 的自动化测试:run-tests用 SBCL 执行 lib/cl/test/make-test-binary.lisp,后者加载thrift-test系统并用 fiasco 运行全部自测,最终clon:dump出可执行二进制run-tests,供check-local调用。

4. 实战第一步:实现服务

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

  • tutorial.calculator(生成的请求/类型命名空间)
  • tutorial.calculator-implementation(程序员实现命名空间)
  • tutorial.calculator-response(生成的响应命名空间)

建议在tutorial-implementation包中实现服务:它导入了common-lisp包,而服务专属的包不导入(以避免 Thrift 方法名与common-lisp内函数名的冲突)。仓库中真实的实现见 tutorial/cl/tutorial-implementation.lisp,其完整代码为:

(in-package #:tutorial-implementation) (defun tutorial.calculator-implementation:ping () (format t "ping()~%")) (defun tutorial.calculator-implementation:add (num1 num2) (format t "add(~a, ~a)~%" num1 num2) (+ num1 num2)) (defun tutorial.calculator-implementation:calculate (logid work) (format t "calculate(~a, ~a)~%" logid work) (handler-case (let* ((num1 (tutorial:work-num1 work)) (num2 (tutorial:work-num2 work)) (op (tutorial:work-op work)) (result (cond ((= op tutorial:operation.add) (+ num1 num2)) ((= op tutorial:operation.subtract) (- num1 num2)) ((= op tutorial:operation.multiply) (* num1 num2)) ((= op tutorial:operation.divide) (/ num1 num2))))) (shared-implementation::add-log logid result) result) (division-by-zero () (error 'tutorial:invalidoperation :why "Division by zero." :what-op (tutorial:work-op work))))) (defun tutorial.calculator-implementation:zip () (format t "zip()~%"))

注意两点:一是函数名的包限定写法(如tutorial.calculator-implementation:add),这是三包分离模型的直接体现;二是calculate中把除零错误映射为 Thrift 定义的tutorial:invalidoperation异常——Thrift 的exception定义在 CL 中会被翻译为可error的异常类,而division-by-zero是 Common Lisp 的标准条件(condition),说明可以在实现层做条件到异常的转换。

共享服务(shared.thrift)的实现见 tutorial/cl/shared-implementation.lisp:它用一个 hash table 作为存储,get-struct读取、add-log写入shared:sharedstruct实例。

5. 实战第二步:翻译 Thrift IDL

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

  • tutorial/tutorial.thrift
  • tutorial/shared.thrift

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

$THRIFT/bin/thrift -r --gen cl $THRIFT/tutorial/tutorial.thrift
  • -r表示递归(recursion),处理include进来的其他 IDL 文件;
  • --gen cl选择目标语言为 Common Lisp(对应编译器后端 compiler/cpp/src/thrift/generate/t_cl_generator.cc)。

在 tutorial/cl/Makefile.am 中,这一步骤被固化为gen-cl目标:$(THRIFT) --gen cl -r $<,随后依次用 SBCL 执行make-tutorial-server.lispmake-tutorial-client.lisp生成可执行文件。注意其中的注释特别说明:服务端和客户端不能并行构建,因为加载make-tutorial-*脚本时 SBCL 会编译其依赖,而共享依赖的并行编译可能互相覆盖、损坏编译产物。

6. 实战第三步:加载翻译后的接口

翻译器为每个 IDL 文件生成三个文件。例如:

  • tutorial-types.lisp:类型定义(def-structdef-exception、枚举等);
  • tutorial-vars.lisp:变量/常量定义;
  • 一个.asd文件:用于同时加载以上两者,并把其他 include(如教程中的shared)作为依赖引入。

教程工程把这些组织为thrift-tutorialASDF 系统(见 tutorial/cl/thrift-tutorial.asd),它依赖thrift-gen-tutorial,并按顺序加载shared-implementation.lisptutorial-implementation.lisp——也就是说,实现文件与生成文件通过 ASDF 的依赖关系自动串起来。

7. 实战第四步:运行服务端

def-service形式中指定的实际服务名(在tutorial.lisp中为calculator)会定义一个同名全局变量,绑定到一个描述各操作的服务实例。要启动服务,只需指定 location 和服务实例:

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

#u"..."是 puri 库提供的 URI 字面量读取器语法;thrift://是 Thrift 约定使用的协议 scheme。服务端随后会在127.0.0.1:9091上接受连接并分派请求。

8. 实战第五步:客户端远程访问

在另一个进程中运行客户端。教程演示了一个完整的客户端会话,其中show宏负责打印每个调用的返回值和可能的错误(用ignore-errors包装):

(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))))

关键点:

  • tutorial.calculator:pingtutorial.calculator:add等是生成的请求函数,第一个参数是with-client绑定的protocol代理——这正是第 2 节「request 函数额外接受protocol参数」的实战场面;
  • 结构体tutorial:work是标准 CLOS 类,用make-instance创建、用setf访问器修改字段(tutorial:work-optutorial:work-num1tutorial:work-num2);
  • operation.subtractoperation.divide是 Thrift 枚举翻译出的符号常量;
  • 跨服务调用shared.shared-service:get-struct展示了include的用法——以.thrift文件名作前缀访问被包含文件中的服务。

9. 已知问题与设计考量

README 还记录了三个值得注意的技术要点:

9.1 optional 字段

当 IDL 将字段声明为optional时,生成的def-struct中该 slot没有 initform,编码操作符会跳过未绑定的 slot。这会带来歧义:特别是 bool 字段,「未提供」与「显式为假」在编码上难以区分。

9.2 实例化协议

struct 类是标准 CLOS 类,exception 类则由具体实现规定。解码器把 initargs 列表交给make-struct。README 指出,在服务端一侧,复用 struct 并通过对 slot 值做直接副作用来解码是有优势的——这暗示了未来可能的性能优化方向。

9.3 map 的表示

Map 现在表示为 hash table。由于通过调用/回复接口传输的数据都是静态类型的,对象本身无需指明编码形式,assoc list 其实也足够。而因为 key 类型是任意的,property list 并没有额外便利:getf基于eq工作,需要新的访问接口,且不适用于函数应用。

10. 从零构建的完整路径速查

结合 lib/cl/Makefile.am 与 tutorial/cl/Makefile.am,一条完整的构建链路是:

  1. 准备依赖:执行bash lib/cl/ensure-externals.sh(或直接使用仓库已 bundle 的 externals);
  2. 加载库(asdf:load-system :thrift),或在脚本中复用 lib/cl/load-locally.lisp 的加载逻辑;
  3. 生成接口thrift --gen cl -r tutorial/tutorial.thrift
  4. 编译教程:在 tutorial/cl 下执行$(SBCL) --script make-tutorial-server.lispmake-tutorial-client.lisp,得到TutorialServerTutorialClient两个可执行二进制;
  5. 运行:先启动./TutorialServer(监听thrift://127.0.0.1:9091),再在另一进程运行./TutorialClient

另外,lib/cl/READMES/readme-cassandra.lisp 提供了一个以 CL 客户端访问 Cassandra Thrift 接口的参考示例,可作为「库 + 生成代码 + 业务调用」组合的进一步参考。

11. 小结

Apache Thrift 的 Common Lisp 支持以「三包分离 + 请求/响应函数配对」为核心模型:thrift --gen cl负责把 IDL 翻译成def-struct/def-exception/def-service组成的 Lisp 代码,程序员只需在-implementation包中填充真正的业务函数,再用serve暴露服务、用with-client发起调用。理解op-request(客户端代理)与op-response(服务端解码+回调+编码)的分工,以及 optional 字段、map 表示等设计细节,就能在 Common Lisp 项目中稳定地接入 Thrift 生态。

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

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

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

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

立即咨询