前幾天看到一篇貼文在整理常見的協定,好像有七、八種。看完我在思考的事情是:每個協定最適合的場景到底是什麼?

協定比較文多半長一個樣子:一張表,左邊是協定,右邊是「雙向 ✅/單向 ❌」「延遲 低/中」。看完之後,遇到一條真的連線——例如「餐廳平板要怎麼收到新訂單」——還是不知道該選哪個。

因為選協定看的不是協定本身,是連線兩端的限制:誰先開口、對方是不是自己人、對方有沒有公開網址、對方會不會斷線、訊息掉了可不可以。表格裡沒有這幾欄。

所以我做了一個外送平台,把十一種協定各放在真實系統會用它的位置,再讓一張訂單從頭走到尾,把線上每一則請求、回應、推送都記下來。

外送世界:先分清楚誰是外人

平台裡的角色分兩邊。外部是平台管不到的:顧客的手機、餐廳的平板、金流商、外送員的手機、客服用的 AI agent。內部是平台自己的服務:對外的 api、管菜單價格的 catalog、派單的 dispatch、寄收據的 notification、營運報表 reports,以及三個 broker(rabbitmq、mosquitto、kafka)。

外送世界全景圖:左邊是外部(外送員、金流商、顧客、餐廳平板、客服 AI agent),右邊是內部服務與三個 broker,十一條連線各標上協定

這條線畫下去,很多選擇就自己出來了:

  • 外部的對象只能用它會講、而且穿得過網路的協定。瀏覽器不能直接講 gRPC——grpc.io 自己寫「there is simply no browser API with enough fine-grained control」——所以 gRPC 只出現在 api 到 catalog 這段內部呼叫
  • 餐廳平板在店裡的網路後面,沒有公開網址。平台連不進去,Webhook 直接出局,只剩平板自己連進來問
  • 金流商是外部公司,但平台有公開網址,所以反過來,金流商用 Webhook 通知平台

跟著一張訂單走

下面是真的跑一次記下來的:訂單 ord_eca449a5,從顧客打開店家頁到客服 AI 查單,共 47 則事件。

段誰 → 誰協定為什麼是它
1 看店家頁顧客 → apiGraphQL一個畫面要店名、菜單、評價,一次說清楚要哪些欄位
2 平板先等著平板 → apiLong polling平板沒有公開網址,只能自己發請求、讓它掛著等
3 下單顧客 → api → catalogREST+gRPC訂單是資源用 REST;api 問價格是每單都要的內部呼叫,用 gRPC
4 打開訂單頁api → 顧客SSE只有平台往顧客推,斷線要能補
5 付款成功金流商 → api → rabbitmqWebhook+AMQP外部公司通知平台;平台內部把「已付款」交給 broker 分送派單、寄收據
6 接單出餐平板 → api → 顧客REST+SSE平板的操作是一般請求;狀態一變就順著第 4 段的 SSE 推給顧客
7 回報位置外送員 → mosquitto → dispatchMQTT手機網路不穩、要省電、每幾秒一筆
8 聊天顧客 ↔ 外送員WebSocket兩邊都會隨時講話
9 客服查單AI agent → apiJSON-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,之後才加的讀者也拿不到。數字在七組對照實驗。

做完之後

現在遇到一條新的連線,我會先想一下應用情境,再去看哪一種協定適合。情境指的就是前面那幾件事:誰先開口、對方是誰、要不要即時雙向、訊息能不能掉。

延伸閱讀