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-client、serve两大核心宏的用法以及如何基于仓库自带的 tutorial/cl 教程代码跑通一个完整的 Calculator 示例并理解生成代码与手写实现之间的分工边界。一、Thrift Common Lisp 库概述Thrift 是一套语言无关的进程间通信协议与库合作的进程之间通过请求/响应消息进行通信消息的结构在使用前通过一份共享的**接口定义IDLInterface Definition Language**预先约定。在 Common Lisp 场景下这份.thrift定义文件会被翻译成若干 Lisp 源文件翻译产物包含以下几类定义三个包package一个用于实现操作符的命名空间另外两个分别用于请求操作符与响应操作符各种类型定义作为 Thrift 的typedef与enum定义的 Lisp 实现def-struct与def-exception形态对应 Thrift 的struct与exception定义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则是形如#uthrift://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 #uthrift://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生成的一个全局变量所绑定的服务实例见下文运行服务端小节。三、构建与加载库BuildingThrift Common Lisp 库以ASDF系统thrift的形式发布。要构建它需要先把以下依赖系统注册给 ASDF依赖系统用途puri提供 Thrift 的 uri 类用于解析#uthrift://...这样的 locationcloser-mop提供类元数据class metadata支持trivial-utf-8字符串编解码usocketsocket 传输transportieee-floatsint 与 float 之间的转换trivial-gray-streamsgray streams 的抽象层alexandria常用工具函数依赖已随库打包用于本地构建测试与教程二进制也可以直接使用这些打包好的依赖来加载库本身。注册完成后执行(asdf:load-system :thrift)这行代码会编译并加载四类内容Thrift 定义文件的 Lisp 编译器即生成器运行时支持传输transport与协议protocol实现客户端与服务端的接口函数若要实际使用还需编写并加载远程服务的接口定义如果是实现服务方还需定义 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-systems把puri、usocket、closer-mop、trivial-utf-8、ieee-floats、trivial-gray-streams、alexandria、bordeaux-threads、cl-ppcre、fiasco、net.didierverna.clon等系统打包到externals/再拉取de.setf.thrift的backport-update分支作为库本体的来源。教程目录下的 tutorial/cl/load-locally.lisp 与该脚本内容一致保证测试与教程使用同一套加载路径。四、按照 Thrift 教程实现一个服务文档其余部分以 Thrift 官方教程为线索演示完整流程的五步实现服务业务逻辑翻译 Thrift IDL加载翻译后的 Lisp 服务接口运行服务端用客户端远程访问服务。4.1 第一步实现服务教程服务包含若干函数add、ping、zip和calculate。每一个被翻译的 IDL 文件都会为每个服务生成三个包。以教程文件为例相关的包是tutorial.calculator—— 请求包生成tutorial.calculator-implementation—— 实现包由程序员填写tutorial.calculator-response—— 响应包生成这种划分的目的是把 Thrift 方法的请求函数生成、**响应函数生成与实现函数程序员编写**分离开。文档建议在tutorial-implementation包中实现服务逻辑因为它import了common-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 还展示了更完整的边界处理calculate用handler-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 IDLIDL 文件使用.thrift扩展名。教程场景有两个文件需要翻译tutorial.thriftshared.thrift由于前者include了后者只需用前者即可生成全部接口$THRIFT/bin/thrift -r --gen cl $THRIFT/tutorial/tutorial.thrift-r表示递归翻译被 include 的文件--gen cl指定目标语言为 Common Lispcl是 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-struct、def-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.asd与gen-cl/tutorial/thrift-gen-tutorial.asd最后加载手写实现所在的 tutorial/cl/thrift-tutorial.asd。这个教程级 ASDF 系统把shared-implementation与tutorial-implementation两个手写文件串行编译进thrift-tutorial系统依赖thrift-gen-tutorial即生成代码。4.4 第四步运行服务端服务在def-service形态中声明的实际名称教程里是calculator。每个服务定义都会定义一个以服务名命名的全局变量并将其绑定到一个描述该服务操作的服务实例上。启动服务只需指定一个 location 与服务实例(in-package :tutorial) (serve #uthrift://127.0.0.1:9091 calculator)仓库教程服务端入口 tutorial/cl/make-tutorial-server.lisp 使用thrift:serve绑定#uthrift://127.0.0.1:9090并通过clon:dump TutorialServer main生成可执行二进制。4.5 第五步用客户端远程访问在另一个进程中运行客户端前文的with-client代码段即此环节即可依次发起ping、add、calculate含触发除零异常与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 classexception类则由具体实现决定解码器对 initargs 列表应用make-struct来构造实例文档指出在服务端一侧复用resourcingstruct 并直接对 slot-value 做副作用式解码是有优势的——这暗示未来可能的优化方向。5.3 map 的表示方式map 目前用hash table表示。由于通过 call/reply 接口传输的数据全部是静态类型的对象本身无需自描述编码形式用association listalist其实就已足够。文档还论证了为什么 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-client与serve两个顶层算子客户端与服务端代码极度精简声明式传输#uthrift://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),仅供参考