什么是序列化與反序列化?
- 序列化是指將數(shù)據(jù)結(jié)構(gòu)或?qū)ο蟀炊x的規(guī)則轉(zhuǎn)換成二進制串的過程。
- 反序列化是指將二進制串依據(jù)相同規(guī)則重新構(gòu)建成數(shù)據(jù)結(jié)構(gòu)或?qū)ο蟮倪^程。
而本質(zhì)就是一種編碼規(guī)范。
在SOME/IP中使用序列化的目的和作用?
- 使數(shù)據(jù)按照固定格式進行編排成為字節(jié)序,實現(xiàn)數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸。
7.1 說明
在AUTOSAR中是指數(shù)據(jù)在PDU中的表達(dá)形式,可以理解為來自應(yīng)用層的真實數(shù)據(jù)轉(zhuǎn)換成固定格式的字節(jié)序,以實現(xiàn)數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸。軟件組件將數(shù)據(jù)從應(yīng)用層傳遞到RTE層,在RTE層調(diào)用SOME/IP Transformer,執(zhí)行可配置的數(shù)據(jù)序列化(Serialize)或反序列化(Deserialize)。SOME/IP Serializer將結(jié)構(gòu)體形式的數(shù)據(jù)序列化為線性結(jié)構(gòu)的數(shù)據(jù);SOME/IP Deserializer將線性結(jié)構(gòu)數(shù)據(jù)再反序列化為結(jié)構(gòu)體形式數(shù)據(jù)。在服務(wù)端,數(shù)據(jù)經(jīng)過SOME/IP Serializer序列化后,被傳輸?shù)椒?wù)層的COM模塊;在客戶端,數(shù)據(jù)從COM模塊傳遞到SOME/IP Deserializer反序列化后再進入RTE層。如下圖參考Autosar Com過程。
?7.2 大小端問題
對于一個由2個字節(jié)組成的16位整數(shù),在內(nèi)存中存儲這兩個字節(jié)有兩種方法:一種是將低序字節(jié)存儲在起始地址,這稱為小端(little-endian)字節(jié)序;另一種方法是將高序字節(jié)存儲在起始地址,這稱為大端(big-endian)字節(jié)序。
假如現(xiàn)有一32位int型數(shù)0x12345678,那么其MSB(Most Significant Byte,最高有效字節(jié))為0x12,其LSB (Least Significant Byte,最低有效字節(jié))為0x78,在CPU內(nèi)存中有兩種存放方式:(假設(shè)從地址0x4000開始存放)
總結(jié):
- 大端是高字節(jié)存放到內(nèi)存的低地址
- 小端是高字節(jié)存放到內(nèi)存的高地址
假如有一個數(shù)據(jù)是0x12345678,直接用memcpy將這個數(shù)copy到下圖中的Length里面來,如果是大端的話,((uint8)Length)[0]就等于0x12;如果是小端的話,就是0x78。
因為對于賦值的方便性來講,大端是網(wǎng)絡(luò)通信中常用的方式(例如TCP/IP),所以SOME/IP格式頭也使用大端。Payload由于是用戶自主定義的內(nèi)容,所以用戶可以自己決定大小端。
7.3 內(nèi)存對齊與填充
SOME/IP協(xié)議通常使用4字節(jié)對齊方式進行數(shù)據(jù)傳輸。這意味著每個字段的長度應(yīng)該是4的倍數(shù)。這種對齊方式的主要目的是提高傳輸效率,因為在很多處理器架構(gòu)中,4字節(jié)對齊是最優(yōu)的方式,可以在內(nèi)存中更快地訪問數(shù)據(jù)。此外,使用4字節(jié)對齊方式還可以確保字段的偏移量是整數(shù),避免了在解析數(shù)據(jù)時出現(xiàn)未對齊數(shù)據(jù)的問題。
在進行SOME/IP序列化時,對于每個結(jié)構(gòu)體中的字段,需要根據(jù)其數(shù)據(jù)類型和對齊要求計算其對齊偏移量。對于大多數(shù)數(shù)據(jù)類型,SOME/IP協(xié)議都要求其對齊偏移量是4的倍數(shù)。例如,對于一個8字節(jié)的double類型字段,其對齊偏移量應(yīng)該是4的倍數(shù),即0、4、8、12等。如果字段的大小不是4的倍數(shù),那么需要在其后添加額外的填充字節(jié),以便滿足對齊要求。
在某些情況下,為了提高傳輸效率,可能需要使用不同的對齊方式。例如,在某些嵌入式系統(tǒng)中,可能需要使用2字節(jié)對齊或8字節(jié)對齊方式。在這種情況下,SOME/IP協(xié)議可以通過在消息頭中包含對齊方式信息來指定所使用的對齊方式。但是,這需要在消息頭中添加額外的信息,可能會導(dǎo)致消息大小增加,降低傳輸效率。因此,4字節(jié)對齊方式仍然是SOME/IP協(xié)議中最常用的對齊方式。(有些項目實際使用中用的是1字節(jié)對齊,即不對齊。因為1字節(jié)對齊是最簡單的對齊方式,大多編譯器很容易實現(xiàn);并且采用一字節(jié)對齊,序列化后沒有冗余數(shù)據(jù),報文的有效負(fù)載段都是有意義的數(shù)據(jù),所以總體傳輸效率得到了一定提升。)
通過在數(shù)據(jù)后插入填充元素來對齊數(shù)據(jù)的開頭,以確保對齊的數(shù)據(jù)從特定的內(nèi)存地址開始。對于有些處理器架構(gòu)可以更高效地訪問數(shù)據(jù)。
當(dāng)可變元素不是序列化數(shù)據(jù)流中最后一個元素,應(yīng)依據(jù)規(guī)則對可變元素進行位填充來實現(xiàn)數(shù)據(jù)對齊。
填充示例:
示例1.
示例2.
注:數(shù)據(jù)對齊填充應(yīng)盡量以8、16、32、64、128或256長度長度進行。
但是!
對于不同的CPU,數(shù)據(jù)的存放有不同的對齊原則,有8、16、32甚至64位對齊(可以配置)。如果一個數(shù)據(jù)是按照CPU對齊的,那么在反序列化的時候會有一定的性能優(yōu)勢。但是SOME/IP序列化的時候只支持對動態(tài)數(shù)據(jù)類型自動添加填充位(即動態(tài)數(shù)組、動態(tài)字符串)。使用場景比較局限且序列化的時候還會消耗一些性能,還有一種場景是使用一條tcp/udp報文承載多個someip報文的時候,原本對齊的數(shù)據(jù)也可能被破壞。博主感覺比較雞肋,很多時候都默認(rèn)使用8bit對齊(也就是不對齊)。我們也舉個例子簡單講講:
假如我們設(shè)計的服務(wù)接口有兩個參數(shù),一個是uint8 arr[5],另一個是uint8 arr2[2],且假設(shè)兩個數(shù)組都是動態(tài)數(shù)組。動態(tài)數(shù)組都是要加長度域的,以表示后面的數(shù)組的字節(jié)數(shù),假設(shè)arr使用2bytes的長度域,arr2使用1byte的字節(jié)域。當(dāng)前CPU是4字節(jié)對齊,那么序列化完arr的5個數(shù)據(jù)后,就不能立即序列化arr2。因為arr的長度域+數(shù)據(jù)域一共7bytes,不是4的整數(shù)倍,要填充1byte。而后面的arr2由于是該someip報文的所有元素的末尾元素,雖然其也是動態(tài)數(shù)組,但是不用填充(因為后面沒有數(shù)據(jù)了,不會影響后面數(shù)據(jù)的反序列化性能)。
7.4 都有哪些數(shù)據(jù)類型
拿C語言舉例,能用到的數(shù)據(jù)類型有:
- 基礎(chǔ)數(shù)據(jù)類型:就是C語言中的保留字能直接使用的類型及其重命名類型。如uint8,short,long和float64等
- 復(fù)雜數(shù)據(jù)類型:就是C語言中需要通過基礎(chǔ)數(shù)據(jù)類型進行組合的新類型。如struct,union,array和string等
需要強調(diào)的一點是:string和動態(tài)array這樣的類型在C語言中是不存在的,但是string可以通過array模擬;動態(tài)array也可以通過struct模擬。在CP協(xié)議中,可以識別這些模擬出來的類型,并序列化成string和動態(tài)array。下面列舉一下someip所支持的所有可序列化的數(shù)據(jù)類型。
需要注意的是:
- someip不支持指針的直接序列化,因為沒有任何意義,通信雙方的內(nèi)容地址和存放的數(shù)據(jù)都是不同的,直接傳地址是去不到對應(yīng)數(shù)據(jù)的。
- someip支持使用TLV(Tag Length Value)格式傳輸數(shù)據(jù),需要配置打開,后續(xù)會有一章專門講解。
7.5 基礎(chǔ)數(shù)據(jù)類型序列化(Basic Datatypes)?
本章比較簡單,就是對基礎(chǔ)數(shù)據(jù)類型的序列化規(guī)則,下面詳細(xì)列舉了所有someip支持傳輸?shù)幕A(chǔ)數(shù)據(jù)類型,看懂下面的表就可以了。
還有一點是,大于8bit的數(shù)據(jù)都有大小端問題,7.1章也講過,Payload里的數(shù)據(jù)是支持用戶配置大小端的。所以只要通信雙方協(xié)商一致,就沒有問題。
7.6 序列化:結(jié)構(gòu)體(Struct)?
上一章講解的基礎(chǔ)數(shù)據(jù)類型的序列化說簡單一點就是挨個存放到buffer里,而負(fù)責(zé)數(shù)據(jù)類型的序列化是有一定規(guī)則的,下面要講解的便是這些規(guī)則。
7.6.1 結(jié)構(gòu)體序列化
結(jié)構(gòu)體是由其他數(shù)據(jù)類型組合成的一個新的數(shù)據(jù)類型。單論結(jié)構(gòu)體自身是沒有任何意義的,也不能攜帶數(shù)據(jù);只有結(jié)構(gòu)體里面的元素才能存放數(shù)據(jù)。所以最終將結(jié)構(gòu)體序列化后存放到buffer里玩外發(fā)的就是這些元素的值。
結(jié)構(gòu)體也可以嵌套設(shè)計(這是應(yīng)用上常用的方式,甚至可以結(jié)構(gòu)體里有數(shù)組元素,數(shù)組元素的類型又是結(jié)構(gòu)體等方式),總之c語言能定義出來的排列組合的數(shù)據(jù)類型,someip都能支持序列化。
下面是3個結(jié)構(gòu)體嵌套的舉例:
- struct1里有三個元素,最后一個元素的數(shù)據(jù)類型是struct2
- struct2里也有三個元素,最后一個元素的數(shù)據(jù)類型是struct3
- struct3以此類推
嵌套的結(jié)構(gòu)體就形成了一個樹,我們做序列化就是在做遍歷這顆樹,然后存放到buffer中。而someip序列化遍歷都是使用的深度遍歷法,即在一層結(jié)構(gòu)體的各個元素,要先進入元素內(nèi)部遍歷下一層,而不是遍歷到下一個元素。如果按照下圖來說明:
深度遍歷的順序是:aàbàefàd
廣度遍歷的順序是:aàbàdàef
還有一點是上述序列化到buffer中都是連續(xù)的,不能插入一些無用數(shù)據(jù)。
7.6.2 結(jié)構(gòu)體可選長度域
結(jié)構(gòu)體序列化的時候,還可以添加一個長度域,用來表示后面多長是這個結(jié)構(gòu)體的數(shù)據(jù)。這個length可以配置占有0/8/16/32位,且這個length里面的長度值的計算不包含length本身的長度(按byte數(shù)計算)。比如下圖中的length的值就應(yīng)該是24(6*4byte)。
7.7 序列化:字符串(String)
7.7.1 字符串序列化
為了更好的兼容所有平臺,someip規(guī)定對string的定義要基于標(biāo)準(zhǔn)的utf-8/16BE/16LE。
- utf-8:字符串的單個字符要按照8bit編碼;字符串序列化前面需要有3 bytes的BOM(byte order mark,大家可以理解為一個必要的string格式的標(biāo)識符,具體可以谷歌一下,這里就不細(xì)講了);字符串必須以\0(0x00,注意這里占用1字節(jié))結(jié)尾
- utf-16:字符串的單個字符要按照16bit編碼;字符串序列化前面需要有2 bytes的BOM(16BE和16LE的2 bytes的BOM順序剛好相反);字符串必須以\0(0x0000,占用2字節(jié))結(jié)尾
從utf-16的定義上不難看出,其字節(jié)數(shù)必定是偶數(shù)。一般我們常用的是utf-8,但是如果說要傳輸中文字符串,那必然要選擇utf-16,不過實際應(yīng)用中很少遇到。
7.7.2 靜態(tài)字符串
像c語言這樣的嵌入式語言沒有直接對string類型的定義,一般是通過數(shù)組模擬出來。特別是在嵌入式系統(tǒng)中,一般不允許動態(tài)開辟內(nèi)存,所以都會給string一個長度上限。而在序列化的時候,只會序列化到\0的地方,后面的數(shù)據(jù)就不會繼續(xù)序列化了。
還有值得一提的是,某些車廠仍然將string序列化成array的形式,這種形式不是未來安全所提倡的趨勢,someip中有開關(guān)可以控制,但是大家盡量使用我們講解的標(biāo)準(zhǔn)someip的序列化形式。
7.7.3 動態(tài)字符串
動態(tài)字符串與靜態(tài)最大的區(qū)別是在字符串頭部添加了長度域,該長度域的長度值等于后續(xù)從BOM開始到\0的長度和。動態(tài)字符串的長度域本身默認(rèn)占用4個bytes(如下圖,以utf-8為例),也可以配置成1,2個bytes。在反序列化的時候會校驗length的長度和解析到\0的長度是否一致,如果有偏差會報錯。
實際上動態(tài)字符串和靜態(tài)字符串都可以是動態(tài)的,因為靜態(tài)字符串是按照\0之前的數(shù)據(jù)序列化,長度也是動態(tài)的。只是它們兩個在c語言中的類型定義有所不同:
- 靜態(tài)string是一個char數(shù)組
- 動態(tài)string是一個struct,struct里有兩個元素,一個是指示后面有多少個string元素的size indicator,一個是char數(shù)組
需要注意的一點是這里的sizeIndicator是指的后面的string中有多少個元素(如果是utf-16,等于字節(jié)數(shù)除以2);而最終序列化到buffer中,是指的序列化后的長度,是后面的字節(jié)數(shù)。比如同樣是?“HelloWorld”?這句話,那么:
- utf-8:
sizeIndicator = 11(HelloWorld共10個字符,加上\0,共11個元素)
length = 14(上述11+3bytes的BOM)
- utf-16:
sizeIndicator = 11(HelloWorld共10個字符,加上\0,共11個元素)
length = 24(上述11*2+2bytes的BOM)
?7.8 序列化:數(shù)組(Arrays)
數(shù)組和字符串很類似,都是分為靜態(tài)和動態(tài)兩類,但是在序列化的方式上又有所區(qū)別。
7.8.1 靜態(tài)長度數(shù)組
靜態(tài)數(shù)組的長度是固定的,不像靜態(tài)字符串那樣通過\0來實現(xiàn)變長。且靜態(tài)數(shù)組可以像動態(tài)字符串那樣通過配置添加一個長度域(也可以沒有)。
我們舉一個2維數(shù)組的例子看看實際序列化后的樣子:
可以看到2維數(shù)組有兩類不同的長度域,一種是第1維度的length_all,長度為后續(xù)所有數(shù)據(jù)的字節(jié)數(shù)總和16;另一種是第2維度的legnth_0/1,分別代表arr[0]/[1]的長度,都是4。如果后續(xù)還有3維,或者更高維度的數(shù)組,以此類推即可。
7.8.2 動態(tài)長度數(shù)組
動態(tài)長度數(shù)組和靜態(tài)的區(qū)別也是在其數(shù)據(jù)類型的定義上(和string類似),sizeIndicator也是指的元素,length是指長度,這里不再贅述。動態(tài)長度數(shù)組必須要有length域,否則就是靜態(tài)數(shù)組;someip會按照sizeIndicator的大小進行序列化,sizeIndicator沒有覆蓋的數(shù)據(jù)不會被傳輸。序列化后的layout也上面靜態(tài)數(shù)組中的圖長的一樣。
當(dāng)然,以上僅僅是舉例,數(shù)組的元素也可以不是基礎(chǔ)數(shù)據(jù)類型,是一些結(jié)構(gòu)體,或者string之類的都行。
7.9 序列化:聯(lián)合體(Union/Variant)
聯(lián)合體可以說是我們使用最少的類型了,甚至有的車廠直接靜止使用聯(lián)合體,以確保someip通信報文的易讀性;同時有些c語言規(guī)范也不提倡使用聯(lián)合體,容易在使用中出問題,所以可能是我們使用中最不常見的一種形式 聯(lián)合體是說將一塊內(nèi)存的數(shù)據(jù)可以解析成不同的類型,以方便調(diào)用,而序列化的時候,只能選定其中一種類型進行傳輸。比如我們有如下的union定義:
我們選定按照b類型傳輸,那么最終的layout如圖:
我們分為3段來看:
- length 代表這個union序列化后的長度,這里包括后面的type+數(shù)據(jù)+填充的長度。這個長度域也可以配置占用字節(jié)數(shù)(0,1,2,4bytes;其中0就是不要有長度域)
- type 代表要將這個聯(lián)合體以哪種類型進行序列話,比如我們選擇上面的uint16,那么對應(yīng)的類型是2(0保留為NULL的含義,從1開始算,uint16對應(yīng)type==2)
- 然后就是uint16 類型的b這個數(shù)據(jù)本身
- 最后為了之前講過的對齊原因,這里填充了2bytes
7.10 序列化:TLV簡述(Tag Length Value)
7.10.1 什么是TLV
TLV是Tag Length Value的簡稱,是someip序列化的一種格式,會有部分車廠在使用,但并不是主流。我們簡單講解一下,讓大家知道這個干什么的,不對細(xì)節(jié)做進一步分析。與之前講過的所有的類型的序列化格式有所區(qū)別,tlv還會再加一層標(biāo)簽,對每個數(shù)據(jù)進行單獨標(biāo)識,方便管理。標(biāo)簽可以在兩個地方加:
- someip服務(wù)接口參數(shù)
- 某參數(shù)定義的struct里的成員
7.10.2 TLV格式
從下圖中可以看到,如果光看藍(lán)色部分,就是和前面章節(jié)中講解的序列化方式是一樣的,而這個主要加了2 bytes的tag頭。一個someip報文里可以有很多這樣的數(shù)據(jù)??梢钥闯?,tag+length+數(shù)據(jù)的形式就在字節(jié)流中將每個數(shù)據(jù)劃分成了一段,形成了一個鏈表,如果不想要該tag的數(shù)據(jù),就可以通過length直接跳過去,甚至不用再反序列化(注意的一點是,這里length無論是靜態(tài)動態(tài)數(shù)據(jù)類型,只要不是基礎(chǔ)數(shù)據(jù)類型都必須要有長度域,以便能通過長度域找到下一個數(shù)據(jù))。
再說一下上面圖中的tag頭,主要是3部分組成,1bit的res保留,3bits的wire type,和12bits的data ID:
- wire type:?wire type用來指示后續(xù)的長度域占用幾個bytes,如果是基礎(chǔ)數(shù)據(jù)類型,沒有長度域,其余類型都必須有長度域
文章來源:http://www.zghlxwxcb.cn/news/detail-618467.html
- data ID:?data ID對于每一層級的數(shù)據(jù)要唯一,比如有一個2維度嵌套的struct,那么每一維度都可以使用data ID = 1,但是同一維度ID不能相同。
文章來源地址http://www.zghlxwxcb.cn/news/detail-618467.html
到了這里,關(guān)于SOME/IP協(xié)議詳解[7 SOME/IP序列化]的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!