消息中間件 一 之 AMQP譯文(上)

RabbitMQ是AMQP的實現成果, 所以在研究RabbitMQ之前, 不如先看看協議本身.

1 Overview 概述

1.1 Goals of This Document 文檔目標

本文檔定義了網絡協議AMQP, 客戶端可以使用該協議與消息中間件服務器進行通信. 我們面向的是在本領域有一定經驗的技術人員, 我們會提供充分的文檔, 工程師可以通過這些文檔使用任何高級語言或者硬件平臺構建解決方案.

1.2 Summary 概述

1.2.1 Why AMQP 爲什麼用AMQP

AMQP提供了完整的方法支持客戶端和消息中間件服務器之間通信的操作. 我們希望開發和使用都使用標準化的消息中間件技術, 這樣會降低企業的花銷和系統集成的成本, 而且會爲大衆提供一個企業級的集成服務. 我們的目標是通過AMQP消息中間件可以驅動網絡本身, 而且通過消息中間件可以開發出新的有用的應用.

1.2.2 Scope of AMQP

爲了完成一個完整功能的消息中間件需要兩方面的支持, 詳細的網絡協議和服務端服務.
因此AMQP在這兩方面做了如下定義:
1 AMQ模型是定義的一套消息處理操作, 它包含了一套在服務端處理消息路由和消息存儲的組件, 以及關聯這些組件的規則.
2 AMQP是一個wire-level(?)的網絡協議, 它允許客戶端應用和服務端進行通信, 而且可以通過自己實現的AMQ模型進行交互.

雖然通過AMQP可以大概推斷出服務端的消息操作, 但是我們認爲對服務端有個詳細的講解有助於理解協議.

1.2.3 The Advanced Message Queuing Model (AMQ model)

我們定義了一些服務端的名詞, 因爲這些操作在任何服務端實現中都應該表現的一樣, 因此在AMQ模型中定義了一些列組件以及這些組件如何關聯的標準規則. 下面是三個重要的組件, 他們貫穿服務端整個執行過程.
1 exchange, 它接收來自生產者應用的信息, 而且通過一些特定的條件路由信息到Message queues, 這些條件通常是信息的屬性或者信息的內容.
2 message queue, 它會一直存儲信息, 直到消息被一個或多個消費者應用安全的使用後纔會刪除該信息.
3 binding, 它定義了exchangemessage queue之間的關係, 也就是提供上面提到的路由規則

使用AMQ模型我們可以簡單模擬傳統的面向消息的中間件概念, 比如存儲轉發隊列和主題訂閱. 當然我們也可以描述一些更難的概念, 比如基於內容的路由, 工作負載分發, 根據需求改變的消息隊列. 籠統的說, AMQP服務器就像是一個郵件服務器, exchange就像是郵件發送代理, message queue就像是郵箱. binding就是發送代理中的路由表. 發佈者發送信息給一個特定的代理, 代理會路由該信息到郵箱. 與AMQP相比, 之前的中間件都是發送者之間發送信息到郵箱(存儲轉發隊列), 或者是郵件列表(主題訂閱).

這個區別就在於綁定exchangemessage queue的規則是由工程師規定的而不是集成在代碼中的, 這樣的話我們就有可能做一些有趣的事, 比如定義這樣的規則: 複製所有包含某某消息頭的消息到某個message queue. AMQ模型的設計依據以下幾個需求:
1 支持主流消息產品中的一些名詞概念
2 提供和主流信息產品相當的性能
3 允許應用程序通過協議對服務端的特定組件進行編程
4 靈活, 擴展性, 簡單

1.2.4 The Advanced Message Queuing Protocol (AMQP)

AMQP協議是一個有現代特性的二進制協議: 多通道, negotiated, 異步, 安全, 可移植, neutral, 高效. AMQP被有效的分爲兩層:
在這裏插入圖片描述

功能層定義了一套命令(通過功能邏輯分類), 這些命令可以表示客戶端應用的操作.
傳輸層負責客戶端到服務端的方法執行, 信道的多路複用, framing(?), 內容編碼, 心跳操作, 數據展示, 錯誤處理. 我們可以在不改變協議的應用層(就是應用可見的部分)的情況下替換任意的傳輸層. 我們也可以爲不同的高級協議使用相同的傳輸層.
所以AMQ模型需要滿足下面的需求:
1 保證一致性實現下的操作性
2 提供明確的服務質量控制
3 命名顯式而且一致
4 允許通過協議完成服務器連接的配置
5 使用容易映射應用級api的命令
6 爲了清晰, 每個操作只做一件事.

AMQP傳輸層的設計依賴下面的需求, 不分先後:
1 爲了信息緊湊, 使用二進制編碼, 這樣打包拆包很快.
2 不限制信息長度
3 在一個連接上實現多個通道
4 長連接, 不需要特殊內部限制
5 允許在管道內使用異步命令
6 擴展方便
7 向前兼容
8 可補償的, 通過使用強斷言模型(?)
9 對編程語言採取中立態度
10 適應代碼發展過程(?)

1.2.5 Scales of Deployment 部署規模

AMQP使用場景包含了不同的規模, 大體如下:
1 開發者使用: 1個服務端, 1個用戶, 10個消息隊列, 每秒一條信息.
2 產品應用: 2個服務端, 10-100用戶, 10-50個消息隊列, 每秒十條信息(每小時36K條信息)
3 部門關鍵應用: 4個服務端, 100-500用戶, 50-100個消息隊列, 每秒100條信息(每小時360K條信息)
4 地區關鍵應用: 略
5 全球關鍵應用: 略
6 市場數據(交易): 略
與容量一樣, 信息的延遲也是非常重要的. 比如市場數據很快就會沒有用. 所以我們可以在遵從規範的前提下提供不同的服務質量.

1.2.6 Functional Scope

我們希望提供一種多樣化的信息架構
1 多對一的存儲轉發
2 多對多的工作負載分發
3 多對多的發佈訂閱
4 多對多的基於內容路由
5 多對多的文件傳輸
6 兩個終端的點對點連接
7 大數據分佈式下的多對多

1.3 Organisation of This Document 文檔的組織結構

文檔分爲五章, 大部分是比較獨立的, 你可以閱讀自己感興趣的部分:
1 overview(本章). 就是個介紹
2 General Architecture, 這章我們是描述了整體架構和AMQP的整體設計. 我們希望通過這章可以讓工程師知道AMQP是如何工作的.
3 Functional Specifications, 這章我們定義了應用是如何使用AMQP的. 這章在每一個協議命令後會有一個小討論來作爲一種實現例子. 閱讀這章之前需要閱讀前一章.
4 Technical Specifications, 這章我們講了AMQP的傳輸層是如何工作的.

1.4 Conventions 約定

2 General Architecture 整體架構

2.1 AMQ Model Architecture AMQ模型架構

This section explains the server semantics that must be standardised in order to guarantee interoperability between AMQP implementations.

2.1.1 Main Entities 主要組件

下面這張圖展示了AMQ模型的整體結構
在這裏插入圖片描述
我們可以總結下一個中間件服務器是什麼: 中間件服務器是一個數據服務器, 它接收信息而且只對信息主要做兩件事, 一是通過特定的條件將信息路由給不同的消費者, 二是當消費者沒有辦法馬上接收信息的時候中間件緩存信息到內存或者硬盤.
之前的中間件服務器通常是通過一個完整的引擎實現各種特定的路由和存儲. AMQ模型將實現劃分爲更小的模塊化的, 然後以更多樣化更強健的方式實現. 首先會將以上任務劃分爲兩個截然不同的角色:
1 exchange, 從生產者接收信息, 並路由信息到message queue
2 message queue, 存儲信息並轉發信息給消費者.

後面我們會介紹. AMQP提供了運行期的編程能力, 主要通過以下兩個發麪實現:
1 在運行期通過協議創建任意的exchangemessage queue
2 在運行期通過協議綁定exchangemessage queue以此創建任意的消息處理系統.

2.1.1.1 The Message Queue 信息隊列

消息隊列可以存儲信息到內存或者硬盤, 然後按一定順序分發信息給一個或多個消費者. 消息隊列就是消息的存儲和分發組件. 每個消息隊列是完全獨立的, 而且相當智能. 消息隊列有很多屬性: 私有的或者共享的, 持久的或者臨時的, 客戶端命名的或者服務端命名的等等. 通過選擇適當的屬性, 我們可以使用消息隊列實現傳統中間件功能, 比如:
1 一個共享的存儲轉發隊列, 用來保存信息和按照輪詢的方式分發信息給消費者. 存儲轉發隊列通常是持久化隊列, 而且是在多個消費者間共享的.
2 一個私有應答(?)隊列, 只存儲和發送信息給一個消費者. 應答隊列通常是臨時的, 由服務端命名的, 而且是消費者私有的隊列.
3 一個私有的訂閱隊列, 會通過訂閱信息收集信息, 然後分發這些信息給唯一的消費者. 訂閱隊列農場是臨時的, 由服務端命名的, 而且是消費者私有的.
這些條目沒有在AMQP中正式定義: 它們只是告訴我們如何使用消息隊列的例子. 所以像創建一個持久的共享的訂閱隊列這種操作是非常容易的.

2.1.1.2 The Exchange 交換機(太難聽, 後面就直接用exchange)

exchange會接收生產者發送的信息, 然後根據事先排列好的條件路由信息到不同信息隊列. 這些條件叫做bindings. exchange是綁定和路由引擎. 也就是說exchange會檢查信息, 然後使用自己的綁定表來決定怎麼發送信息給消息隊列或者其他的exchange. exchange絕對不會存儲信息. exchange這個詞既是指一類算法也是指這類算法的實例. 爲了更恰當, 我們使用exchange類型和exchange實例.
AMQP定義了一系列標準exchange類型, 這些類型覆蓋了基本的路由分發信息所需要的類型. AMQP服務器會默認提供這些類型對應的exchange實例. 應用端在使用AMQP中可以額外創建自己的exchange實例. exchange類型需要命名以便應用可以使用他們自己創建的exchange類型. 同樣exchange實例也需要命名, 這樣應用才知道怎麼去綁定隊列和發佈者消息.

2.1.1.3 The Routing Key

通常情況下, exchange會檢查信息屬性, 它的頭信息和內容信息, 使用這些信息和其他合理信息來決定如何路由這條信息.
在大多數簡單情況下, exchange只檢查信息的一個被叫做routing key的字段. routing key是exchange可以用來決定如何路由信息的虛擬地址.
對於點對點路由, routing key通常是信息隊列的名稱. 對於發佈訂閱路由, routing key通常是一個有層級結構的主題值.
在更復雜的情況下, routing key可能要結合信息的頭信息或者內容信息.

2.1.1.4 Analogy to Email 類比郵箱系統

類比郵箱系統來看AMQP中的概念, 我們會發現這些概念並不是很新奇.
1 AMQP信息與郵件信息類似
2 消息隊列就像是一個郵箱
3 消費者就像一個可以獲取和刪除郵件的郵件客戶端
4 exchange就像一個MTA郵件轉發代理, MTA會檢查郵件而且根據routing key和路由表決定如何發送信息給一個或多個郵箱.
5 a routing key corresponds to an email To: or Cc: or Bcc: address, without the server information (routing is entirely internal to an AMQP server)😭?)
6 exchange實例就像是獨立的MTA進程, 它們處理一些郵件子域, 或者特殊的郵件通信類型.
7 binding就像MTA中路由表中的條目

AMQP的優勢是我們可以在運行時創建消息隊列, exchange, bindings, 而且我們可以編排這些組件的關係, 使得它遠比只是映射到郵箱名字這樣的方式複雜.
我們不應該太深入的將AMQP和郵箱類比, 它們還是有本質的區別的. 對於AMQP來說挑戰是在服務器中路由和轉發信息, 這種服務器按照SMTP的說法是"自主系統". 相較而言, 郵箱系統的挑戰是如何在自主系統間路由信息. 在系統間和在系統中路由信息是完全不同的問題而且有不同的解決方法, 即使只是爲了維護性能這樣的普通問題.(這句話應該是說這麼簡單的問題的解決也是不一樣的, 突出兩個問題的不同?)
爲了在不同的AMQP服務器之間路由信息, 需要構建溝通的橋樑, 也就是爲了傳輸信息其中一個服務器需要充當其他服務器的客戶端. 這種工作方式一般適合預期使用AMQP的業務類型,因爲這些橋樑可能由業務流程、合同義務和安全考慮來支撐的.

2.1.2 Message Flow 信息流轉

This diagram shows the flow of messages through the AMQ model server:
下面這幅圖展示了AMQ模型中信息是如何流轉的
在這裏插入圖片描述

2.1.2.1 Message Life-cycle 信息聲明週期

AMQP信息包括一系列屬性外加可見的內容.
一個新的信息是由生產者應用通過AMQP客戶端api創建的. 生產者將信息內容放到信息中, 可能還會加入一些屬性. 生產者使用路由信息標記信息, 這個路由信息特別像是一個地址, 但是結構更多樣複雜. 然後生產者會發送信息給服務器上的exchange. 當信息到達服務器, exchange通常會路由這些信息給服務器上已經存在的消息隊列. 如果信息沒有被路由, 那麼exchange可能會默默的刪除這條信息, 或者把信息返回給生產者. 這裏就需要生產者自己選擇信息沒有被路由時的執行策略了.

一條信息可以存在於多個信息隊列. 服務器可以通過多種方法實現, 比如複製信息, 使用引用計數等(?). 這樣做並不會影響功能. 但是, 當一個信息被路由到多個隊列, 那麼這些隊列中該條信息是一模一樣的, 沒有唯一的信息可以區分這些副本.
當一條信息到達信息隊列, 信息隊列會馬上嘗試通過AMQP傳輸信息給消費者. 如果沒有辦法傳輸, 消息隊列會保存信息(至於是保存到內存還是硬盤需要生產者指定)直到消費者可以接受信息. 如果沒有消費者, 消息隊列可能會通過AMQP返回這個信息給生產者(同樣, 如果生產者這樣指定的話).

當信息隊列可以發送信息給消費者時, 消息隊列會從它的內部緩存中刪除這條信息. 可能會立即刪除, 也可能等接收到消費者確認成功消費信息之後. 由消費者決定什麼時候什麼方式告知信息隊列.消費者也可以拒絕信息(消極確認).
生產者發送信息和消費者確認信息屬於事務範疇. 現實經常出現一個應用既是生產者也是消費者的情況, 這時候它既要發送信息也要發送確認信息, 然後提交或者回滾事務. 信息從服務端發送給消費者是沒有事務的, 只對確認信息執行事務已經足夠了.

2.1.2.2 What The Producer Sees 生產者視角

通過前面類比郵件系統, 我們知道生產者不會直接發送信息給消費者. 否則會打亂AMQ模型. 就好像允許郵件繞開MTA的路由表直接到達郵箱一樣. 這樣的話就沒有辦法增加一些中間過濾器和操作, 比如垃圾郵件檢測. AMQ模型使用和郵件系統一樣的原則: 所有的信息都會發送到一個地方, exchange或者MTA, 這個地方可以通過一些發送者不知道的規則和信息檢測信息, 然後路由信息到下一個節點, 這個操作對於發送者也是透明的.

2.1.2.3 What The Consumer Sees 消費者視角

從消費者的視角來看會打破我們之前的郵件系統類比. 郵件客戶端完全是被動的, 它們可以查看它們的郵箱, 但是它們不會知道它們的郵箱是如何被填滿的. AMQP消費者也可以像郵件系統客戶端那樣. 我們創建一個應用, 該應用只接收來自特定消息隊列的信息並處理.
但是我們也可以讓AMQP客戶端應用有下面的功能:
1 創建或者銷燬消息隊列
2 通過創建binding, 客戶端可以定義消息隊列接收什麼樣的信息
3 選擇不同的exchange, 這樣就完全改變了路由規則
對於郵件系統這些功能就像是:
1 創建一個郵箱
2 告訴MTA把包含特殊頭信息的信息都複製到這個郵箱
3 完全改變郵件系統解析地址和其他消息頭的方式
我們發現AMQP與其說是系統其實更像一個可以編寫不同組件到一起的語言. 這也是我們一個目標: 通過協議可以編寫服務端功能.

2.1.2.4 Automatic Mode 自動模式(默認模式?)

大部分集成架構不需要上面說到的這種複雜模式. 就像是一個攝影愛好者, 大部分AMQP用戶需要一種"傻瓜"模式. AMQP通過以下兩個簡單原則提供這種模式:
1 爲生產者提供默認exchange
2 爲消息隊列提供一個默認binding, 基於消息隊列的名稱.
實際上, 默認binding允許生產者直接發送信息給消息隊列, 並授予適當權限. 這種模式模擬了人們期望的傳統中間件發送信息到目的地的最簡單的方式.
默認binding並不會限制消息隊列在複雜場景的應用. 但是它確實可以讓用戶在使用AMQP的時候不用關心exchange和binding的工作.

2.1.3 Exchanges

2.1.3.1 Types of Exchange

每一種exchange類型都實現了一種特殊的路由算法. 有一些標準exchange類型會在後面的章節介紹, 但是其中有兩個特別重要:
1 直連exchange, 它通過一個routing key路由信息, 默認的exchange就是一個直連exchange.
2 主題exchange, 它通過一個路由模板路由信息.
服務端會在啓動的時候創建一些周知的exchange, 它們有直連的和主題的, 客戶端可以使用它們.

2.1.3.2 Exchange Life-cycle exchange聲明週期

每一個AMQP服務器都會預先創建一系列exchange實例. 這些exchange會在服務器啓動的時候創建而且不會被銷燬. AMQP應用也可以自己創建自己的exchange. AMQP在創建exchange的時候不會使用像是"創建"這樣的方法, 而是類似"聲明"方法, 它的意思是: 如果當前沒有則創建, 否則繼續. 通常我們覺得應用創建exchange私用, 然後當處理完工作後銷燬這樣是合理的. 而且AMQP也提供了銷燬方法, 但是通常應用是不需要這樣做的. 在我們本章的例子中, 我們假設exchange都是由服務器創建的, 我們不會展示應用聲明exchange.

2.1.4 Message Queues

2.1.4.1 Message Queue Properties 消息隊列屬性

當客戶端應用創建消息隊列時, 有一些重要屬性可以選擇:
1 名稱, 如果沒有設置, 服務端會自動給客戶端命名. 通常情況下, 當應用間共享消息隊列時, 一般會先對消息隊列的名稱達成一致, 當應用自己使用消息隊列是, 它可以讓服務端自己命名隊列.
2 專一的, 如果設置, 該隊列只屬於當前連接, 當連接斷開時隊列也被銷燬
3 持久化, 如果設置, 服務器重啓消息隊列還會存在, 但是隊列會丟失臨時信息.

2.1.4.2 Queue Life-cycles 隊列的生命週期

有兩個主要的消息隊列生命週期:
1 持久化消息隊列, 它會被多個消費者共享, 而且可以獨立存在.比如, 無論是否有消費者接收信息, 該隊列都會存在而且接收信息.
2 臨時消息隊列, 它是某個消費者私有的, 而且綁定到該消費者的. 當消費者下線, 消息隊列也會被刪除.
其中還有一些變體, 比如共享消息隊列, 它可以被多個消費者共享, 當最後一個消費者下線後會刪除該消息隊列.下圖展示了一個臨時消息隊列的創建和銷燬
在這裏插入圖片描述

2.1.5 Bindings

binding就是exchange和消息隊列之間的關係, 它可以告訴exchange如何路由信息到消息隊列. binding是擁有和使用隊列的客戶端應用通過命令給exchange創建的. 使用僞代碼表示綁定命令大概如下:
Queue.Bind <queue> TO <exchange> WHERE <condition>
下面我們看看三個典型應用: 共享隊列, 私有應答隊列, 發佈訂閱隊列.

2.1.5.1 Constructing a Shared Queue 構造一個共享隊列

共享隊列是經典中間件的點對點隊列. 在AMQP中我們可以使用默認的exchange和binding. 定義我們的隊列名稱爲"app.svc01". 下面是創建隊列的僞代碼:
Queue.Declare queue=app.svc01
共享隊列可能有多個消費者. 爲了從隊列消費信息, 每個消費者需要這樣:
Basic.Consume queue=app.svc01
對於發送信息, 每個生產者會發送信息給默認exchange:
Basic.Publish routing-key=app.svc01

2.1.5.2 Constructing a Reply Queue 構造一個應答隊列

應答隊列通常是由服務端命名的臨時隊列. 這些隊列通常是私有的, 比如只有一個消費者. 除了這些特殊的部分, 應答隊列使用和標準隊列一個樣的匹配條件, 所以我們也可以使用默認exchange.
下面是創建應答隊列的僞代碼, 其中S:表示一個服務端的響應:

Queue.Declare 
    queue=<empty>
    exclusive=TRUE 
S:Queue.Declare-Ok
    queue=tmp.1

生產者發送信息到默認exchange:

Basic.Publish 
    exchange=<empty>
    routing-key=tmp.1

標準信息屬性中有一個是"Reply-To", 它用來表示應答隊列的名稱.

2.1.5.3 Constructing a Pub-Sub Subscription Queue 構造發佈訂閱隊列

在傳統中間件中, “訂閱"的定義不明確, 它至少表示兩種完全不一樣的概念: 匹配消息的一系列條件和臨時隊列持有的匹配到的信息. AMQP把這部分工作分給了binding和消息隊列. 在AMQP中沒有組件叫做"訂閱”.
我們統一下什麼是發佈訂閱:
1 保存一個或多個消費者信息
2 通過各種不同的方式從多個數據源收集信息, 這些方式可能是匹配主題, 消息內容等.
訂閱隊列和應答隊列最主要的區別是, 訂閱隊列的名稱是和如何路由無關的, 而且路由方式是通過抽象的匹配條件而不是1對1的routing key. 我們來看一個通用的發佈訂閱模型然後實現它. 我們需要一個可以匹配主題樹的exchange類型. 在AMQP中這就是主題exchange. 主題exchange可以匹配通配符, 比如"STOCK.USD.*" 可以匹配到routing key “STOCK.USD.NYSE”. 我們不能使用默認的exchange和binding, 因爲這些不能進行發佈訂閱路由. 所以我們必須創建一個binding. 下面是創建binding和發佈訂閱隊列的僞代碼:

Queue.Declare 
    queue=<empty>
    exclusive=TRUE 
S:Queue.Declare-Ok
    queue=tmp.2 
Queue.Bind
    queue=tmp.2
    TO exchange=amq.topic
    WHERE routing-key=STOCK.USD.*

消費者消費代碼如下:

Basic.Consume 
    queue=tmp.2

生產者發送信息代碼如下:

Basic.Publish
    exchange=amq.topic 
    routing-key=STOCK.USD.ACME

主題exchange執行時會收到routing key (“STOCK.USD.ACME”), 然後根據綁定表, 找到一個匹配隊列tmp.2, 然後就會路由這些信息給這個隊列.

2.2 AMQP Command Architecture AMQP命令架構

這章介紹應用如何和服務端進行通信.

2.2.1 Protocol Commands (Classes & Methods) 協議命令(類和方法)

中間件非常複雜, 我們的挑戰是在設計協議架構時克服這種複雜性. 我們的方法是構建一個傳統的api模型, 該模型會有一些類, 每個類會包含一些方法, 每個方法只做好一件事. 這會導致有大量的命令, 但是另一方面也會相對容易理解.
AMQP命令以類進行分組. 每個類都會覆蓋一個特殊的功能領域. 有些類是可選的, 每一個需要支持的終端可以去實現這些類.
有兩個完全不一樣的方法如下:
1 同步請求響應, 一端發送請求另一端需要給出響應. 同步請求響應用在對性能要求不嚴格的場景.
2 異步通知, 一端發送方法不會等待響應. 異步方法用在對性能要求嚴格的場景.
爲了保證方法執行簡單, 我們爲每一個同步請求定義了不同的響應. 也就是說沒有一個方法是兩個不同請求的響應. 也就是說一個端點在發送一個同步請求後, 還可以接收和處理傳入的方法直到接收到同步響應. 這也是AMQP和大多數傳統RPC協議的區別.
一個方法會被定義爲同步請求同步響應或者異步. 最後, 每個方法都會被定會爲客戶端側的還是服務端側的.

2.2.2 Mapping AMQP to a middleware API

我們講AMQP設計爲可以映射到中間件API. 這個映射是自動的(不是所有方法和參數對應用是由意義的), 但是也需要手動(需要給定一些規則, 後面的所有方法的映射都不需要手動干涉).
這樣的好處是當我們學習了AMQP的語法後, 開發者會發現在他們使用的任何環境都會發現相同的語法提供.
比如, 下面是隊列聲明方法例子:

Queue.Declare 
    queue=my.queue
    auto-delete=TRUE 
    exclusive=FALSE

這個可以轉換爲線級結構(?)
在這裏插入圖片描述
或者一個更高級的API
queue_declare (session, "my.queue", TRUE, FALSE);
映射異步方法的僞代碼如下:
send method to server
同步代碼如下

send request method to server 
repeat
    wait for response from server
    if response is an asynchronous method
        process method (usually, delivered or returned content) 
    else
        assert that method is a valid response for request
        exit repeat 
    end-if
end-repeat

值得注意的是, 對於大多數應用, 中間件可以完全隱藏在技術層, 而且實際使用的API要比中間件的健壯性和性能重要.

2.2.3 No Confirmations 沒有確認

一個拖沓的協議是緩慢的. 我們使用大量的異步來解決性能問題. 通常我們發送信息從一端到另一端, 然後我們儘快結束信息發送, 不等待消息確認. 必要的情況下我們會在更高層面, 比如消費者層實現節流.
因爲我們採用了斷言模型, 所以我們可以忽略確認信息. 我們要麼成功, 要麼失敗然後關閉連接或者通道.
在AMQP中沒有確認. 成功是沒事, 失敗就比較麻煩. 當應用需要跟蹤成功和失敗時, 他們需要使用事務.

2.2.4 The Connection Class 連接類

AMQP是一個連接的協議. 連接被設計爲持久的, 而且可以承載多個通道. 連接的生命週期如下:
1 客戶端打開一個和服務端的TCP/IP連接, 然後發送協議頭. 這是唯一一個客戶端需要發送但是沒有被構造爲方法的信息.
2 客戶端選擇安全策略(Start-Ok, 括號裏的就是一個指令, 表示當前行爲)
3 服務端開始權限驗證過程, 該過程會使用SASL質疑響應模型. 它會發送給客戶端一個質疑(Secure)
4 客戶端發送一個權限響應(Secure-OK). 比如用最簡單的機制, 響應包括用戶名和密碼.
5 如果沒有通過服務端會重發發送質疑信息, 否則就順利通過, 發送一系列參數給客戶端, 比如信息結構的最大長度(Tune)
6 客戶端接收參數, 或者可以降低參數(Tune-Ok)
7 客戶端正式打開連接然後選擇一個虛擬主機(Open)
8 服務端確認選擇的虛擬主機是有效的(Open-Ok)
9 現在客戶端就可以使用自己選中的連接了
10 客戶端或者服務端選擇關閉連接(Close)
11 另一端握手策略關閉連接(Close-Ok)
12 服務端和客戶端關閉它們自己的socket連接
對於連接沒有完全打開的錯誤是不可以握手的. 在檢測協議頭成功之後(後面會詳細介紹細節), 在發送Open或者Open-Ok指令之前, 任何一端如果檢測到錯誤則必須關閉自己的socket而不用發送任何信息.

2.2.5 The Channel Class 通道

AMQP是一個多通道協議. 多通道提供了一種方式, 將重量級的TCP/IP連接分解爲多個輕量級連接. 這樣會使得協議是"防火牆友好的", 因爲這樣對端口的使用就是可預測的. 這也意味着流量整形(traffic shaping?)和其他網絡QoS特性可以很方便的使用.
通道之間是相互獨立的, 而且可以同時執行不同的方法, 有效帶寬會在當前活動的通道間共享.
爲了編程方便, 多線程客戶端應用可能會經常使用"每個線程一個通道"模型, 這樣是被期望和鼓勵的. 然而, 一個客戶端也可以打開多個連接和一個或者多個AMQP服務器, 這樣也是完全可以的. 通道的生命週期如下:
1 客戶端打開一個新的通道(Open)
2 服務端確認新通道已經準備就緒(Open-Ok)
3 客戶端和服務端開始使用通道
4 任何一端關閉通道(Close)
5 另一端握手關閉通道(Close-Ok)

2.2.6 The Exchange Class

exchange類允許客戶端應用管理服務端的exchange. 該類允許應用通過腳本修改自己的鏈路(應該指的是動態修改路由鏈路?)而不是依賴一些配置接口. 注意: 大部分的應用不需要這種複雜程度, 現存的中間件(legacy)也不太可能支持這種語法. exchange聲明週期如下:
1 客戶端請求服務端確保exchange是否存在(Declare). 客戶端可以完善這個功能, 比如"如果不存在則創建一個exchange", 或者"沒有的話警告我, 但是不要創建"
2 客戶端發送信息給exchange
3 客戶端可以選擇刪除exchange(Delete)

2.2.7 The Queue Class

隊列類允許客戶端應用管理在服務器上的隊列. 對於幾乎所有的應用, 在消費信息的時候最基礎的一步就是至少檢查下要消費信息的隊列是否存在.
持久化信息隊列的生命週期相當簡單:
1 客戶端判斷信息隊列是否存在(Declare, 通過使用"passive"參數)
2 服務端確認信息隊列存在(Declare-Ok)
3 客戶端從消息隊列獲取信息
臨時信息隊列的生命週期更有趣些:
1 客戶端創建一個信息隊列(Declare, 通常是沒有名字的信息隊列, 這樣服務端就要爲這個隊列命名). 服務端確認隊列(Declare-Ok)
2 客戶端創建一個該隊列的消費者. 消費者的嚴格定義在Basic類中.
3 客戶端取消消費者, 可以是顯式的也可以通過關閉通道或者鏈接.
4 當消息隊列的最後一個消費者也消失後, 在一個合適的延遲後, 服務端會刪除該隊列.
AMQP實現了可以發佈訂閱發送消息的消息隊列. 它支持一種有趣的結構, 該結構允許訂閱在訂閱者之間實現負載均衡.
訂閱消息隊列的生命週期包含一個額外的綁定步驟:
1 客戶端創建消息隊列, 服務端確認
2 客戶端綁定信息隊列和主題exchange, 然後服務端確認
3 客戶端使用信息隊列像之前的例子那樣

2.2.8 The Basic Class

Basic類實現了這篇文章描述的信息收發能力. 它支持下面這些重要語法:
1 從客戶端發送信息給服務端, 異步執行(Publish)
2 創建和刪除消費者(Consume, Cancel)
3 從服務端發送信息給客戶端, 異步(Deliver, Return)
4 確認信息(Ack, Reject)
5 同步從信息隊列中獲取信息(Get)

2.2.9 The Transaction Class

AMQP支持兩種事務
1 自動事務, 每一次發送信息和確認都是作爲一個獨立的事務被執行.
2 服務端恩地事務, 服務端會緩存發送的信息和確認信息, 然後提交它們給需要的客戶端.
事務類(“tx”)允許應用使用第二種事務, 也就是服務端事務. 該類支持的語法如下:
1 應用在需要事務的通道請求服務端事務
2 應用正常工作
3 應用提交或者回滾事務
4 應用重複上述流程
事務只覆蓋了發送信息和確認信息, 不包括髮送. 因此, 事務回滾不會重新編排信息或者重新發送信息, 客戶端有權在後面的事務確認該信息.

2.3 AMQP Transport Architecture

這部分解釋了命令如何映射到協議上的.

2.3.1 General Description 總體描述

AMQP是一個二進制協議. 信息會組織到不同類型的幀中. 幀保存了協議的方法和其他信息. 所有的幀都有相同的結構: 幀頭, 負載, 幀尾. 結構負載的格式依賴於幀的類型.
我們假設一個可依賴的面向流的網絡傳輸層(TCP/IP或者其他相當的).
在一個單獨連接中, 可以由多個獨立的控制線程, 這些線程稱作通道. 每一個結構都會用通道號編號. 由於幀的相互交錯, 不同的通道可以共享一個連接. 對於任意一個通道, 每個幀都以嚴格的順序執行, 可以用來驅動協議解析器(通常是一個狀態機)(For any given channel, frames run in a strict sequence that can be used to drive a protocol parser (typically a state machine)?).
我們使用很小的數據類型集來構造幀, 比如比特, 實數, 字符串, 字段表. 幀的字段會編排的很緊密, 而不會讓解析複雜和很慢. 通過協議說明生成幀層相對來說比較簡單.
wire-level格式化被設計爲可伸縮的而且足夠通用, 可以被其他高級協議使用. 我們認爲AMQP隨着時間推移是可擴展的, 可改良的, 並且wire-level格式將會支持這一點.

2.3.2 Data Types 數據類型

AMQP數據類型在方法幀中使用, 它們如下:
1 實數(1~8字節), 用來表示長度, 數量等. 實數一般是正數, 而且可能在幀是沒有對齊的.
2 比特, 用來表示開關值, 比特包裝爲字節.
3 短字符串, 用來保存短文本屬性. 短字符串最大255個字節
4 長字符串, 用來保存二進制數據塊
5 屬性表 保存鍵值對. 值可以是字符串和數字等.

2.3.3 Protocol Negotiation 協議協商

AMQP客戶端和服務端會協商協議. 這個意思是說, 當客戶端連接的時候, 服務端會建議一些可選條件, 客戶端可以接收或者修改. 當兩端對輸出達成一致時, 連接會繼續. 協商是一個非常有用的技術, 因爲它可以讓我們判斷假設和預期.
在AMQP中, 我們可協商一些方面如下:
1 實際的協議和版本, 服務端可能在一個端口有多個協議.
2 參數加密和兩端的權限. 這屬於前面說的方法層
3 最大幀長度, 通道數量, 還有其他可選的限制
同意約定的限制可能會使雙方提前分配主要的緩存, 避免死鎖. 每一個進入的幀要麼遵守約定的限制(這樣是安全的), 要麼超過限制, 超過限制是錯誤的, 必須斷開連接. 這非常符合"要麼正常工作, 要麼不工作"的AMQP哲學.
雙方在協商中最少需要達成以下內容:
1 服務端必須告訴客戶端正在執行的限制
2 客戶端響應而且可能會減少這次連接的限制

2.3.4 Delimiting Frames 幀的分割

TCP/IP是一個流式協議, 也就是說它沒有內置的可以分割幀的機制. 現有的協議有以下幾種不同的解決方法:
1 一次連接只發送一陣. 簡單但是慢
2 在流中給幀加分隔符. 簡單但是解析慢
3 計算幀的長度, 並在發送的時候在每個幀的開始標記長度. 這樣簡單而且快速, 我們選擇這種

2.3.5 Frame Details 幀的細節

所有的幀都包括一個7字節的頭, 任意字節的負載, 和一個字節的幀尾.
在這裏插入圖片描述

爲了讀取幀, 我們需要:
1 讀頭信息, 檢查幀類型和通道
2 依靠幀類型, 我們讀負載並執行
3 最後讀幀尾
在考慮性能的實際使用場景中, 我們會使用"預讀緩衝" 或者"收集讀"來避免多個獨立系統讀取一個幀的情況.

2.3.5.1 Method Frames 方法幀

方法幀用來承載高級協議的命令. 一個方法幀表示一個命令. 方法幀的負載如下:在這裏插入圖片描述
執行一個方法幀, 我們需要:
1 讀取方法幀的負載
2 拆箱爲一個結構體. 一個給定的方法通常都有同樣的結構, 所以我們可以快速的拆箱.
3 檢查在當前上下文中是否允許使用該方法
4 檢查方法參數是否有效
5 執行方法
方法幀主體被夠潮成一個AMQP數據列表. 編碼直接由協議生成, 所以特別快

2.3.5.2 Content Frames 內容幀

內容是客戶端通過AMQP服務器發送給另一個客戶端的應用數據. 內容大概就是一系列屬性加上一個二進制數據塊. 屬性由基礎類定義, 這些組成了內容的頭信息幀. 數據部分可以是任意大小的. 可能超過了多個數據塊, 每一個都是內容主體幀.
觀察一個通道, 我們可能會看到如下內容:
在這裏插入圖片描述
某些方法(比如 Basic.Publish)正式定義爲會攜帶內容的方法. 當一個終端發送這樣一個方法幀, 那麼經常會在後面攜帶一個內容頭和零個或多個內容體.
一個內容頭格式如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5lka5nfY-1578713442138)(evernotecid://1E064798-B141-466B-8039-48A0F117FF38/appyinxiangcom/26054172/ENResource/p399)]
我們把內容體放到不一樣的幀中(而不是放到方法中), 以便AMQP可以支持零複製技術, 在該技術中內容從不會被編碼. 我們把內容屬性放在他們自己的幀中, 這樣接收者就可以選擇丟棄他們不需要的內容.

2.3.5.3 Heartbeat Frames 心跳幀

心跳是一種被設計來撤銷TCP/IP特性的技術, 也就是說它可以將關閉了一段時間的斷連接恢復連接. 在某些場景下, 我們需要迅速知道某一個端點是不是離線了, 或者其他不能響應的原因(比如死循環). 因爲心跳可以在一個比較低的層級來實現, 所以我們在傳輸層以一種特殊類型的幀來實現, 而不是一個類方法.

2.3.6 Error Handling 異常處理

AMQP使用異常來處理錯誤. 任何可以操作錯誤(比如消息隊列找不到, 沒有權限等)最後都會是一個通道異常. 任何結構錯誤(無效參數, 方法序列化錯誤)都是一個連接異常. 異常會關閉通道或者連接, 然後返回一個代碼和文按告知客戶端應用. 我們使用三位數的應答碼, 再加上文字, 就像HTTP協議和其他協議做的那樣.

2.3.7 Closing Channels and Connections 關閉通道和連接

一個連接或者通道在客戶端發送"Open", 服務端發送"Open-Ok"後會認爲是打開的. 從這點來看, 一端想要關閉連接必須使用握手協議.
無論是正常還是由於異常關閉連接都需要特別小心. 突然地關閉一般不會被馬上感知到, 而且後帶來後續異常, 我們可能丟失錯誤響應碼. 正確的做法是在所有關閉的時候都使用握手協議, 以便我們在關閉的時候可以確保其他客戶端都已經知道是怎麼回事了.
當一端決定關閉連接時, 它會發送一個關閉方法. 接收端必須響應一個"可以關閉"的信息, 然後雙方可以關閉自己的連接. 注意, 如果某一端忽略了關閉, 當兩端同時發送關閉可能會導致死鎖.

2.4 AMQP Client Architecture AMQP客戶端結構

我們可以直接從應用讀寫AMQP幀, 但是這肯定是一個糟糕的設計. 就算是最簡單的AMQP通信也要比HTTP複雜, 應用開發人員爲了發送信息給消息隊列不需要知道二進制幀的結構. AMQP客戶端結構建議由如下幾個方面:
1 結構層. 在這一層以某種語言的結構獲取AMQP協議方法, 然後序列化他們爲wire-level結構. 該層可以直接通過AMQP說明構造(這是一個由XML實現的協議建模語言, 專門爲了AMQP設計).
2 連接管理層. 這一層讀寫AMQP幀, 管理全部的連接和會話邏輯. 在這一層, 我們可以封裝打開連接和會話, 錯誤處理, 內容的傳輸等邏輯. 這一層的大部分可以由AMQP自動創建. 例如,規範定義了哪些方法攜帶內容,因此可以機械地生成邏輯“發送方法,然後可選地發送內容”
3 API層. 這一層暴露應用使用的API. 這一層可能會反應一些存在的標準, 或者一些高級的AMQP方法, 在這一部分需要我們做好映射關係. AMQP方法的設計使得這個映射既簡單又有效. API層可能會分爲多層, 比如創建一個更高級的API層.
另外, 通常還有I/O層, 可以很簡單, 也可以很複雜. 下圖展示了建議的結構圖
在這裏插入圖片描述
在本文中, 當我們說"客戶端API"時, 我們指的是應用層之下的. 我們會使用"客戶端API"和"應用"來指兩個事, "應用"使用"客戶端API"和中間件服務端通信.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章