簡單二進制編碼指南
一、簡介
效率和性能是現代數據服務的兩個重要方面,尤其是當我們流式傳輸大量數據時。當然,使用高性能編碼減少消息大小是實現它的關鍵。
然而,內部編碼/解碼算法可能既麻煩又脆弱,這使得它們難以長期維護。
幸運的是,簡單二進制編碼可以幫助我們以實用的方式實現和維護量身定制的編碼/解碼系統。
在本教程中,我們將討論簡單二進制編碼 (SBE) 的用途以及如何將其與代碼示例一起使用。
2. 什麼是 SBE?
SBE 是用於編碼/解碼消息以支持低延遲流的二進製表示。它也是FIX SBE標準的參考實現,該標準是財務數據編碼的標準。
2.1。消息結構
為了保持流語義,消息必須能夠被順序讀取或寫入,沒有回溯。這消除了額外的操作——比如取消引用、處理位置指針、管理額外的狀態等——並更好地利用硬件支持來保持最大的性能和效率。
讓我們看一下 SBE 中消息的結構:
- 標頭:它包含必填字段,例如消息的版本。必要時還可以包含更多字段。
- 根字段:消息的靜態字段。它們的塊大小是預定義的,不能更改。它們也可以定義為可選的。
- 重複組:這些代表集合類型的表示。組可以包含字段,也可以包含內部組,以便能夠表示更複雜的結構。
- 可變數據字段:這些是我們無法提前確定其大小的字段。字符串和 Blob 數據類型是兩個示例。他們將在消息的末尾。
接下來,我們將了解為什麼這個消息結構很重要。
2.2. SBE(不)什麼時候有用?
SBE 的強大源於其消息結構。它針對數據的順序訪問進行了優化。因此, SBE 非常適合固定大小的數據,例如數字、位集、枚舉和數組。
SBE 的一個常見用例是金融數據流——主要包含數字和枚舉——SBE 是專門為其設計的。
另一方面, SBE 不太適合像 string 和 blob 這樣的可變長度數據類型。原因是我們很可能不知道未來的確切數據大小。因此,這最終將在流式傳輸時進行額外的計算,以檢測消息中數據的邊界。毫不奇怪,如果我們談論毫秒延遲,這可能會影響我們的業務。
儘管 SBE 仍支持 String 和 Blob 數據類型,但它們始終放在消息的末尾,以將可變長度計算的影響降至最低。
3. 建立圖書館
要使用 SBE 庫,讓我們將以下 Maven依賴項添加到我們的pom.xml
文件中:
<dependency>
<groupId>uk.co.real-logic</groupId>
<artifactId>sbe-all</artifactId>
<version>1.27.0</version>
</dependency>
4. 生成 Java 存根
在我們生成我們的 Java 存根之前,很明顯,我們需要形成我們的消息模式。 SBE 提供了通過 XML 定義我們的模式的能力。
接下來,我們將看到如何為我們的消息定義一個模式,它傳輸示例市場交易數據。
4.1。創建消息模式
我們的模式將是一個基於 FIX 協議的特殊 XSD 的 XML 文件。它將定義我們的消息格式。
所以,讓我們創建我們的模式文件:
<?xml version="1.0" encoding="UTF-8"?>
<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
package="com.baeldung.sbe.stub" id="1" version="0" semanticVersion="5.2"
description="A schema represents stock market data.">
<types>
<composite name="messageHeader"
description="Message identifiers and length of message root.">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
<enum name="Market" encodingType="uint8">
<validValue name="NYSE" description="New York Stock Exchange">0</validValue>
<validValue name="NASDAQ"
description="National Association of Securities Dealers Automated Quotations">1</validValue>
</enum>
<type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII"
description="Stock symbol"/>
<composite name="Decimal">
<type name="mantissa" primitiveType="uint64" minValue="0"/>
<type name="exponent" primitiveType="int8"/>
</composite>
<enum name="Currency" encodingType="uint8">
<validValue name="USD" description="US Dollar">0</validValue>
<validValue name="EUR" description="Euro">1</validValue>
</enum>
<composite name="Quote"
description="A quote represents the price of a stock in a market">
<ref name="market" type="Market"/>
<ref name="symbol" type="Symbol"/>
<ref name="price" type="Decimal"/>
<ref name="currency" type="Currency"/>
</composite>
</types>
<sbe:message name="TradeData" id="1" description="Represents a quote and amount of trade">
<field name="quote" id="1" type="Quote"/>
<field name="amount" id="2" type="uint16"/>
</sbe:message>
</sbe:messageSchema>
如果我們詳細查看模式,我們會注意到它有兩個主要部分, <types>
和<sbe:message>
。我們將首先開始定義<types>
。
作為我們的第一種類型,我們創建messageHeader
。它是強制性的,並且還有四個必填字段:
<composite name="messageHeader" description="Message identifiers and length of message root.">
<type name="blockLength" primitiveType="uint16"/>
<type name="templateId" primitiveType="uint16"/>
<type name="schemaId" primitiveType="uint16"/>
<type name="version" primitiveType="uint16"/>
</composite>
-
blockLength
:表示為消息中的根字段保留的總空間。它不計算重複字段或可變長度字段,如字符串和 blob。 -
templateId
:消息模板的標識符。 -
schemaId
:消息模式的標識符。架構總是包含一個模板。 -
version
:我們定義消息時消息模式的版本。
接下來,我們定義一個枚舉Market
:
<enum name="Market" encodingType="uint8">
<validValue name="NYSE" description="New York Stock Exchange">0</validValue>
<validValue name="NASDAQ"
description="National Association of Securities Dealers Automated Quotations">1</validValue>
</enum>
我們的目標是保存一些眾所周知的交換名稱,我們可以在模式文件中進行硬編碼。它們不會經常變化或增加。因此,類型 < enum>
非常適合這裡。
通過設置encodingType=”uint8″,
我們保留 8 位空間用於在單個消息中存儲市場名稱。這使我們能夠支持2^8 = 256
個不同的市場(0 到 255)——一個無符號 8 位整數的大小。
緊接著,我們定義了另一種類型Symbol
。這將是一個 3 或 4 個字符的字符串,用於標識 AAPL (Apple)、MSFT (Microsoft) 等金融工具:
<type name="Symbol" primitiveType="char" length="4" characterEncoding="ASCII" description="Instrument symbol"/>
正如我們所看到的,我們使用characterEncoding=”ASCII”
來限製字符——7 位,最多 128 個字符——我們設置一個length=”4″
的上限以不允許超過 4 個字符。因此,我們可以盡可能地減小尺寸。
之後,我們需要價格數據的複合類型。因此,我們創建了Decimal
類型:
<composite name="Decimal">
<type name="mantissa" primitiveType="uint64" minValue="0"/>
<type name="exponent" primitiveType="int8"/>
</composite>
Decimal
由兩種類型組成:
-
mantissa
:十進制數的有效數字 -
exponent
: 十進制數的小數位數
例如,值mantissa=98765
和exponent=-3
表示數字 98.765。
接下來,與Market
非常相似,我們創建另一個<enum>
來表示Currency
,其值映射為uint8
:
<enum name="Currency" encodingType="uint8">
<validValue name="USD" description="US Dollar">0</validValue>
<validValue name="EUR" description="Euro">1</validValue>
</enum>
最後,我們通過組合我們之前創建的其他類型來定義Quote
:
<composite name="Quote" description="A quote represents the price of an instrument in a market">
<ref name="market" type="Market"/>
<ref name="symbol" type="Symbol"/>
<ref name="price" type="Decimal"/>
<ref name="currency" type="Currency"/>
</composite>
最後,我們完成了類型定義。
但是,我們仍然需要定義一個消息。所以,讓我們定義我們的消息TradeData
:
<sbe:message name="TradeData" id="1" description="Represents a quote and amount of trade">
<field name="quote" id="1" type="Quote"/>
<field name="amount" id="2" type="uint16"/>
</sbe:message>
當然,在類型方面,我們可以從規範中找到更多的細節。
在接下來的兩節中,我們將討論如何使用我們的模式來生成我們最終用來編碼/解碼消息的 Java 代碼。
4.2.使用SbeTool
生成 Java 存根的一種直接方法是使用 SBE jar 文件。這會自動運行實用程序類SbeTool
:
java -jar -Dsbe.output.dir=target/generated-sources/java
<local-maven-directory>/repository/uk/co/real-logic/sbe-all/1.26.0/sbe-all-1.26.0.jar
src/main/resources/schema.xml
請注意,我們必須將佔位符<local-maven-directory>
與我們的本地 Maven 路徑一起調整才能運行該命令。
成功生成後,我們將在target/generated-sources/java
文件夾中看到生成的 Java 代碼。
4.3.將SbeTool
與 Maven 一起使用
使用SbeTool
很簡單,但我們甚至可以通過將其集成到 Maven 中使其更加實用。
因此,讓我們將以下 Maven 插件添加到我們的pom.xml
中:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<includeProjectDependencies>false</includeProjectDependencies>
<includePluginDependencies>true</includePluginDependencies>
<mainClass>uk.co.real_logic.sbe.SbeTool</mainClass>
<systemProperties>
<systemProperty>
<key>sbe.output.dir</key>
<value>${project.build.directory}/generated-sources/java</value>
</systemProperty>
</systemProperties>
<arguments>
<argument>${project.basedir}/src/main/resources/schema.xml</argument>
</arguments>
<workingDirectory>${project.build.directory}/generated-sources/java</workingDirectory>
</configuration>
<dependencies>
<dependency>
<groupId>uk.co.real-logic</groupId>
<artifactId>sbe-tool</artifactId>
<version>1.27.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/java/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
因此,一個典型的 Maven clean install
命令會自動生成我們的 Java 存根。
此外,我們可以隨時查看SBE 的 Maven 文檔以獲取更多配置選項。
5. 基本消息
當我們準備好 Java 存根後,讓我們看看我們如何使用它們.
首先,我們需要一些數據進行測試。因此,我們創建了一個類MarketData
:
public class MarketData {
private int amount;
private double price;
private Market market;
private Currency currency;
private String symbol;
// Constructor, getters and setters
}
我們應該注意到,我們的MarketData
構成了 SBE 為我們生成的Market
和Currency
類。
接下來,讓我們定義一個MarketData
對象,以便稍後在我們的單元測試中使用:
private MarketData marketData;
@BeforeEach
public void setup() {
marketData = new MarketData(2, 128.99, Market.NYSE, Currency.USD, "IBM");
}
由於我們已經準備好MarketData
,我們將在下一節中了解如何將其寫入 TradeData 並將其讀入我們的TradeData
。
5.1。寫信息
大多數情況下,我們希望將數據寫入ByteBuffer
,因此我們在生成的編碼器MessageHeaderEncoder
和TradeDataEncoder
旁邊創建一個具有初始容量的ByteBuffer
:
@Test
public void givenMarketData_whenEncode_thenDecodedValuesMatch() {
// our buffer to write encoded data, initial cap. 128 bytes
UnsafeBuffer buffer = new UnsafeBuffer(ByteBuffer.allocate(128));
MessageHeaderEncoder headerEncoder = new MessageHeaderEncoder();
TradeDataEncoder dataEncoder = new TradeDataEncoder();
// we'll write the rest of the code here
}
在寫入數據之前,我們需要將我們的價格數據解析為兩部分,尾數和指數:
BigDecimal priceDecimal = BigDecimal.valueOf(marketData.getPrice());
int priceMantissa = priceDecimal.scaleByPowerOfTen(priceDecimal.scale()).intValue();
int priceExponent = priceDecimal.scale() * -1;
我們應該注意到我們使用BigDecimal
進行此轉換。在處理貨幣值時使用BigDecimal
始終是一個好習慣,因為我們不想失去精度。
最後,讓我們編碼並編寫我們的TradeData
:
TradeDataEncoder encoder = dataEncoder.wrapAndApplyHeader(buffer, 0, headerEncoder);
encoder.amount(marketData.getAmount());
encoder.quote()
.market(marketData.getMarket())
.currency(marketData.getCurrency())
.symbol(marketData.getSymbol())
.price()
.mantissa(priceMantissa)
.exponent((byte) priceExponent);
5.2.閱讀信息
要讀取消息,我們將使用寫入數據的同一個緩衝區實例。但是,這次我們需要解碼器MessageHeaderDecoder
和TradeDataDecoder
:
MessageHeaderDecoder headerDecoder = new MessageHeaderDecoder();
TradeDataDecoder dataDecoder = new TradeDataDecoder();
接下來,我們解碼TradeData
:
dataDecoder.wrapAndApplyHeader(buffer, 0, headerDecoder);
同樣,我們需要從尾數和指數兩部分解碼我們的價格數據,以便將價格數據轉換為double
值。當然,我們再次使用BigDecimal
:
double price = BigDecimal.valueOf(dataDecoder.quote().price().mantissa())
.scaleByPowerOfTen(dataDecoder.quote().price().exponent())
.doubleValue();
最後,讓我們確保我們的解碼值與原始值匹配:
Assertions.assertEquals(2, dataDecoder.amount());
Assertions.assertEquals("IBM", dataDecoder.quote().symbol());
Assertions.assertEquals(Market.NYSE, dataDecoder.quote().market());
Assertions.assertEquals(Currency.USD, dataDecoder.quote().currency());
Assertions.assertEquals(128.99, price);
六,結論
在本文中,我們學習瞭如何設置 SBE、通過 XML 定義消息結構並使用它在 Java 中編碼/解碼我們的消息。
與往常一樣,我們可以在 GitHub 上找到所有代碼示例以及更多內容。