C++网络编程学习:网络数据报文的收发

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows

一、网络数据报文的格式定义

  • 报文有两个部分,包头包体,是网络消息的基本单元。
  • 包头: 描述本次消息包的大小,描述包体数据的作用。
  • 包体: 其中包含了需要传输的数据。

  根据此数据结构,我们可以根据包头的内容,来灵活的对包体的数据进行处理。

二、将包头与包体数据分开收发

1.概括

  通过上文对网络数据报文的定义,我们可以很轻易的想到:

  1. 发送端进行两次send操作,第一次send发送包头,第二次send发送包体,即可实现网络数据报文的发送。
  2. 接收端进行两次recv操作,第一次recv接收包头,第二次recv接收包体并根据包头的内容进行数据处理,即可实现网络数据报文的接收。

  按以上操作,即可实现网络数据报文的收发。

2.代码及其详细注释

2.1 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGOUT,//登出
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包体1 登录 传输账号与密码
struct Login
{
char UserName[32];//用户名
char PassWord[32];//密码
};
//包体2 登录结果 传输结果
struct LoginResult
{
int Result;
};
//包体3 登出 传输用户名
struct Logout
{
char UserName[32];//用户名
};
//包体4 登出结果 传输结果
struct LogoutResult
{
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//IPV4 数据流类型 TCP类型
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//绑定网络端口和IP地址
sockaddr_in _myaddr = {};//建立sockaddr结构体 sockaddr_in结构体方便填写 但是下面要进行类型转换
_myaddr.sin_family = AF_INET;//IPV4
_myaddr.sin_port = htons(8888);//端口 host to net unsigned short
_myaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络地址 INADDR_ANY监听所有网卡的端口
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{
cout<<"绑定不成功"<<endl;
}
else
{
//cout<<"绑定成功"<<endl;
}

//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<<endl;
}

//等待接收客户端连接
sockaddr_in _clientAddr = {};//新建sockadd结构体接收客户端数据
int _addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度
SOCKET _temp_socket = INVALID_SOCKET;//声明客户端套接字

_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
if(INVALID_SOCKET == _temp_socket)//接收失败
{
cout<<"接收到无效客户端Socket"<<endl;
}
else
{
cout<<"新客户端加入"<<endl;
printf("IP地址为:%s \n", inet_ntoa(_clientAddr.sin_addr));
}

while(true)
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("客户端已退出\n");
break;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGIN://登录 接收登录包体
{
Login _login = {};
recv(_temp_socket,(char*)&_login,sizeof(Login),0);
/*
进行判断操作
*/
printf("%s已登录\n",_login.UserName);
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
LoginResult _result = {1};
send(_temp_socket,(char*)&_result,sizeof(LoginResult),0);//发包体
}
break;
case CMD_LOGOUT://登出 接收登出包体
{
Logout _logout = {};
recv(_temp_socket,(char*)&_logout,sizeof(Logout),0);
/*
进行判断操作
*/
printf("%s已登出\n",_logout.UserName);
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
LogoutResult _result = {1};
send(_temp_socket,(char*)&_result,sizeof(LogoutResult),0);//发包体
}
break;
default://错误
{
_head.cmd = CMD_ERROR;
_head.date_length = 0;
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
}
break;
}
}

//关闭客户端socket
closesocket(_temp_socket);

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

printf("任务结束,程序已退出");

getchar();

return 0;
}

2.2 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGOUT,//登出
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包体1 登录 传输账号与密码
struct Login
{
char UserName[32];//用户名
char PassWord[32];//密码
};
//包体2 登录结果 传输结果
struct LoginResult
{
int Result;
};
//包体3 登出 传输用户名
struct Logout
{
char UserName[32];//用户名
};
//包体4 登出结果 传输结果
struct LogoutResult
{
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//连接服务器
sockaddr_in _sin = {};//sockaddr结构体
_sin.sin_family = AF_INET;//IPV4
_sin.sin_port = htons(8888);//想要连接的端口号
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{
cout<<"连接失败"<<endl;
closesocket(_mysocket);
}
else
{
cout<<"连接成功"<<endl;
}

while(true)
{
//输入请求
char _msg[256] = {};
scanf("%s",_msg);
//处理请求
if(0 == strcmp(_msg,"exit"))
{
break;
}
else if(0 == strcmp(_msg,"login"))
{
//发送
Login _login = {"河边小咸鱼","123456"};
DateHeader _head = {CMD_LOGIN,sizeof(_login)};
send(_mysocket,(const char*)&_head,sizeof(_head),0);
send(_mysocket,(const char*)&_login,sizeof(_login),0);
//接收
DateHeader _head2 = {};
LoginResult _result= {};
recv(_mysocket,(char*)&_head2,sizeof(DateHeader),0);
recv(_mysocket,(char*)&_result,sizeof(LoginResult),0);
printf("result:%d\n",_result.Result);
}
else if(0 == strcmp(_msg,"logout"))
{
//发送
Logout _logout = {"河边小咸鱼"};
DateHeader _head = {CMD_LOGOUT,sizeof(_logout)};
send(_mysocket,(const char*)&_head,sizeof(_head),0);
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
//接收
DateHeader _head2 = {};
LogoutResult _result= {};
recv(_mysocket,(char*)&_head2,sizeof(DateHeader),0);
recv(_mysocket,(char*)&_result,sizeof(LogoutResult),0);
printf("result:%d\n",_result.Result);
}
else
{
printf("不存在的命令\n");
}
}

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

return 0;
}

三、将分开收发报文数据改为一次收发

1.思路

  由上文,我们可以知道,可以通过两次send和两次recv进行报文的收发,但是其中操作较为麻烦,需要多次声明DateHeader包头结构体,不仅消耗时间资源,也容易出错。
  因此,我们可以尝试将分开收发改为一次收发。大致思路为完善报文的结构体,使包体继承包头结构体,或者使包体结构体中包含一个包头结构体。由此完善报文的结构体,只进行一次send操作,即可发送所有报文数据。
  在进行数据接收时,我们先接收包头大小的数据,随后根据包头的内容,来确定接下来接收数据的大小,即接收 总数据大小 减去 包头数据大小 的数据。而在接下来使用recv接收剩下数据时,要使用指针偏移,跳过结构体包头的接收(因为接收过了),直接接收到包体数据位置上。

1
2
3
4
5
/*
因为包头已经接收过了 所以进行指针偏移:&_login+sizeof(DateHeader)
接收数据长短为:sizeof(Login)-sizeof(DateHeader) 减去包头的大小
*/
recv(_temp_socket,(char*)&_login+sizeof(DateHeader),sizeof(Login)-sizeof(DateHeader),0);

2.代码及其详细注释

2.1 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//IPV4 数据流类型 TCP类型
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//绑定网络端口和IP地址
sockaddr_in _myaddr = {};//建立sockaddr结构体 sockaddr_in结构体方便填写 但是下面要进行类型转换
_myaddr.sin_family = AF_INET;//IPV4
_myaddr.sin_port = htons(8888);//端口 host to net unsigned short
_myaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络地址 INADDR_ANY监听所有网卡的端口
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{
cout<<"绑定不成功"<<endl;
}
else
{
//cout<<"绑定成功"<<endl;
}

//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<<endl;
}

//等待接收客户端连接
sockaddr_in _clientAddr = {};//新建sockadd结构体接收客户端数据
int _addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度
SOCKET _temp_socket = INVALID_SOCKET;//声明客户端套接字

_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
if(INVALID_SOCKET == _temp_socket)//接收失败
{
cout<<"接收到无效客户端Socket"<<endl;
}
else
{
cout<<"新客户端加入"<<endl;
printf("IP地址为:%s \n", inet_ntoa(_clientAddr.sin_addr));
}

while(true)
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("客户端已退出\n");
break;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGIN://登录 接收登录包体
{
Login _login;
recv(_temp_socket,(char*)&_login+sizeof(DateHeader),sizeof(Login)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登录\n密码:%s\n",_login.UserName,_login.PassWord);
LoginResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LoginResult),0);//发包体
}
break;
case CMD_LOGOUT://登出 接收登出包体
{
Logout _logout;
recv(_temp_socket,(char*)&_logout+sizeof(DateHeader),sizeof(Logout)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登出\n",_logout.UserName);
LogoutResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LogoutResult),0);//发包体
}
break;
default://错误
{
_head.cmd = CMD_ERROR;
_head.date_length = 0;
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
}
break;
}
}

//关闭客户端socket
closesocket(_temp_socket);

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

printf("任务结束,程序已退出");

getchar();

return 0;
}

2.2 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//连接服务器
sockaddr_in _sin = {};//sockaddr结构体
_sin.sin_family = AF_INET;//IPV4
_sin.sin_port = htons(8888);//想要连接的端口号
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{
cout<<"连接失败"<<endl;
closesocket(_mysocket);
}
else
{
cout<<"连接成功"<<endl;
}

while(true)
{
//输入请求
char _msg[256] = {};
scanf("%s",_msg);
//处理请求
if(0 == strcmp(_msg,"exit"))
{
break;
}
else if(0 == strcmp(_msg,"login"))
{
//发送
Login _login;
strcpy(_login.UserName,"河边小咸鱼");
strcpy(_login.PassWord,"123456");
send(_mysocket,(const char*)&_login,sizeof(_login),0);
//接收
LoginResult _result;
recv(_mysocket,(char*)&_result,sizeof(LoginResult),0);
printf("result:%d\n",_result.Result);
}
else if(0 == strcmp(_msg,"logout"))
{
//发送
Logout _logout;
strcpy(_logout.UserName,"河边小咸鱼");
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
//接收
LogoutResult _result;
recv(_mysocket,(char*)&_result,sizeof(LogoutResult),0);
printf("result:%d\n",_result.Result);
}
else
{
printf("不存在的命令\n");
}
}

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

return 0;
}

网络编程学习记录

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows

笔记一:建立基础TCP服务端/客户端  点我跳转
笔记二:网络数据报文的收发  点我跳转
笔记三:升级为select网络模型  点我跳转
笔记四:跨平台支持Windows、Linux系统  点我跳转
笔记五:源码的封装  点我跳转
笔记六:缓冲区溢出与粘包分包  点我跳转
笔记七:服务端多线程分离业务处理高负载  点我跳转
笔记八:对socket select网络模型的优化  点我跳转
笔记九:消息接收与发送分离  点我跳转
笔记十:项目化 (加入内存池静态库 / 报文动态库)  更多笔记请点我

@TOC

一、网络数据报文的格式定义

  • 报文有两个部分,包头包体,是网络消息的基本单元。
  • 包头: 描述本次消息包的大小,描述包体数据的作用。
  • 包体: 其中包含了需要传输的数据。

  根据此数据结构,我们可以根据包头的内容,来灵活的对包体的数据进行处理。

二、将包头与包体数据分开收发

1.概括

  通过上文对网络数据报文的定义,我们可以很轻易的想到:

  1. 发送端进行两次send操作,第一次send发送包头,第二次send发送包体,即可实现网络数据报文的发送。
  2. 接收端进行两次recv操作,第一次recv接收包头,第二次recv接收包体并根据包头的内容进行数据处理,即可实现网络数据报文的接收。

  按以上操作,即可实现网络数据报文的收发。

2.代码及其详细注释

2.1 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGOUT,//登出
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包体1 登录 传输账号与密码
struct Login
{
char UserName[32];//用户名
char PassWord[32];//密码
};
//包体2 登录结果 传输结果
struct LoginResult
{
int Result;
};
//包体3 登出 传输用户名
struct Logout
{
char UserName[32];//用户名
};
//包体4 登出结果 传输结果
struct LogoutResult
{
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//IPV4 数据流类型 TCP类型
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//绑定网络端口和IP地址
sockaddr_in _myaddr = {};//建立sockaddr结构体 sockaddr_in结构体方便填写 但是下面要进行类型转换
_myaddr.sin_family = AF_INET;//IPV4
_myaddr.sin_port = htons(8888);//端口 host to net unsigned short
_myaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络地址 INADDR_ANY监听所有网卡的端口
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{
cout<<"绑定不成功"<<endl;
}
else
{
//cout<<"绑定成功"<<endl;
}

//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<<endl;
}

//等待接收客户端连接
sockaddr_in _clientAddr = {};//新建sockadd结构体接收客户端数据
int _addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度
SOCKET _temp_socket = INVALID_SOCKET;//声明客户端套接字

_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
if(INVALID_SOCKET == _temp_socket)//接收失败
{
cout<<"接收到无效客户端Socket"<<endl;
}
else
{
cout<<"新客户端加入"<<endl;
printf("IP地址为:%s \n", inet_ntoa(_clientAddr.sin_addr));
}

while(true)
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("客户端已退出\n");
break;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGIN://登录 接收登录包体
{
Login _login = {};
recv(_temp_socket,(char*)&_login,sizeof(Login),0);
/*
进行判断操作
*/
printf("%s已登录\n",_login.UserName);
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
LoginResult _result = {1};
send(_temp_socket,(char*)&_result,sizeof(LoginResult),0);//发包体
}
break;
case CMD_LOGOUT://登出 接收登出包体
{
Logout _logout = {};
recv(_temp_socket,(char*)&_logout,sizeof(Logout),0);
/*
进行判断操作
*/
printf("%s已登出\n",_logout.UserName);
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
LogoutResult _result = {1};
send(_temp_socket,(char*)&_result,sizeof(LogoutResult),0);//发包体
}
break;
default://错误
{
_head.cmd = CMD_ERROR;
_head.date_length = 0;
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
}
break;
}
}

//关闭客户端socket
closesocket(_temp_socket);

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

printf("任务结束,程序已退出");

getchar();

return 0;
}

2.2 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGOUT,//登出
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包体1 登录 传输账号与密码
struct Login
{
char UserName[32];//用户名
char PassWord[32];//密码
};
//包体2 登录结果 传输结果
struct LoginResult
{
int Result;
};
//包体3 登出 传输用户名
struct Logout
{
char UserName[32];//用户名
};
//包体4 登出结果 传输结果
struct LogoutResult
{
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//连接服务器
sockaddr_in _sin = {};//sockaddr结构体
_sin.sin_family = AF_INET;//IPV4
_sin.sin_port = htons(8888);//想要连接的端口号
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{
cout<<"连接失败"<<endl;
closesocket(_mysocket);
}
else
{
cout<<"连接成功"<<endl;
}

while(true)
{
//输入请求
char _msg[256] = {};
scanf("%s",_msg);
//处理请求
if(0 == strcmp(_msg,"exit"))
{
break;
}
else if(0 == strcmp(_msg,"login"))
{
//发送
Login _login = {"河边小咸鱼","123456"};
DateHeader _head = {CMD_LOGIN,sizeof(_login)};
send(_mysocket,(const char*)&_head,sizeof(_head),0);
send(_mysocket,(const char*)&_login,sizeof(_login),0);
//接收
DateHeader _head2 = {};
LoginResult _result= {};
recv(_mysocket,(char*)&_head2,sizeof(DateHeader),0);
recv(_mysocket,(char*)&_result,sizeof(LoginResult),0);
printf("result:%d\n",_result.Result);
}
else if(0 == strcmp(_msg,"logout"))
{
//发送
Logout _logout = {"河边小咸鱼"};
DateHeader _head = {CMD_LOGOUT,sizeof(_logout)};
send(_mysocket,(const char*)&_head,sizeof(_head),0);
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
//接收
DateHeader _head2 = {};
LogoutResult _result= {};
recv(_mysocket,(char*)&_head2,sizeof(DateHeader),0);
recv(_mysocket,(char*)&_result,sizeof(LogoutResult),0);
printf("result:%d\n",_result.Result);
}
else
{
printf("不存在的命令\n");
}
}

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

return 0;
}

三、将分开收发报文数据改为一次收发

1.思路

  由上文,我们可以知道,可以通过两次send和两次recv进行报文的收发,但是其中操作较为麻烦,需要多次声明DateHeader包头结构体,不仅消耗时间资源,也容易出错。
  因此,我们可以尝试将分开收发改为一次收发。大致思路为完善报文的结构体,使包体继承包头结构体,或者使包体结构体中包含一个包头结构体。由此完善报文的结构体,只进行一次send操作,即可发送所有报文数据。
  在进行数据接收时,我们先接收包头大小的数据,随后根据包头的内容,来确定接下来接收数据的大小,即接收 总数据大小 减去 包头数据大小 的数据。而在接下来使用recv接收剩下数据时,要使用指针偏移,跳过结构体包头的接收(因为接收过了),直接接收到包体数据位置上。

1
2
3
4
5
/*
因为包头已经接收过了 所以进行指针偏移:&_login+sizeof(DateHeader)
接收数据长短为:sizeof(Login)-sizeof(DateHeader) 减去包头的大小
*/
recv(_temp_socket,(char*)&_login+sizeof(DateHeader),sizeof(Login)-sizeof(DateHeader),0);

2.代码及其详细注释

2.1 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//IPV4 数据流类型 TCP类型
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//绑定网络端口和IP地址
sockaddr_in _myaddr = {};//建立sockaddr结构体 sockaddr_in结构体方便填写 但是下面要进行类型转换
_myaddr.sin_family = AF_INET;//IPV4
_myaddr.sin_port = htons(8888);//端口 host to net unsigned short
_myaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络地址 INADDR_ANY监听所有网卡的端口
if(SOCKET_ERROR == bind(_mysocket,(sockaddr*)&_myaddr,sizeof(sockaddr_in)))//socket (强制转换)sockaddr结构体 结构体大小
{
cout<<"绑定不成功"<<endl;
}
else
{
//cout<<"绑定成功"<<endl;
}

//监听网络端口
if(SOCKET_ERROR == listen(_mysocket,5))//套接字 最大多少人连接
{
cout<<"监听失败"<<endl;
}
else
{
//cout<<"监听成功"<<endl;
}

//等待接收客户端连接
sockaddr_in _clientAddr = {};//新建sockadd结构体接收客户端数据
int _addr_len = sizeof(sockaddr_in);//获取sockadd结构体长度
SOCKET _temp_socket = INVALID_SOCKET;//声明客户端套接字

_temp_socket = accept(_mysocket,(sockaddr*)&_clientAddr,&_addr_len);//自身套接字 客户端结构体 结构体大小
if(INVALID_SOCKET == _temp_socket)//接收失败
{
cout<<"接收到无效客户端Socket"<<endl;
}
else
{
cout<<"新客户端加入"<<endl;
printf("IP地址为:%s \n", inet_ntoa(_clientAddr.sin_addr));
}

while(true)
{
//接收客户端发送的数据
DateHeader _head = {};
int _buf_len = recv(_temp_socket,(char*)&_head,sizeof(DateHeader),0);
if(_buf_len<=0)
{
printf("客户端已退出\n");
break;
}
printf("接收到包头,命令:%d,数据长度:%d\n",_head.cmd,_head.date_length);
switch(_head.cmd)
{
case CMD_LOGIN://登录 接收登录包体
{
Login _login;
recv(_temp_socket,(char*)&_login+sizeof(DateHeader),sizeof(Login)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登录\n密码:%s\n",_login.UserName,_login.PassWord);
LoginResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LoginResult),0);//发包体
}
break;
case CMD_LOGOUT://登出 接收登出包体
{
Logout _logout;
recv(_temp_socket,(char*)&_logout+sizeof(DateHeader),sizeof(Logout)-sizeof(DateHeader),0);
/*
进行判断操作
*/
printf("%s已登出\n",_logout.UserName);
LogoutResult _result;
_result.Result = 1;
send(_temp_socket,(char*)&_result,sizeof(LogoutResult),0);//发包体
}
break;
default://错误
{
_head.cmd = CMD_ERROR;
_head.date_length = 0;
send(_temp_socket,(char*)&_head,sizeof(DateHeader),0);//发包头
}
break;
}
}

//关闭客户端socket
closesocket(_temp_socket);

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

printf("任务结束,程序已退出");

getchar();

return 0;
}

2.2 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#define WIN32_LEAN_AND_MEAN

#include<winSock2.h>
#include<windows.h>
#include<bits/stdc++.h>

#pragma comment(lib,"ws2_32.lib")//链接此动态链接库 windows特有

using namespace std;

//枚举类型记录命令
enum cmd
{
CMD_LOGIN,//登录
CMD_LOGINRESULT,//登录结果
CMD_LOGOUT,//登出
CMD_LOGOUTRESULT,//登出结果
CMD_ERROR//错误
};
//定义数据包头
struct DateHeader
{
short cmd;//命令
short date_length;//数据的长短
};
//包1 登录 传输账号与密码
struct Login : public DateHeader
{
Login()//初始化包头
{
this->cmd = CMD_LOGIN;
this->date_length = sizeof(Login);
}
char UserName[32];//用户名
char PassWord[32];//密码
};
//包2 登录结果 传输结果
struct LoginResult : public DateHeader
{
LoginResult()//初始化包头
{
this->cmd = CMD_LOGINRESULT;
this->date_length = sizeof(LoginResult);
}
int Result;
};
//包3 登出 传输用户名
struct Logout : public DateHeader
{
Logout()//初始化包头
{
this->cmd = CMD_LOGOUT;
this->date_length = sizeof(Logout);
}
char UserName[32];//用户名
};
//包4 登出结果 传输结果
struct LogoutResult : public DateHeader
{
LogoutResult()//初始化包头
{
this->cmd = CMD_LOGOUTRESULT;
this->date_length = sizeof(LogoutResult);
}
int Result;
};

int main()
{
//启动windows socket 2,x环境 windows特有
WORD ver = MAKEWORD(2,2);//WinSock库版本号
WSADATA dat;//网络结构体 储存WSAStartup函数调用后返回的Socket数据
if(0 != WSAStartup(ver,&dat))//正确初始化后返回0
{
return 0;
}

//建立一个socket
SOCKET _mysocket = socket(AF_INET,SOCK_STREAM,0);//IPV4 数据流类型 类型可以不用写
if(INVALID_SOCKET == _mysocket)//建立失败
{
return 0;
}

//连接服务器
sockaddr_in _sin = {};//sockaddr结构体
_sin.sin_family = AF_INET;//IPV4
_sin.sin_port = htons(8888);//想要连接的端口号
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//想要连接的IP
if(SOCKET_ERROR == connect(_mysocket,(sockaddr*)&_sin,sizeof(sockaddr_in)))
{
cout<<"连接失败"<<endl;
closesocket(_mysocket);
}
else
{
cout<<"连接成功"<<endl;
}

while(true)
{
//输入请求
char _msg[256] = {};
scanf("%s",_msg);
//处理请求
if(0 == strcmp(_msg,"exit"))
{
break;
}
else if(0 == strcmp(_msg,"login"))
{
//发送
Login _login;
strcpy(_login.UserName,"河边小咸鱼");
strcpy(_login.PassWord,"123456");
send(_mysocket,(const char*)&_login,sizeof(_login),0);
//接收
LoginResult _result;
recv(_mysocket,(char*)&_result,sizeof(LoginResult),0);
printf("result:%d\n",_result.Result);
}
else if(0 == strcmp(_msg,"logout"))
{
//发送
Logout _logout;
strcpy(_logout.UserName,"河边小咸鱼");
send(_mysocket,(const char*)&_logout,sizeof(_logout),0);
//接收
LogoutResult _result;
recv(_mysocket,(char*)&_result,sizeof(LogoutResult),0);
printf("result:%d\n",_result.Result);
}
else
{
printf("不存在的命令\n");
}
}

//关闭socket
closesocket(_mysocket);

//清除windows socket 环境
WSACleanup();

return 0;
}