6.2 Service 请求应答
上一节的 Topic 是"喊一嗓子、发完不管"。但很多时候,小莫需要的是一问一答:问一句"现在几点?",然后等一个明确的答复。这就是第二种模式——Service(服务)请求应答。
有时候我不是广播,而是想问某个零件一个具体问题,并等它回答我。比如问"电池还剩多少电?"——喊一嗓子可不行,我要的是那个准确的数字。
学习目标
学完本节,你将能够:
- 说清 Service 模式的核心:"一问一答,有来有回";
- 理解为什么需要
request_id(请求编号) 把"问"和"答"对上号; - 用纯 Python + 元数据(metadata)实现一对完整的"客户端 / 服务端";
- 明白 Service 和 Topic 的本质区别与联系。
前置要求
先回到黑板教室
第一章我们这样点过 Service:
小明问小红"第三题答案?",小红答"42"。
对比一下 Topic 和 Service:
Service 里有两个角色:
- 客户端(Client):发起提问的一方(小明);
- 服务端(Server):负责回答的一方(小红)。
关键难题:怎么把"答"和"问"对上号?
这是 Service 最核心的机制,一定要理解。
想象一个热闹的场景:小莫同时问了服务端三个问题("几点了?""剩多少电?""温度多少?")。服务端陆续回了三个答复。问题来了——小莫怎么知道哪个答复对应哪个问题?
如果答复只是"42""87""25"三个数字飞回来,小莫完全对不上号!
解决办法:给每个问题编一个号。 小莫问的时候,在问题上贴个标签"这是 1 号问题";服务端回答时,也在答复上贴回"这是对 1 号问题的回答"。这样小莫一看编号就对上了。
这个编号,在 DORA 里叫 request_id(请求编号)。它就是上一节说的"在黑板角落多写的小暗号"——Service 本质还是 Topic,只是多约定了一个 request_id 元数据来配对。
就像去食堂点餐拿的取餐号!我点餐时拿个"38 号",做好了广播"38 号取餐",我一听就知道是我的。request_id 就是这个取餐号,让我的问和答不会错乱。
元数据(metadata):贴在数据上的"便签"
要贴这个编号,我们要用到一个新东西:元数据(metadata)。
回忆第四章的 echo,它转发时写过 event["metadata"],当时我们说它是"写在黑板角落的备注"。现在正式认识它:
- 数据(value):消息的主体内容(比如问题的参数
a=2, b=3); - 元数据(metadata):附在数据旁边的额外说明(比如"这是 1 号请求")。
在 Python 里,发送时通过 send_output 的第三个参数带上元数据,它是一个普通字典:
接收方从事件里读回元数据:
动手实现:一个加法服务
我们来做一个完整的例子:客户端不断问"a + b 等于几",服务端算出来答回去。目录 course/ch06-service。
客户端 client.py
四个关键步骤都标了号:① 生成编号 → ② 发请求时贴上 → ③ 收答复时读回 → ④ 用编号找回当初的问题。
服务端 server.py
服务端计算完,回答复时必须带回请求里的 request_id。最省事的做法就是 metadata=event["metadata"]——把收到的整张便签原样贴回去。忘了传,客户端就对不上号、认不出这是谁的答复。
连成数据流 dataflow.yml
注意这里的连线形成了一个回路:client 的 request → server,server 的 response → client。一问一答的双向就是这么连出来的。
跑起来
你会看到问答成对出现:
每个"答"都精确对上了它的"问"——这就是 request_id 的功劳。
看那个 ✓!每次我都能确认"这个答复正是我刚才那个问题的"。哪怕我一口气问一堆,也不会张冠李戴。取餐号真好用~
Service 和 Topic 到底啥关系?
回扣上一节埋的伏笔。你可能发现了:上面的代码里,我们从头到尾用的还是 send_output 和 inputs/outputs——和 Topic 一模一样!
区别只有两点:
- 连成了回路:client 和 server 互为对方的上下游(双向);
- 约定了
request_id:靠这个元数据把请求和应答配对。
所以正如上节所说:Service 不是新机制,它就是"Topic + request_id 约定"。 底层还是那块黑板,只是多贴了取餐号。
进阶延伸:真实项目里的超时与容错(可跳过)
我们的例子假设服务端总能及时回答。但真实系统里,服务端可能崩溃、可能很慢。成熟的做法会给"等答复"加一个超时(timeout):等太久就放弃、走备用方案,避免客户端傻等。
DORA 的 Rust API 提供了 recv_service_response 这样的辅助方法,内置超时和"服务端重启"处理。Python 里目前多用手写逻辑(记录 pending、配合超时判断)。零基础阶段先掌握"编号配对"的核心思想即可,超时容错等你做真实项目时再深入。
动手练习
改造服务端:收到请求后,不做加法,而是回一个当前的计数值(每收到一个请求就 +1,表示"这是我处理的第几个请求")。客户端照常用 request_id 匹配。
想一想:request_id 的匹配逻辑需要改吗?
参考答案
服务端维护一个计数器,request_id 匹配逻辑完全不用改——因为编号配对和"回答内容是什么"无关:
这说明 request_id 机制是通用的配对手段,和业务内容解耦。
常见报错 FAQ
req_id in pending 总是 False(对不上)
最常见原因:服务端没把 request_id 传回来。检查服务端 send_output 是否带了 metadata=event["metadata"]。
KeyError: 'request_id'
读元数据时键名拼错,或发送方根本没写这个键。确认发送和接收两端用的键名完全一致(都用 "request_id")。
uuid 相关报错
确认文件顶部 import uuid。我们用的是 uuid.uuid4()(随机编号),它在所有 Python 版本都能用,适合做配对。
小结
- Service(请求应答) 是"一问一答、有来有回"的双向通信,有客户端和服务端两个角色。
- 靠
request_id(请求编号,像取餐号) 把每个应答和它对应的请求配对。 - 元数据(metadata) 是贴在数据上的便签,用
send_output(..., metadata={...})发、用event["metadata"]读;服务端要把编号原样传回。 - Service 本质是 Topic + request_id 约定——底层还是发布订阅。
下一节学第三种模式 Action(长任务)——当小莫要做一件耗时的事、还想边做边汇报进度时该怎么办。