張大禹
(中國人民解放軍91550 部隊 遼寧省大連市 116000)
在數(shù)據(jù)傳輸?shù)倪^程中,數(shù)據(jù)本身的結構變化對于用戶來說是透明的,但是數(shù)據(jù)的接收端和發(fā)送端,在數(shù)據(jù)結構和存儲策略方面不可能永遠一致,因此就出現(xiàn)了稱之為“字節(jié)一致”的原則,用于實現(xiàn)數(shù)據(jù)接收端對于所接收到數(shù)據(jù)處理,尤其是當涉及傳輸位域結構相關問題的時候,這一原則的價值尤其突出,能夠幫助將將傳輸過程中的數(shù)據(jù)碎片重新組合在一起。
數(shù)據(jù)無論是在日常生活中還是在計算機傳輸和存儲領域中,都有一定的規(guī)則可以遵循,這些規(guī)則的存在讓讀者或者信息的接收端能夠合理對內容進行理解。
數(shù)據(jù)存儲涉及到字節(jié)序(byte-order)與比特序(bit-order)兩個概念,多種情況下字節(jié)會被作為數(shù)據(jù)存儲的基本單元對待,但是字節(jié)序并不是存儲順序,這二者之間還存在比特的編址問題。對于不同的存儲系統(tǒng)來說,其存儲順序也有所不同,這樣的系統(tǒng)之間想要實現(xiàn)有效的數(shù)據(jù)交換,如果涉及到按位定義的位域結構(bit-field)時,就必須將比特序納入考慮的范圍。這里需要進一步明確比特序的常規(guī)形態(tài),即如果將數(shù)的高位存儲置于高地址之上,則 稱之為小端(little-endian),反之則稱之為大端(big-endian)[1]。
在實際對于數(shù)進行書寫的時候,從左到右位次從高到低,無論系統(tǒng)是小端還是大端,這個規(guī)則都不應當有所改變,這是將二進制數(shù)的每一位與物理存儲的比特位進行對應的時候必須堅持的原則。但是需要依據(jù)大小端的定義對每個字節(jié)和比特的地址偏移狀況進行標記。除此以外,圖形表示法也是常見的方法。此種表達方式對每個數(shù)據(jù)成員進行記錄的時候,遵循在水平方向從左到右表示地址從低到高,垂直方向的高度則表示從最高位到對低位的變化。此種表達方式可以對交換數(shù)據(jù)過程中的數(shù)據(jù)成員分布以及高低位變化一目了然,這對于弄清楚數(shù)據(jù)傳輸過程中的各種細節(jié)十分重要,也有助于我們正確復原數(shù)據(jù)[2]。這種用來表示字符串的圖多呈現(xiàn)出鋸齒形狀,以字符串“SUN”為例,其鋸齒圖參見圖1。
圖1: 從小端傳輸?shù)酱蠖说淖址忼X圖
首先來對字節(jié)一致原則進行闡述。
在數(shù)據(jù)傳輸過程中,數(shù)據(jù)的發(fā)送端和接收端,具體來說就是兩側的內存,在這兩側內存中間,還有諸多的通信環(huán)節(jié),諸如通信鏈路和各類接口等等。但是不管經過多少環(huán)節(jié),都必須遵循“字節(jié)一致”的原則,這個原則讓大端和小端不同系統(tǒng)之間,在經歷了數(shù)據(jù)的傳輸之后,保持著字節(jié)的順序不變,而只是對每個字節(jié)內的比特序進行反轉,字節(jié)的值不會有所改變。具體來說,就是在數(shù)據(jù)傳輸?shù)倪^程中,不同存儲順序的系統(tǒng)之間交換數(shù)據(jù)時,堅持字節(jié)順序不變,以及字節(jié)的值不變,但是對應的,字節(jié)內的比特序會發(fā)生反轉改變[3]。
在此基礎之上進一步對數(shù)據(jù)傳輸過程進行審視。在TCP/IP 協(xié)議框架之下,普遍選用大端序展開工作,這種統(tǒng)一的標準使得所有在TCP/IP 協(xié)議框架之下傳輸數(shù)據(jù)的存儲端都可以實現(xiàn)對于協(xié)議以及傳輸結果的正確解讀。因為大端序廣泛應用在TCP/IP 協(xié)議基礎的網絡環(huán)境中,因此也會被人稱為網絡序,但是這里存在一個誤解,即TCP/IP 協(xié)議采用的網絡序只會作用于協(xié)議頭部中參數(shù)的存儲順序,而不會作用于整個傳輸?shù)臄?shù)據(jù)包。例如在struct sockaddr_in 結構之中,頭部的IP 地址和端口號都會采用大端序進行編輯,但是對于除去頭部以外的部分,也就是用戶需要傳輸?shù)臄?shù)據(jù)部分而言,TCP/IP協(xié)議并不會干涉用戶定義的數(shù)據(jù)結構,也不會過問遵從了大端還是小端規(guī)則,只會依據(jù)字節(jié)一致的原則將數(shù)據(jù)作為字節(jié)流進行傳輸,保證每個字節(jié)的順序和內容保持一定。對于負責數(shù)據(jù)傳輸?shù)暮瘮?shù)進行考察可以更好地對此種情況進行了解。在函數(shù)sendto()以及recvfrom()中,其收發(fā)緩沖區(qū)的類型被定義為const char*或char*,也就是說,數(shù)據(jù)的接收端會對字節(jié)流進接收以及解讀,依據(jù)發(fā)送端的數(shù)據(jù)結構來將傳輸接收到的數(shù)據(jù)結構進行映射,從而實現(xiàn)對于數(shù)據(jù)成員的還原。
在傳輸?shù)倪^程中,如果數(shù)據(jù)的收發(fā)雙方采用了同種存儲順序邏輯,則數(shù)據(jù)接收端可以直接對數(shù)據(jù)結構進行定義和還原,但是對于存儲策略也有所不同的收發(fā)雙方而言,數(shù)據(jù)接收端就必須利用字節(jié)一致的原則對所接收到的數(shù)據(jù)進行考察,將這些數(shù)據(jù)重新映射到新的數(shù)據(jù)結構之中。
在這個數(shù)據(jù)傳輸?shù)倪^程之中,還有一個重要的問題需要予以關注,就是字節(jié)對齊。對于數(shù)據(jù)傳輸參與的收發(fā)兩端而言,不同的字節(jié)對齊方式必然會造成傳輸數(shù)據(jù)群體中的每一個元素,其排列方式和結構長度的變化。即便是數(shù)據(jù)收發(fā)兩側采用了同樣的存儲序策略,如果無法落實字節(jié)對齊問題,同樣也會造成數(shù)據(jù)接收端數(shù)據(jù)內容接收的失敗。對于此種問題,C/C++編譯器提供了預處理指令#pragma pack(n),這一命令可以讓數(shù)據(jù)的接收端與數(shù)據(jù)的發(fā)送端保持同樣的字節(jié)對齊方式。然而雖然有這樣的指令用于支持,但如果數(shù)據(jù)傳輸雙方的存儲順序存在差異,也仍然需要面對復雜的處理過程。如果有數(shù)據(jù)元素超越了字節(jié)邊界,則需要在數(shù)據(jù)的接收端對數(shù)據(jù)結構進行重新定義,這個時候字節(jié)對齊問題的意義就會顯得尤為突出。編譯器為了保持字節(jié)對齊,還有可能需要對結構體進行填充。這個時候預處理指令#pragma pack(1)可以用于實現(xiàn)結構成員之間沒有填充或者基于字節(jié)定義位域結構等情況的填充,實現(xiàn)方法即面對結構進行字節(jié)的逐一填充[4]。
上一節(jié)中已經提到,如果在數(shù)據(jù)傳輸過程之中,數(shù)據(jù)收發(fā)的兩端采用了不同的存儲順序,則二者之間的數(shù)據(jù)結構映射就會面對更為復雜的情況,處理方案也會更加復雜。在這一部分中對此類情況展開更為深入的闡述。
首先,對于以字節(jié)作為基本單位的數(shù)據(jù)來說,都可以依據(jù)字節(jié)一致的規(guī)則進行傳輸,字節(jié)的順序以及具體的值都不會發(fā)生改變,數(shù)據(jù)的接收端只需要按照發(fā)送端的數(shù)據(jù)結構定義來對數(shù)據(jù)進行處理即可。這樣的情況下,雖然字節(jié)的高低位會發(fā)生反轉,但是對接收端來說并沒有什么影響,也不需要額外進行控制。此類狀態(tài)之下的傳輸可以參見圖1。
其次,對于多字節(jié)數(shù)據(jù)而言,當數(shù)據(jù)的收發(fā)兩端采用了不同額字節(jié)序的時候,同樣,在字節(jié)一致原則的控制之下,接收端獲取到的字節(jié)順序和值并不會因為傳輸過程而有所變化,但字節(jié)權重值會出現(xiàn)反轉。小端系統(tǒng)中的低地址字節(jié)在大端系統(tǒng)中就會成為高位地址字節(jié),這種轉變需要在數(shù)據(jù)傳輸中給予關注。此種情況可以參見圖2。
圖2: 多字節(jié)數(shù)據(jù)傳輸鋸齒圖
圖2(a)為信息發(fā)送端,假設為小端系統(tǒng),傳輸?shù)臄?shù)據(jù)如圖,為一個四個字節(jié)的整數(shù),在進行傳輸之后,在信息接收端形成如圖2(b)的形態(tài),其中數(shù)據(jù)接收端為大端系統(tǒng)。因此就涉及到需要在數(shù)據(jù)接收端對接收到的數(shù)據(jù)結果進行調整,具體來說,就是要將圖2(b)調整成為圖2(c)狀態(tài)。具體而言調整規(guī)則比較簡單,即將多字節(jié)數(shù)據(jù)的最高地址上的字節(jié)內容與最低地址上的字節(jié)內容進行交換,次高地址上的字節(jié)內容與次低地址上的字節(jié)內容進行交換,以此類推,完成整個數(shù)據(jù)的翻轉。
最后,在數(shù)據(jù)傳輸領域之中,有一個特殊的領域叫做“位域”。從概念上看,位域本質上也是一種數(shù)據(jù)結構,其可以將數(shù)據(jù)以“位”的形式進行緊湊存儲,并且允許工作人員對這一類結構中的位進行操作。總體來說,位域的核心優(yōu)勢在于對存儲空間的節(jié)約,這種節(jié)約尤其是在程序需要大容量的數(shù)據(jù)單元的時候,能夠突出其優(yōu)勢。
對于位域的傳輸而言,需要進一步分為兩種情況進行考察。
(1)即單字節(jié)內的位域結構。在對此種位域進行傳輸?shù)臅r候需要注意,無論何種系統(tǒng),在進行結構定義的時候,編譯器都會按照結構成員來進行次序的確定,從內存的低地址開始進行存儲資源的分配。假設在大端系統(tǒng)中來對某位域結構進行定義如下:
改數(shù)據(jù)傳輸?shù)叫《讼到y(tǒng)之后,高低位出現(xiàn)了翻轉,因此對應的結構會轉變成如下:
對于上述的這種情況,用鋸齒圖進行表示,可以參見圖3。
圖3: 數(shù)據(jù)傳輸發(fā)送端以及接收端分別問大端系統(tǒng)和小端系統(tǒng)情況下的鏡像關系
從圖3 中可以看到,當傳輸內容到達接收端之后,原來的高低位發(fā)生了變化,低位換到了高位,原來的高位換到了低位,但是其數(shù)據(jù)值并未發(fā)生改變。
(2)則是跨字節(jié)的位域結構。此種情況發(fā)生在位域結構超出了字節(jié)邊界,對應的情況也會隨之變得復雜。這主要是考慮到高位和低位反轉之后,跨字節(jié)的位域會出現(xiàn)地址不連續(xù)的問題,即整個存儲內容會出現(xiàn)在兩個或者更多地址空間中。這種情況通常稱之為空間斷裂。例如,一個數(shù)據(jù)發(fā)送端如果采用大端系統(tǒng)規(guī)則工作,其上的位域結構為:
在存儲的時候,上面的結構在內存中的映像參見圖4(a),其中灰色部分是跨字節(jié)邊界的部分。這樣的數(shù)據(jù)傳輸?shù)叫《讼到y(tǒng)之后,對應的在內存中的映射見圖4(b)。
圖4: 跨字節(jié)位域數(shù)據(jù)從大端系統(tǒng)傳輸?shù)叫《讼到y(tǒng)中的對比
無論是大端還是小端系統(tǒng),都是從存儲環(huán)境的低位開始展開存儲資源的分配,因此想要實現(xiàn)對于發(fā)送端結構的正確描述,就需要數(shù)據(jù)的接收端重新展開對于結構成員的順序定義。對于上述大端系統(tǒng)發(fā)送的數(shù)據(jù),小端系統(tǒng)應當將數(shù)據(jù)定義成如下結構:
在此種情況中,變量s3.v2 跨越了字節(jié)邊界,對應在到達信息接收端的時候,存儲空間斷為兩個部分。對于這種情況,可以考慮用移位拼接的算法進行處理:
數(shù)據(jù)傳輸過程看似透明,但是其中的細節(jié)關系十分微妙,尤其是高低位的翻轉以及位域的地址斷裂問題。在面對此類情況的時候,需要牢牢把握字節(jié)一致原則,結合內存映射和 鋸齒圖進行理解,實現(xiàn)數(shù)據(jù)的有效還原。