不去问 — 轮询比「浪费」更糟的地方

5 分钟
目标MCP 客户端课程最后一篇。订阅、被通知、然后再读。轮询费资源只是次要的——真正的问题是:两次询问之间值变了两次,中间那一下永远看不到。

到第四篇为止,想知道最新的值就得去问——每秒问一次,或者靠人刷新。

订阅

client.onResourceUpdated((uri) {
  // The notification says only that it changed. Read to learn what to.
  seen.add(uri);
  stdout.writeln('notified: $uri');
});

await client.subscribeResource('desk://waiting');

然后让值真的变一下。

await client.callTool('desk.admit', {'count': 1});
subscribed to desk://waiting
notified: desk://waiting
after the notification, reading gives {waiting: 2}
notifications received: 1

通知只带 uri

处理器收到的是一个 uri。变成几个人不会来。服务端第六篇讲了为什么不带值;在接收这一侧,它的意思就是 你必须去读。

final now = await client.readResource('desk://waiting');

多走一趟看着吃亏,可这样永远是对的。不管来几条通知、什么顺序,读的那一刻的值 就是最新的。

轮询真正的问题

拒绝轮询的理由通常是成本。每秒敲一次服务端是浪费。

那是次要的。真正的问题是,你看不到两次询问之间。

09:00:00    轮询 → waiting 3
09:00:00.4  进来两个 → waiting 1
09:00:00.7  来了一个 → waiting 2
09:00:01    轮询 → waiting 2

画面从 3 到了 2。它没看到曾经是 1 的那一刻。 如果只要最终值,无所谓;可要是有「归零就报警」这类规则,那条规则永远不会触发。

把间隔调小也消不掉,只是变窄。而越窄,浪费越大。

结束就退订

await client.unsubscribeResource('desk://waiting');

画面关掉就退订。不退,服务端会一直往一个不存在的对象发。

真板那篇里的 ESP32 把这件事放进了 画面定义里 —— onReady 订阅,onDestroy 解除。生命周期写在画面上,客户端就忘不掉。

校验 —— 是轮询就失败

grep -q 'notified: desk://waiting' captures/s5.txt || die "no notification arrived"
grep -q 'notifications received: 1' captures/s5.txt || die "wrong notification count"
grep -q 'reading gives {waiting: 2}' captures/s5.txt || die "the read after the notification was stale"

# Polling would show up as a timer; the claim is that there is none.
grep -qE 'Timer|periodic' bin/step5.dart && die "it is polling, not subscribing"

代码里出现 Timer 或 periodic 就失败。因为只看结果,轮询和订阅长得一模一样 —— 两个都会在屏上显示 2。

数通知条数也是同样的道理。0 说明没来,好几条说明重复订阅了。

五篇结束了

step1  connected to Course 1.0.0
step2  tools: desk.admit
       resources: ui://desk, desk://waiting
step3  admit 1 -> {waiting: 2}
       admit 99 -> isError=true "only 2 waiting"
step4  ui://desk — 546 B, 6 lines, type "page", title "Desk"
step5  notifications received: 1 · after the notification, reading gives {waiting: 2}

5 steps · each checked against its own claim · driven against the course server

没有 mock 服务端。 五级全都真的把服务端课程里做的那台服务端起起来再接。所以两条轨一旦走偏,就会在这儿卡住——第三篇里真的卡了一次。

自己跑一遍

cd content/sample/course-client
dart pub get
bash verify.sh            # 五级全跑
dart run bin/step5.dart

可以带走的

  1. onResourceUpdated + subscribeResource — 先挂处理器再订阅
  2. 通知之后去读 — 通知只带 uri
  3. 禁止轮询的检查 — 结果看起来一样,所以要看代码

下一个轨道

服务端和客户端都立住了。下一段是画面本身——一坨 JSON 是怎么变成一块画面的。

运行示例

makemind-academy/course_client/
git clone https://github.com/makemind-academy/course_client
cd course_client
dart pub get
(cd course-server && dart pub get)
dart run bin/step5.dart
在 GitHub 上打开 →