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

网络编程学习记录

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

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


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

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

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

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

1.概括

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

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

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

2.代码及其详细注释

2.1 服务端代码

#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 客户端代码

#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接收剩下数据时,要使用指针偏移,跳过结构体包头的接收(因为接收过了),直接接收到包体数据位置上。

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

2.代码及其详细注释

2.1 服务端代码

#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 客户端代码

#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;
} 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页