Socket无法直接传输结构体

        本次文章会采用VS2017调试,并且看到这个内存变化,从而来解释为什么Socket不能直接传输结构体。【如果结构体里面含有指针是无法传输的。其他的我也不敢打保票,但是结构体里面含有指针应该是一种比较常用情况,所以我们基本上是不使用Scoket来传输结构体的。】

        下面的代码的是在Windows上,采用的是vs2017。并且客户端和服务端都是本机,所以不存在数据传输之后出现大小端的问题。这篇文章并不是从“接收方和发送方可能大小端不同”这个角度来考虑不能使用结构体传输的,而是在保证了大小端一定是相同的情况下,从结构体的内部成员来考虑不能使用结构体传输信息的。

序列化与反序列化

        为什么我这里需要提到序列化与反序列化呢?因为我在学些序列化的工具(例如protobuf)的时候,就有一个疑问:为什么我们的消息不直接采用结构体传输,为什么需要对消息序列化呢?原应就是我们无法采用网络来传输结构体信息。但是我又有疑问了:我明明之前写的程序都是可以的,我之前还采用socket传输文件信息,都是可以的。怎么文件都可以传输,就不能传输结构体呢。况且我我清楚的记得我之前就是采用socket传输过结构体,也没有采用序列化。而我在接收端也可以完整的接收到数据,数据也没有丢失。那么,我们提出序列化的意义到底是什么呢?

        直到今天,我发现Socket是无法传输结构体的。如果我们结构体里面有指针的话,等到接收端接收之后,就无法打印出来了。下面请看我的例子。注意我下面的例子都是采用windows下的scoket编程实现的。

不能传输的例子01

服务端代码:

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char *data;				//消息数据
}Message;


int main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	//监听连接
	int listenfd, connfd;
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in severaddr, clientaddr;
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	severaddr.sin_port = htons(10010);
	bind(listenfd, (sockaddr*)&severaddr, sizeof(severaddr));
	listen(listenfd, 10);
	connfd = accept(listenfd, NULL, NULL);

	//给客户端发送数据
	Message message;
	message.type = 1;
	message.data = (char*)malloc(16);
	memset(message.data, 0, 16);
	memcpy(message.data,"hello",6);
	send(connfd, (char*)&message, sizeof(message), 0);
	closesocket(connfd);
	closesocket(listenfd);
	WSACleanup();
	return 0;
}

客户端代码:

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char *data;				//消息数据
}Message;

int  main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	int  len = 0;
	int connfd = socket(AF_INET, SOCK_STREAM, 0);
	if (connfd < 0)
	{
		cout << "socket函数失败" << endl;
		return -1;
	}
	sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	serveraddr.sin_port = htons(10010);
	//连接服务端
	int re = connect(connfd, (sockaddr*)&serveraddr, sizeof(serveraddr));
	if (re == INVALID_SOCKET)
	{
		cout << "connect() fail" << WSAGetLastError() << endl;
		return -1;
	}
	//发送消息
	Message buf;					//把断点打在这里
	buf.data = (char*)malloc(16);
	memset(buf.data, 0, 16);
	cout << &buf << endl;
	recv(connfd, (char*)&buf, sizeof(buf), 0);
	cout << buf.type << " ";
	cout << buf.data << endl;
	closesocket(connfd);
	WSACleanup();
	return 0;
}

我们把断点打在客户端接收消息的这里。我们在调试的时候来看看我们的客户端到底收到的是什么信息。按照服务端的发送的数据来看,客户端应该收到的是buf.type=1;buf.data=“hello”;那么我们现在来看看到底收到的是什么数据。

        按F10我们调试可以发现:
buf.type的数据是正确的。调试时候的信息如下:
在这里插入图片描述
我们可以看到,我们是正确的收到了这个type的值。同时,我们进入到内存中看看这个type的值到底是多少。因为我们前面已经打印了&buf的值,所以我们在调试的时候打开内存查找窗口,来看&buf这个地址的值是不是1。【如果大家对于这里vs调试不知道怎么查看内存的话,可以百度查一下。这里我就不详细说了。】
在这里插入图片描述
通过监视&buf的值,我们发现buf的地址就是0x008ffa00 ;所以我们现在就到内存查看器中看看这个地址上的数据是多少:
在这里插入图片描述

我们可以看到,从地址0x008ffa00 开始的4个字节,得到的数据就是1。【因为Message buf的第一个成员就是int type。所以地址成员占4个字节。并且我的机器上是采用的是小端模式,即低字节放在低地址处。所以这里的01000000代表的就是1。】

所以可以得出的结论就是:buf的中的type这个成员的值正确得到了。那么接下来就是测试buf中data这个成员值得到没有。

        从我监视的buf这个变量的值来看,上面显示的就是内存读取错误。
在这里插入图片描述

就是说:地址0x00ab5f18 这个地址我们的程序是不能访问的。所以我们的程序继续执行下去就会宕掉。但是,在这个程序宕掉之前,我们看看这个内存中data的值是什么:
在这里插入图片描述

我们可以看到,data的值就是18 5f ab 00。【因为data的类型是char*类型,在32位程序下,该类型占4个字节。】又因为我们这个机器是小端模式,所以这个数据不就是00ab5f18 吗。所以说,是服务端发送了00ab5f18 给我们的客户端。但是服务端发送的不是字符串"hello"吗?怎么就变成00ab5f18 。所以我们应该调试这个服务端,看看这个数据到底是怎么来的。

我们发现服务端的message.data的地址就是0x00ab5f18
在这里插入图片描述
message.data的值是我们服务端采用malloc()函数分配得到的这个地址,是指向堆的一个地址。所以现在就是说:我们data的值也就是0x00ab5f18直接给发给客户端,而不是把0x00ab5f18这个地址所存放的数据"hello"发给客户端。所以这就是为什么客户端无法正确显示data数据的原因。并不是因为客户端没有接收到服务端发送的数据,而是客户端接收到这个地址信息无法使用。从传值的角度来说的,的确就是正确的发送了信息。因为这个mesage.data存放的值就是0x00ab5f18,而它现在将这个数据正确传输到了客户端,客户端无法读取这个地址的信息也不能怪服务端。

我们将本次实验的内存四区图画出来就是这样子:
在这里插入图片描述

对于,客户端而言,自己虽然接受到了这个地址数据,但是无法访问这块堆内存。因为现在server和client在同一台机器上,x000ab5f18这块内存已经分配给server了,所以客户端肯定是无法访问的。退一步讲,就算客户端可以访问这个地址,但是socket更多的用于的就是server和client是在不同的机器上的。所以在server上地址为x000ab5f18的数据是“hello”,不一定client上地址为x000ab5f18就是这个数据。这个是谁都无法保证的。

不能传输的例子02

        为了证明上面说的:服务端就是把数据原原本本的传输给了客户端,我们再次写一个例子。这个例子就是上面那个代码,但是改变的就是服务端的结构体中的data不再是指针,而是一个数组。因为是数组的话,我们就不需要开辟空间,那么此时传输给客户端的肯定就是原本的数据了。

服务端代码:

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char data[8];				//消息数据
}Message;


int main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	//监听连接
	int listenfd, connfd;
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in severaddr, clientaddr;
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	severaddr.sin_port = htons(10010);
	bind(listenfd, (sockaddr*)&severaddr, sizeof(severaddr));
	listen(listenfd, 10);
	connfd = accept(listenfd, NULL, NULL);

	//给客户端发送数据
	Message message;
	message.type = 1;
	memcpy(message.data,"hello",6);
	send(connfd, (char*)&message, sizeof(message), 0);
	closesocket(connfd);
	closesocket(listenfd);
	WSACleanup();
	return 0;
}

客户端代码:

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char *data;				//消息数据
}Message;

int  main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	int  len = 0;
	int connfd = socket(AF_INET, SOCK_STREAM, 0);
	if (connfd < 0)
	{
		cout << "socket函数失败" << endl;
		return -1;
	}
	sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	serveraddr.sin_port = htons(10010);
	//连接服务端
	int re = connect(connfd, (sockaddr*)&serveraddr, sizeof(serveraddr));
	if (re == INVALID_SOCKET)
	{
		cout << "connect() fail" << WSAGetLastError() << endl;
		return -1;
	}
	Message buf;
	buf.data = (char*)malloc(16);
	memset(buf.data, 0, 16);
	cout << &buf << endl;
	//recv(connfd, (char*)&buf, sizeof(buf), 0);
	recv(connfd, (char*)&buf, 10, 0);
	cout << buf.type << " ";
	cout << buf.data << endl;
	closesocket(connfd);
	WSACleanup();
	return 0;
}

首先说明:我这样写代码肯定是不对的。因为我服务端发送的结构体和我客户端用来接收的结构体都不同,这样子接收出来信息肯定是对不上的。我之所以这样去做,就是为了说明服务端发送的数据就是原原本本发给客户端,至于客户端怎么理解这个数据就是客户端应用层该处理的事情了。例如上面01这个例子中,服务端发送了一个地址0x00ab5f18过来,而客户端也接收到了这个地址0x00ab5f18。但是我们客户端和服务端作为两个独立的进程,肯定是无法访问这个地址的。而且,这个还是在同一台机器上,我们socket面临的情况更多都是不同的机器。我们无法保证机器A地址0x00ab5f18和机器B地址0x00ab5f18是一样的。

        我们首先看到服务端message.data中内存中的数据是:
在这里插入图片描述
从监视上看,这个内存中的数据就是hello。那么我们现在从内存中看看:
在这里插入图片描述
我们可以看到,message.data在内存中的数据就是hello,在16进制中显示的就是hello一共5字节,每一个字节的ACSII码。所以按照我们上面的推断,我们现在就会把 68 65 6c 6c 6f这个数据发送给客户端。

我们看到客户端的内存中显示情况:
在这里插入图片描述
可以看到,这里buf.data的数据的确就是 68 65 6c 6c 6f。也就是我们的确就是把这个数据收到了。至于怎么处理这个数据,怎么理解这个数据就是我们应用层的事情了。按照正常的逻辑,我们应该按照ACSII来解释。但是我们这里却把这个值复制给了一个指针,我们看到这个监视的情况:
在这里插入图片描述
因为buf.data是char *类型,所以仅仅只取4个字节。所以buf.data的值就是0x6c6c6568。但是这个地址我们很大几率上是不能访问的。所以,这个上面显示的就是“读取字符串出错。”

        所以,我举上面例子就是为了说明:socket在传输数据的时候,的确就是把数据原原本本的传输给我们了,但是问题在于如果一旦又了指针的数据,进程A的地址不一定在进程B中是可以用的。就算是可以用的,我们的数据肯定也不一样。

        这里顺便说一下:TCP就是按照字节传输我们的数据了,至于如何对待我们的数据,如何解释我们的数据都是需要应用层自己来做的。就算我们02这个例子,我们一共发了5个字符,也就是5字节的数据,但是客户端仅仅只提取了4个字节。因为我们采用的是一个指针来接收的。

        我们来从内存四区图中来理解这个:
在这里插入图片描述
hello传输过来的时候,按照每一个字节来传输的。【图上面写错了,应该是h,而不是H】

h的ASCII是104,转化为16进制就是68.也就是说,服务端发送来的数据就是68 65 6c 6c 6f 00 。但是我们的程序(也就是应用层)仅仅只接收了前面4个字节,将其看作了一个char* 的值。那么现在我们如果cout<<buf.data<<endl;就是说要打印地址为0x6c6c6568的数据。但是这个位置我们程序很大几率是不能访问的。所以说,这里我们利用socket来传输结构体就出现了错误。没有从接收到的数据还原为原来的数据。

可以接收的例子01

服务端

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char data[8];				//消息数据
}Message;


int main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	//监听连接
	int listenfd, connfd;
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in severaddr, clientaddr;
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	severaddr.sin_port = htons(10010);
	bind(listenfd, (sockaddr*)&severaddr, sizeof(severaddr));
	listen(listenfd, 10);
	connfd = accept(listenfd, NULL, NULL);

	//给客户端发送数据
	Message message;
	message.type = 1;
	memcpy(message.data,"hello",6);
	send(connfd, (char*)&message, sizeof(message), 0);
	closesocket(connfd);
	closesocket(listenfd);
	WSACleanup();
	return 0;
}

客户端

#include <WinSock2.h>
#include <iostream>

using namespace std;

//消息体
typedef struct Message
{
	int type;				//消息类型
	char data[8];				//消息数据
}Message;

int  main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	int  len = 0;
	int connfd = socket(AF_INET, SOCK_STREAM, 0);
	if (connfd < 0)
	{
		cout << "socket函数失败" << endl;
		return -1;
	}
	sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	serveraddr.sin_port = htons(10010);
	//连接服务端
	int re = connect(connfd, (sockaddr*)&serveraddr, sizeof(serveraddr));
	if (re == INVALID_SOCKET)
	{
		cout << "connect() fail" << WSAGetLastError() << endl;
		return -1;
	}
	Message buf;
	memset(buf.data, 0, 8);
	cout << &buf << endl;
	recv(connfd, (char*)&buf, 10, 0);
	cout << buf.type << " ";
	cout << buf.data << endl;
	closesocket(connfd);
	WSACleanup();
	return 0;
}

暂时没有解决的问题

        下面的问题都是自己无聊中想到的,不一定正确。所以大家如果有答案的话,可以到下方讨论。

        前面我写的例子都是基本的数据类型。但是我这次做了将结构体里面存放了一个string的数据,然后出现了一个让我比较迷惑的事情:

服务端代码:

#include <WinSock2.h>
#include <iostream>
#include <string>
using namespace std;

//消息体
typedef struct Message
{
	int type;					//消息类型
	string data;				//消息数据
}Message;


int main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	//监听连接
	int listenfd, connfd;
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in severaddr, clientaddr;
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	severaddr.sin_port = htons(10010);
	bind(listenfd, (sockaddr*)&severaddr, sizeof(severaddr));
	listen(listenfd, 10);
	connfd = accept(listenfd, NULL, NULL);

	//给客户端发送数据
	Message message;
	message.type = 1;
	message.data = "hello";
	send(connfd, (char*)&message, sizeof(message), 0);
	closesocket(connfd);
	closesocket(listenfd);
	WSACleanup();
	return 0;
}

客户端代码:

#include <WinSock2.h>
#include <iostream>
#include <string>
using namespace std;

//消息体
typedef struct Message
{
	int type;					//消息类型
	string data;				//消息数据
}Message;

int  main()
{
	WSADATA wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "Winsock 初始化失败" << endl;
		return -1;
	}
	int connfd = socket(AF_INET, SOCK_STREAM, 0);
	if (connfd < 0)
	{
		cout << "socket函数失败" << endl;
		return -1;
	}
	sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	serveraddr.sin_port = htons(10010);
	//连接服务端
	int re = connect(connfd, (sockaddr*)&serveraddr, sizeof(serveraddr));
	if (re == INVALID_SOCKET)
	{
		cout << "connect() fail" << WSAGetLastError() << endl;
		return -1;
	}
	Message buf;
	cout << &buf << endl;
	recv(connfd, (char*)&buf, sizeof(buf), 0);
	//recv(connfd, (char*)&buf, 10, 0);
	cout << buf.type << " ";
	cout << buf.data << endl;
	closesocket(connfd);
	WSACleanup();
	system("pause");
	return 0;
}

因为具体string底层实现我不是很清楚。按照我的想法,string应该就是采用char *来实现的。但是如果是采用char *的话,那么内存中这个变量存放的应该就是数据的地址。但是我观察内存中的数据看到,发现这个数据是直接存放在内存中的。应该不是简单就是采用char *来实现的,我们看到这个内存中的数据:
在这里插入图片描述

通过上面的监视可以看到,我们看看0x0076f964 这个位置的数据:
在这里插入图片描述

我们可以看到,在0x0076f964 开始的4个字节之后,并不是立马存放的是"hello"这个数据,而是存放了一些其他的数据(就是我红色框框中表示的)。这个数据代表什么意思我的确不了解,但是我感觉这个数据应该和string这个类型有关。应该是我们在形成string这个类型的时候,需要的东西。就和我们new和array new的时候,也不是简单的分配了空间给你,还有一些额外的空间也分配出来了【侯捷老师讲过】。这些数据是为了之后操作系统回收内存使用的。我看到这个string的详细视图:
在这里插入图片描述
我们可以看到,在存放"hello"之前,还需要存放size和capacit以及allocator。所以我估计空出来的4个字节,就是用来存放这些数据的。

        分析完服务端的具体数据之后,按照道理来说,就是将服务端数据原原本本发给客户端,我们来看看客户端接收到的到底是什么数据:
在这里插入图片描述
从监视上看,我们需要看到0x00bff93c这个地址的数据:
在这里插入图片描述
可以看到,这个数据的确全部都发送过来了,所以我们是可以直接访问的。按照调试的结果,也是可以获取到这个数据:
在这里插入图片描述
但是问题在于我继续调试的时候,就直接宕掉了:
在这里插入图片描述
就是结果是可以输出的,但是最后结果给宕掉了。原因暂时还不知道。

注意:我们一定要注意在客户端接收消息的时候,recv()函数中的第三个参数是可以控制到底接收几个字节的数据的。所以有的时候我们查看内存发现这个数据没有完整的接收,可能并不是网络传输出现了问题,还有一种情况就是我们的recv()函数中设置的接收的字节数少了。这也是我为什么在代码中recv()这里的注释我是没有删掉的,而是直接展现给大家看的。方便大家查看设置不同的值有什么区别。

说一个题外话,我觉得数据就是看我们自己程序员怎么看待。反正内存中从低到高数据就是65 67 34 78 56 88 …但是我们到底是每一个字节看作一个char类型,还是每4个字节看作是一个int类型,还是每4个自己看作是一个指针类型…这个都是我们程序员自己做的事情,也是应用层自己做的事情。就和我之前的一篇文章:https://blog.csdn.net/suliangkuanjiayou/article/details/103169160
这里也说明了:数据就是我们程序员自己看待的,如果你认为他是指针就是指针,你认为他是int类型就是int类型数据。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在 QT 中,可以使用 `QTcpSocket` 类进行 TCP 传输,而将结构体传输则可以使用 `QDataStream` 类进行序列化和反序列化。 以下是一个简单的示例代码,其中 `MyStruct` 是需要传输结构体: ```c++ struct MyStruct { int a; double b; }; MyStruct myStruct = {1, 2.0}; // 创建 QTcpSocket 对象并连接服务器 QTcpSocket* socket = new QTcpSocket(this); socket->connectToHost("127.0.0.1", 1234); if (socket->waitForConnected()) { // 创建一个 QByteArray 对象 QByteArray block; // 创建一个 QDataStream 对象,将其绑定到 QByteArray 对象上 QDataStream out(&block, QIODevice::WriteOnly); // 设置数据流的版本号 out.setVersion(QDataStream::Qt_5_15); // 将结构体序列化并写入数据流中 out << myStruct; // 将数据流中的数据发送给服务器 socket->write(block); } ``` 在服务器端接收数据时,可以使用类似的方式进行反序列化: ```c++ QTcpSocket* clientConnection = qobject_cast<QTcpSocket*>(sender()); if (clientConnection) { // 读取从客户端发送过来的数据 QByteArray data = clientConnection->readAll(); // 创建一个 QDataStream 对象,将其绑定到 QByteArray 对象上 QDataStream in(&data, QIODevice::ReadOnly); // 设置数据流的版本号 in.setVersion(QDataStream::Qt_5_15); // 读取数据流中的数据并反序列化为结构体 MyStruct myStruct; in >> myStruct; // 处理接收到的结构体数据 qDebug() << "Received struct: " << myStruct.a << myStruct.b; } ``` 在上述代码中,`out << myStruct` 将 `MyStruct` 序列化为二进制格式,并将其写入到 `QByteArray` 中。而 `in >> myStruct` 则从 `QByteArray` 中读取二进制数据,并将其反序列化为 `MyStruct` 对象。注意,在进行序列化和反序列化时,需要使用相同的数据流版本号。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值