Java中用Aeron實現UDP訊息傳遞

banq發表於2024-06-06

在本文中,我們將介紹Aeron,這是一個由 Adaptive Financial Consulting 維護的多語言庫,旨在實現應用程式之間的高效 UDP 訊息傳遞。它專為提高效能而設計,旨在實現高吞吐量、低延遲和容錯。

在使用 Aeron 之前,我們需要在我們的構建中包含最新版本,在撰寫本文時版本為1.44.1 。

如果我們使用 Maven,我們可以在pom.xml中包含它的依賴項:

<dependency>
    <groupId>io.aeron</groupId>
    <artifactId>aeron-all</artifactId>
    <version>1.44.1</version>
</dependency>

或者如果我們使用 Gradle,我們可以將其包含在build.gradle中:

implementation("io.aeron:aeron-all:1.44.1")
此時,我們已準備好開始在我們的應用程式中使用它。

請注意,目前 Aeron 的某些部分無法與 Java 16 或更新版本相容。這是由於JPMS 阻止了某些互動。

媒體驅動程式
Aeron 在應用程式和傳輸之間採用間接方式工作。這被稱為媒體驅動程式,因為它是我們的應用程式和傳輸媒體之間的互動。

每個 Aeron 程序都與一個媒體驅動程式互動,並透過該驅動程式與其他程序互動 - 無論是在同一臺機器上還是遠端。它透過檔案系統執行此互動。我們需要將媒體驅動程式和所有應用程式指向磁碟上的同一目錄,該目錄儲存了各個方面。請注意,我們只能同時為任何給定目錄執行一個媒體驅動程式。嘗試執行多個媒體驅動程式將失敗。

當我們想要簡單的時候,我們可以執行應用程式中嵌入的媒體驅動程式:

MediaDriver mediaDriver = MediaDriver.launch();

這將啟動具有所有預設設定的媒體驅動程式。具體來說,這將使用預設媒體驅動程式目錄執行。

我們還有另一種專為嵌入式使用而設計的啟動方法。它的作用與以前完全相同,只是它會生成一個隨機目錄,以確保同一臺機器上的多個例項不會發生衝突:

MediaDriver mediaDriver = MediaDriver.launchEmbedded();

在這兩種情況下,我們還可以提供MediaDriver.Context物件來進一步配置媒體驅動程式:

MediaDriver.Context context = new MediaDriver.Context();
context.threadingMode(ThreadingMode.SHARED);
MediaDriver mediaDriver = MediaDriver.launch(context);

執行此操作時,我們需要在使用完媒體驅動程式後將其關閉。該介面實現了AutoCloseable,因此我們可以使用try-with-resources 模式來管理此操作。

或者,我們可以將媒體驅動程式作為外部應用程式執行。我們可以使用作為依賴項包含的aeron-all.jar JAR 檔案來執行此操作:

$ java -cp aeron-all-1.44.1.jar io.aeron.driver.MediaDriver

其功能與上面的MediaDriver.launch()完全相同。

Aeron API 客戶端
我們透過Aeron類使用 Aeron 執行所有 API 互動。我們需要建立一個新的例項並將其指向我們的媒體驅動程式。只需建立一個新例項即可指向預設位置的媒體驅動程式 - 就像我們使用MediaDriver.launch()啟動它一樣:

Aeron aeron = Aeron.connect();

或者,我們可以提供一個Aeron.Context物件來配置連線,包括指定媒體驅動程式正在執行的目錄:

Aeron.Context ctx = new Aeron.Context();
ctx.aeronDirectoryName(mediaDriver.aeronDirectoryName());
Aeron aeron = Aeron.connect(ctx);

如果我們的媒體驅動程式位於非標準目錄中,包括我們使用MediaDriver.launchEmbedded() 啟動它,那麼我們必須這樣做。如果我們指向的目錄沒有正在執行的媒體驅動程式,Aeron.connect ()呼叫將阻塞,直到有正在執行的媒體驅動程式為止。

我們可以將任意數量的 Aeron 客戶端連線到同一個媒體驅動程式。通常,這些客戶端來自不同的應用程式,但如果需要,它們也可以來自同一個應用程式。但是,如果我們這樣做,那麼我們還需要使用Aeron.Context的新例項:

Aeron.Context ctx1 = new Aeron.Context();
ctx1.aeronDirectoryName(mediaDriver.aeronDirectoryName());
aeron1 = Aeron.connect(ctx1);
System.out.println(<font>"Aeron 1 connected: " + aeron1);
Aeron.Context ctx2 = new Aeron.Context();
ctx2.aeronDirectoryName(mediaDriver.aeronDirectoryName());
aeron2 = Aeron.connect(ctx2);
System.out.println(
"Aeron 2 connected: " + aeron2);

與MediaDriver一樣,Aeron例項也是AutoCloseable 的。這意味著我們可以用 try-with-resources 模式包裝它,以確保我們正確關閉它。

傳送和接收訊息
現在我們已經有了 Aeron API 客戶端,我們可以使用它來傳送和接收訊息。

緩衝區
Aeron 將所有訊息(包括髮送和接收)表示為DirectBuffer例項。歸根結底,這些不過是一組位元組,但它們為我們提供了一組方法來處理一組標準型別。

當我們傳送訊息時,我們需要根據自己的資料自行構建緩衝區。為此,我們最好使用UnsafeBuffer例項 - 之所以這樣命名是因為它使用sun.misc.Unsafe來讀取和寫入底層緩衝區的值。建立它需要一個位元組陣列或ByteBuffer例項,然後我們可以使用BufferUtil.allocateDirectAligned()來幫助最有效地完成此操作:

UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(256, 64));

一旦我們獲得了緩衝區,我們就可以使用一系列getXyz()和putXyz()方法來操作緩衝區中的資料:

<font>// Put a string into the buffer starting at index 0.<i>
int length = buffer.putStringWithoutLengthUtf8(0, message); 
// Read a string of the given length from the buffer starting from the given offset.<i>
String message = buffer.getStringWithoutLengthUtf8(offset, length); 

請注意,我們需要自己管理緩衝區中的偏移量。每當我們將資料放入緩衝區時,它都會返回寫入資料的長度,以便我們計算下一個偏移量。當我們從緩衝區讀取時,我們需要知道長度是多少。

頻道和流
使用透過特定通道傳輸的已識別流,可以使用 Aeron 傳送和接收資料。

我們將通道指定為特定格式的 URI,告訴 Aeron 如何傳輸訊息。然後,我們的媒體驅動程式使用它與傳輸媒體進行互動,確保它正確傳送和接收訊息。流僅以數字標識。唯一的要求是同一通訊的兩端使用相同的流 ID。

最簡單的此類通道是aeron:ipc,它使用媒體驅動程式中的共享記憶體進行傳輸和接收。請注意,這僅在雙方使用相同的媒體驅動程式且不允許聯網時才有效。

更有用的是,我們可以使用aeron:udp使用UDP傳送和接收。這使我們能夠與我們可以連線的任何地方的任何其他應用程式進行通訊。特別是,我們的應用程式將與媒體驅動程式通訊,然後媒體驅動程式將相互通訊。

指定 UDP 通道時,我們至少需要包含主機和埠。在接收端,我們將在此監聽;在傳送端,我們將在此傳送訊息。例如,aeron:udp?endpoint=localhost:20121將透過localhost:20121上的 UDP 傳送和接收訊息。

訂閱
一旦設定好媒體驅動程式和 Aeron 客戶端,我們就可以接收訊息了。我們透過建立對特定頻道上特定流的訂閱,然後輪詢該流以獲取訊息來實現這一點。

新增訂閱足以讓媒體驅動程式設定一切以便能夠接收我們的訊息。我們使用Aeron例項上的addSubscription()方法執行此操作:

Subscription subscription = aeron.addSubscription(<font>"aeron:udp?endpoint=localhost:20121", 1001);

與之前一樣,當不再使用它時,我們需要關閉它,以便媒體驅動程式知道停止監聽訊息。與往常一樣,它是AutoCloseable,因此我們可以使用 try-with-resources 來管理它。

當我們訂閱時,我們需要接收訊息。Aeron 使用輪詢機制執行此操作,讓我們完全控制它何時處理訊息。要輪詢訊息,我們需要提供一個FragmentHandler來處理收到的訊息。如果我們想將所有程式碼內聯,我們可以使用 lambda 來實現它;如果我們想重用它,我們可以使用單獨的類來實現介面:

FragmentHandler fragmentHandler = (buffer, offset, length, header) -> {
    String data = buffer.getStringWithoutLengthUtf8(offset, length);
    System.out.printf(<font>"Message from session %d (%d@%d) <<%s>>%n",
            header.sessionId(), length, offset, data);
};

Aeron 使用緩衝區、資料起始偏移量以及接收資料的長度來呼叫此方法。然後,我們可以根據需要根據應用程式需要處理此緩衝區。

當我們準備輪詢新訊息時,我們使用 Subscription.poll ()方法:

int fragmentsRead = subscription.poll(fragmentHandler, 10);

這裡,我們提供了FragmentHandler例項以及嘗試接收單個訊息時要考慮的訊息片段數量。請注意,即使媒體驅動程式中有許多訊息可用,我們一次也最多隻能接收一條訊息。但是,如果沒有訊息可用,這將立即返回,如果收到的訊息太大,我們可能只會收到其中的一部分。

Publication
訊息傳遞的另一面是傳送訊息。我們使用Publication來實現這一點,它可以將訊息傳送到特定通道上的特定流。

我們可以使用Aeron.addPublication()方法新增新發布。然後我們需要等待它連線,這要求接收端有一個訂閱準備好接收訊息:

ConcurrentPublication publication = aeron.addPublication(<font>"aeron:udp?endpoint=localhost:20121", 1001);
while (!publication.isConnected()) {
    TimeUnit.MILLISECONDS.sleep(100);
}

如果沒有連線,它將立即無法傳送訊息,而不是等待有人新增訂閱。

與之前一樣,當不再使用它時,我們需要將其關閉,以便媒體驅動程式可以釋放任何分配的資源。與往常一樣,這是AutoCloseable,因此我們可以使用 try-with-resources 來管理它。

一旦我們有了連線的釋出,我們就可以向其提供訊息。這些訊息始終以填充緩衝區的形式提供,然後傳送給連線的訂閱者:

UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(256, 64));
buffer.putStringWithoutLengthUtf8(0, message);
long result = publication.offer(buffer, 0, message.length());

如果訊息已傳送,我們將返回一個表示已傳輸位元組數的值,如果緩衝區太大,該值可能小於我們預期傳送的位元組數。或者,它可能會向我們返回一組錯誤程式碼之一,所有錯誤程式碼都是負數,因此很容易與成功情況區分開來:
  • Publication.NOT_CONNECTED – 釋出未連線到訂閱者。
  • Publication.BACK_PRESSURED – 來自訂閱者的背壓意味著我們現在無法再傳送任何訊息。
  • Publication.ADMIN_ACTION – 某些管理操作(例如日誌輪換)導致傳送失敗。在這種情況下,立即重試通常是安全的。
  • Publication.CLOSED – Publication例項已關閉。
  • Publication.MAX_POSITION_EXCEEDED – 媒體驅動程式中的緩衝區已滿。通常,我們可以透過關閉出版物並建立新出版物來解決這個問題。

相關文章