前幾天看到一篇貼文在整理常見的協定,好像有七、八種。看完我在思考的事情是:每個協定最適合的場景到底是什麼?
協定比較文多半長一個樣子:一張表,左邊是協定,右邊是「雙向 ✅/單向 ❌」「延遲 低/中」。看完之後,遇到一條真的連線——例如「餐廳平板要怎麼收到新訂單」——還是不知道該選哪個。
因為選協定看的不是協定本身,是連線兩端的限制:誰先開口、對方是不是自己人、對方有沒有公開網址、對方會不會斷線、訊息掉了可不可以。表格裡沒有這幾欄。
所以我做了一個外送平台,把十一種協定各放在真實系統會用它的位置,再讓一張訂單從頭走到尾,把線上每一則請求、回應、推送都記下來。
外送世界:先分清楚誰是外人
平台裡的角色分兩邊。外部是平台管不到的:顧客的手機、餐廳的平板、金流商、外送員的手機、客服用的 AI agent。內部是平台自己的服務:對外的 api、管菜單價格的 catalog、派單的 dispatch、寄收據的 notification、營運報表 reports,以及三個 broker(rabbitmq、mosquitto、kafka)。

這條線畫下去,很多選擇就自己出來了:
- 外部的對象只能用它會講、而且穿得過網路的協定。瀏覽器不能直接講 gRPC——grpc.io 自己寫「there is simply no browser API with enough fine-grained control」——所以 gRPC 只出現在 api 到 catalog 這段內部呼叫
- 餐廳平板在店裡的網路後面,沒有公開網址。平台連不進去,Webhook 直接出局,只剩平板自己連進來問
- 金流商是外部公司,但平台有公開網址,所以反過來,金流商用 Webhook 通知平台
跟著一張訂單走
下面是真的跑一次記下來的:訂單 ord_eca449a5,從顧客打開店家頁到客服 AI 查單,共 47 則事件。
| 段 | 誰 → 誰 | 協定 | 為什麼是它 |
|---|---|---|---|
| 1 看店家頁 | 顧客 → api | GraphQL | 一個畫面要店名、菜單、評價,一次說清楚要哪些欄位 |
| 2 平板先等著 | 平板 → api | Long polling | 平板沒有公開網址,只能自己發請求、讓它掛著等 |
| 3 下單 | 顧客 → api → catalog | REST+gRPC | 訂單是資源用 REST;api 問價格是每單都要的內部呼叫,用 gRPC |
| 4 打開訂單頁 | api → 顧客 | SSE | 只有平台往顧客推,斷線要能補 |
| 5 付款成功 | 金流商 → api → rabbitmq | Webhook+AMQP | 外部公司通知平台;平台內部把「已付款」交給 broker 分送派單、寄收據 |
| 6 接單出餐 | 平板 → api → 顧客 | REST+SSE | 平板的操作是一般請求;狀態一變就順著第 4 段的 SSE 推給顧客 |
| 7 回報位置 | 外送員 → mosquitto → dispatch | MQTT | 手機網路不穩、要省電、每幾秒一筆 |
| 8 聊天 | 顧客 ↔ 外送員 | WebSocket | 兩邊都會隨時講話 |
| 9 客服查單 | AI agent → api | JSON-RPC(MCP) | MCP 規定訊息用 JSON-RPC 2.0 |
幾個實錄裡的片段:
- 第 2 段的請求是第 3 則事件送出的,一直掛著;到第 13 則才回——那時金流商的 Webhook(第 11 則)剛到,訂單變成已付款。平板從頭到尾沒有開任何對外的 port
- 第 3 段,顧客的
POST /orders是第 4 則,回應是第 7 則,中間第 5、6 則是 api 去問 catalog 的 gRPC 呼叫。顧客看到的是一個請求,背後其實是兩個協定 - 顧客的 SSE 依序收到五個狀態:pending_payment、paid、accepted、preparing、ready,一個都沒漏
- 第 9 段,AI agent 送
notifications/initialized,api 回 202、沒有內容——JSON-RPC 的「通知」本來就不回覆
只有跑起來才看得到的事
AMQP 的 confirm 比接收端還晚到。 第 12 則是 api 把「已付款」發布到 rabbitmq;第 16、17 則 dispatch 和 notification 已經收到、處理完、ack 了;broker 回給 api 的 confirm 卻排在第 22 則,晚到平板都已經接單。我錄了兩次,兩次 confirm 都比兩個接收端晚。
這件事在規格裡不會寫成「一定」——RabbitMQ 文件只說 broker「confirms messages as it handles them」。但它推翻了一個很自然的假設:發布端拿到 confirm 的時候,下游可能早就處理完了。 如果你的程式在 confirm 回來之後才去做某件「下游處理前要做」的事,順序就錯了。
記錄的時機本身會說謊。 第一版的錄製是在 hook 回來之後才記,畫出來 AMQP 的「發布」排在「dispatch 收到」之後,MQTT 的「dispatch 收到上線」排在「外送員連線」之前。原因都一樣:hook 的呼叫時機比真正送出晚。改成「請求在送出前記、回應在收到時記」,順序才對。
這兩件事,都滿意外的。
Kafka 為什麼不在這趟旅程裡
十一種協定裡,Kafka 是後來加的,也沒有放進這張訂單的旅程。不是漏掉,是它跟其他十個的關係不一樣。
其他十個都在回答「這件事現在要告訴誰」。Kafka 回答的是「之後才出現的人,能不能看到以前發生的事」:營運報表在訂單都寫完之後才上線、財務又更晚,它們要的是歷史。Kafka 官方文件的說法是「events are not deleted after consumption」,這正是 AMQP 做不到的——AMQP 的訊息處理完、ack 之後就刪了。
這個差別有對照實驗:事件寫完之後才上線的讀者,Kafka 拿得到全部,AMQP 就算事先替報表準備好 queue,之後才加的讀者也拿不到。數字在七組對照實驗。
做完之後
現在遇到一條新的連線,我會先想一下應用情境,再去看哪一種協定適合。情境指的就是前面那幾件事:誰先開口、對方是誰、要不要即時雙向、訊息能不能掉。