你知不知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服务器了,所以啊,数据还是要序列化成与语言平台无关的中间格式再共享出去才好啊

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