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)操作符;
- 各种类型定义:作为 Thrift
typedef和enum定义的实现; DEF-STRUCT与DEF-EXCEPTION形式:对应 Thrift 的struct与exception定义;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-service、def-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-threads、cl-ppcre、fiasco、net.didierverna.clon等测试与命令行辅助库)bundle 到externals/目录,并从上游仓库拉取de.setf.thrift的backport-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-userinit3.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. 实战第一步:实现服务
教程包含add、ping、zip、calculate等函数。每个翻译出的 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.lisp与make-tutorial-client.lisp生成可执行文件。注意其中的注释特别说明:服务端和客户端不能并行构建,因为加载make-tutorial-*脚本时 SBCL 会编译其依赖,而共享依赖的并行编译可能互相覆盖、损坏编译产物。
6. 实战第三步:加载翻译后的接口
翻译器为每个 IDL 文件生成三个文件。例如:
tutorial-types.lisp:类型定义(def-struct、def-exception、枚举等);tutorial-vars.lisp:变量/常量定义;- 一个
.asd文件:用于同时加载以上两者,并把其他 include(如教程中的shared)作为依赖引入。
教程工程把这些组织为thrift-tutorialASDF 系统(见 tutorial/cl/thrift-tutorial.asd),它依赖thrift-gen-tutorial,并按顺序加载shared-implementation.lisp与tutorial-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:ping、tutorial.calculator:add等是生成的请求函数,第一个参数是with-client绑定的protocol代理——这正是第 2 节「request 函数额外接受protocol参数」的实战场面;- 结构体
tutorial:work是标准 CLOS 类,用make-instance创建、用setf访问器修改字段(tutorial:work-op、tutorial:work-num1、tutorial:work-num2); operation.subtract、operation.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,一条完整的构建链路是:
- 准备依赖:执行
bash lib/cl/ensure-externals.sh(或直接使用仓库已 bundle 的 externals); - 加载库:
(asdf:load-system :thrift),或在脚本中复用 lib/cl/load-locally.lisp 的加载逻辑; - 生成接口:
thrift --gen cl -r tutorial/tutorial.thrift; - 编译教程:在 tutorial/cl 下执行
$(SBCL) --script make-tutorial-server.lisp与make-tutorial-client.lisp,得到TutorialServer与TutorialClient两个可执行二进制; - 运行:先启动
./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),仅供参考