本次文章会采用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类型数据。