你知不知Java如何解析C++通過tcp socket傳過來的結構體啊

你:知不知Java如何解析C++通過tcp socket傳過來的結構體啊

我:知不知?

不久前,接到一個任務,使用Java寫一個flume的TcpSource做爲服務端,用於接收C++客戶端程序發送的未序列化的C++結構體並解析成Java對象,要完成這個需求的開發,首先需要了解一點點,結構體內存對齊,字節填充,CPU大小端及網絡字節序,然後就是一點點反彙編,會寫點點C++ /Java代碼,會簡單的使用tcpdump抓包就OK了

咱們先看下結構體內存對齊

簡單點一般結構體內存對齊的2個要素就是:

1. 對於結構體中的每一個成員變量的相對偏移地址要能被min(自身大小,對齊係數)整除

2. 對於結構體的總體大小,要能被min(最大的成員變量大小,對齊係數)整除

有如下代碼

在這裏插入圖片描述

反彙編如下

在這裏插入圖片描述
32位系統下gcc編譯器默認4字節對齊

由反彙編代碼可知編譯器
在char a後多填充了3字節(紅色方框1),可使int b的相對偏移地址滿足要素1,成員變量int b的相對偏移地址0x804a020-0x804a01c=4能被min(int b自身大小4,對齊係數4)=4整除

在char d後多填充了1字節(紅色方框2),可使結構體總體大小滿足要素2,結構體的總體大小12能被min(最大的成員變量int b的大小4,對齊係數4)=4整除

爲什麼大家要關注結構體的內存對齊?其實內存中的變量都要對齊,32位系統下malloc返回的地址都是4字節對齊的

因爲結構體的內存對齊會影響結構體內存佔用的大小,在聲明一個結構體時,有意的調整各個變量的位置,有時可減少甚至避免由於內存對齊導致的字節填充,可以使結構體更加的緊湊以達到節約內存的目地

修改代碼如下:
在這裏插入圖片描述
同樣的4個成員變量但不同的順序,此次的結構體沒有字節填充,大小隻有8字節,比上次少了4字節

爲什麼要關注結構體的內存對齊?

由於結構體的內存對齊,結構體中有些字節是無用的,當我們把一個系統的結構體傳輸到另一個系統時,就算都是C++系統,都運行於x86服務器,但有可能,兩個系統在編譯時,對結構體使用了不同的內存對齊大小,如果發送端採用直接發送結構體的方式,接收端直接強轉使用就會出現問題

如下代碼分別對同一個結構體

1字節對齊
在這裏插入圖片描述

4字節對齊

在這裏插入圖片描述

我們可以看到在採用1字節對齊時,結構體內無填充,而使用4字節對齊時結構體內填填充了4字節(圖中紅色方框)

由此我們知道了結構體內存對齊要靠字節填充來完成,由於結構體的內存對齊,結構體中有些字節是無用的,我們在此次使用Java程序解析時是要跳過這些字節的(劃重點),或者你嫌麻煩,可以使此結構體以1字節對齊,簡單粗爆,或精心設計結構體避免出現字節填充,就不會有多餘的字節填充進去了,再或者咱使用protobuf序列化反序列化更省事

爲什麼要結構體內存對齊?

一說cpu在訪問對齊的地址時性能更好,二說是某些cpu只能訪問對齊的地址

對於一說,其實我也不知對不對,比如結構體中2字節的short如果不對齊有可能造成同一個變量的2個字節出現跨緩存,分別被緩存到cpu的兩條cacheline中,當cpu在訪問這個short變量時就會訪問兩次緩存從不同的cacheline分別取得這個short變量的2個字節,拼成2字節再做計算從而影響cpu的性能

接下來咱們看下CPU大小端,網絡字節序,要想知道CPU是大端還是小端在linux下只需lscpu即可
在這裏插入圖片描述
X86的機器都是小端模式,而ARM即可以運行在小端也可以運行在大端模式

大小端描述的是多字節類型在內存中的存放順序,如short ,int,long,double,float在內存中存儲的方式,到底是低地址放低字節還是高地址放低字節。低地址放低字節,高地址放高字節與咱們的閱讀順序相反存放就是小端,反之就是大端,更符合咱們的閱讀順序

如下代碼
在這裏插入圖片描述
反彙編如下
在這裏插入圖片描述
咱們可以看到低地址0x804a01c存放的是低字節0x78,高地址0x804a01b存放的是高字節0x12與咱們的閱讀順序相反,是小端,咱們要反着才能拼出一個32的整形的值,是不是彆扭

爲什麼我們要關注大小端?

大小端有什麼用?各有什麼優勢?

沒啥用吧!沒啥優勢吧!

由於存在大小端,當小端系統上的一個多字節,如int通過網絡傳輸到另一臺大端系統上,這時就會出現問題,0x12345678將會變成0x78563412(劃重點),因此出現了網絡字節序,來做爲一箇中間層,爲了確保能正確的在不同字節序的系統中處理網絡中來自其它系統的多字節,還要有約束條件,就是發送之前要把多字節轉換成網絡字節序再發送,接收到後要轉換成本系統的字節序再做處理

網絡字節序是什麼?其就是大端存儲模式,網絡字節序的出現,倒是讓使用大端存儲模式的系統多了些優勢,在接收到網絡字節序後,咱不用再轉換成本系統的字節序了

由此我們知道了在不同字節序的系統間共享二進制多字節的數據類型在解析時,要明確知道它的字節序,然後做相應的字節序轉換

有了結構體內存對齊,有了大小端導致的數據解析錯亂,於是就有了序列化,來解決這些不同系統間的差異性產生的問題

結構體內存對齊字節填充,系統的大小端會導致多字節類型數據解析錯亂,因此咱們在把結構體或對象從一個系統傳輸到另一個系統時,要進行序列化,可以序列化成文本的json,xml也可序列化成二進制的字節流,在此咱們只關注二進制的,二進制的序列化可以簡單的理解成對象的深拷貝,就是把結構體,或對象中的成員變量拿出來一個一個的轉換成中間格式再拷貝到字節數組,同時去除因內存對齊而填充的多餘字節,最後再發送出去。

由於歷史原因,我們的程序都是C++的,而程序運行的服務器都是清一色x86,在傳輸結構體時,沒有進行序列化,沒有字節序的轉換,因此接收到時也無需反序列化直接強轉成相應的結構體就可以使用了,所以此次使用Java程序對接C++程序時,要清楚的知道內存中的每一個字節,到底有沒有用,及這些字節是什麼,由於x86是小端,Java是大端所以還要對所有接收到的多字節類型變量進行字節序轉換,,,

我們知道了,結構體內部會字節填充,多字節類型變量會有字節序,這兩種差異會數據導致解析錯亂,接下來看下如何操作才能正確的完成C++程序通過Tcp Socket傳輸C++結構體,給JAVA程序解析

OK,開始步入正題

C++客戶端的代碼如下:

發送字符流可以使用特殊字符如換行做分隔,發送二進制無邊界字節流,爲了防止TCP的粘包,一般咱們都是發TLV的包,然後readn直至L纔算讀到一個完整的包
在這裏插入圖片描述
java服務端的代碼如下:
在這裏插入圖片描述

OK,編譯運行tcpdump抓包如下,有點亂啊,將就着還是能看的哈
在這裏插入圖片描述

在抓到的包裏我找出C++客戶端程序發送的結構體的字節流與結構體成員變量的對應關係如上圖,Java服務端收到的包同樣如此,我們知道Java默認是大端模式,而我們發送的數據,可以看到是小端,所以Java服務端程序在解析時要進行大小端的轉換

以上圖爲參考跳過該跳的讀取該讀的,修改Java服務端代碼如下:
在這裏插入圖片描述

再次運行程序Java服務端輸出如下
在這裏插入圖片描述
簡直完美,成功接收解析

至此咱們已經完成了Java程序通過tcp
socket接收解析C++結構體,怎麼樣,是不是沒想到,原來這麼簡單啊

來看下咱要解析的結構體,真是個體力活啊,還有兩個,還好沒有嵌套的結構體,不過咱知道了基本原理,纔不管它怎樣嵌套,有多少變量,都無所謂啦,被脫下了衣服的女人,應該都是一樣的吧,,,
在這裏插入圖片描述

爲什麼不改C++程序讓其輸出序列化的對象?程序是其它部門的不是想改就能改,你讓別人改,對別人又沒啥好處,,,

我們也已經開始使用ARM服務器了,所以啊,數據還是要序列化成與語言平臺無關的中間格式再共享出去纔好啊

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