项目(百万并发网络通信架构)2---封装网络数据报

一、为什么设计网络数据报

  • 在消息交互时使用字符串不安全,且比较麻烦不好处理

二、本文案例中用到的数据报格式

  • 本文的案例为:
    • 客户端可以输入login来登录服务端,或者输入logout来退出服务端
    • 服务端根据客户端传来的数据,做相应的回复

消息类型枚举

//消息的类型
enum CMD
{
	CMD_LOGIN,         //登录
	CMD_LOGIN_RESULT,  //登录结果
	CMD_LOGOUT,        //退出
	CMD_LOGOUT_RESULT, //退出结果
	CMD_ERROR          //错误
};

包头

//数据报文的头部
struct DataHeader
{
	short cmd;        //命令的类型
	short dataLength; //数据的长度
};

包体:登录请求消息体、登录请求回送消息体

//登录消息体
struct Login :public DataHeader 
{
	Login() {
		cmd = CMD_LOGIN;
		dataLength = sizeof(Login);
	}
	char userName[32]; //账号
	char PassWord[32]; //密码
};

//登录结果
struct LoginResult :public DataHeader
{
	LoginResult():result(0) {
		cmd = CMD_LOGIN_RESULT;
		dataLength = sizeof(LoginResult);
	}
	int result; //登录的结果,0代表正常
};

包体:退出请求消息体、退出请求回送消息体

//退出消息体
struct Logout :public DataHeader
{
	Logout() {
		cmd = CMD_LOGOUT;
		dataLength = sizeof(Logout);
	}
	char userName[32]; //账号
};

//退出结果
struct LogoutResult :public DataHeader
{
	LogoutResult() :result(0) {
		cmd = CMD_LOGOUT_RESULT;
		dataLength = sizeof(LogoutResult);
	}
	int result; //退出的结果,0代表正常
};

三、服务端代码如下

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()

#include <windows.h>
#include <WinSock2.h>
#include <iostream>
#include <string.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

using namespace std;

//消息的类型
enum CMD
{
	CMD_LOGIN,         //登录
	CMD_LOGIN_RESULT,  //登录结果
	CMD_LOGOUT,        //退出
	CMD_LOGOUT_RESULT, //退出结果
	CMD_ERROR          //错误
};

//数据报文的头部
struct DataHeader
{
	short cmd;        //命令的类型
	short dataLength; //数据的长度
};

//登录消息体
struct Login :public DataHeader 
{
	Login() {
		cmd = CMD_LOGIN;
		dataLength = sizeof(Login); //消息长度=消息头(父类)+消息体(子类)
	}
	char userName[32]; //账号
	char PassWord[32]; //密码
};

//登录结果
struct LoginResult :public DataHeader
{
	LoginResult():result(0) {
		cmd = CMD_LOGIN_RESULT;
		dataLength = sizeof(LoginResult);
	}
	int result; //登录的结果,0代表正常
};

//退出消息体
struct Logout :public DataHeader
{
	Logout() {
		cmd = CMD_LOGOUT;
		dataLength = sizeof(Logout);
	}
	char userName[32]; //账号
};

//退出结果
struct LogoutResult :public DataHeader
{
	LogoutResult() :result(0) {
		cmd = CMD_LOGOUT_RESULT;
		dataLength = sizeof(LogoutResult);
	}
	int result; //退出的结果,0代表正常
};

int main()
{
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	//建立socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock){
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		std::cout << "建立socket成功!" << std::endl;
	}

	//初始化服务端地址
	struct sockaddr_in _sin = {};
	_sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.104");
	//_sin.sin_addr.S_un.S_addr = INADDR_ANY;
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);

	//绑定服务端地址
	if (SOCKET_ERROR == bind(_sock, (struct sockaddr*)&_sin, sizeof(_sin))){
		std::cout << "ERROR:绑定地址失败!" << std::endl;
	}
	else {
		std::cout << "绑定地址成功!" << std::endl;
	}

	//监听网络端口
	if (SOCKET_ERROR == listen(_sock, 5)) {
		std::cout << "ERROR:监听网络端口失败!" << std::endl;
	}
	else {
		std::cout << "监听网络端口成功!" << std::endl;
	}

	//用来保存客户端地址
	struct sockaddr_in _clientAddr = {};
	int nAddrLen = sizeof(_clientAddr);
	SOCKET _cSock = INVALID_SOCKET;
	char msgBuf[] = "Hello, I'm Server.";

	//接收客户端连接
	_cSock = accept(_sock, (struct sockaddr*)&_clientAddr, &nAddrLen);
	if (INVALID_SOCKET == _cSock) {
		std::cout << "ERROR:接收到无效客户端!" << std::endl;
	}
	else {
		std::cout << "接受到新的客户端连接,IP=" << inet_ntoa(_clientAddr.sin_addr)
			<< ",Socket=" << static_cast<int>(_cSock) << std::endl;
	}

	//循环处理客户端的数据
	while (true)
	{
		//先接收消息头
		DataHeader header = {};
		int _nLen = recv(_cSock, (char*)&header, sizeof(header), 0);
		if (_nLen < 0) {
			std::cout << "recv函数出错!" << std::endl;
			break;
		}
		else if (_nLen == 0) {
			std::cout << "客户端已退出!" << std::endl;
			break;
		}
		
		//判断头部中cmd的类型
		switch (header.cmd)
		{
			case CMD_LOGIN: //如果是登录
				{
					//接收消息体
					Login login = {};
					recv(_cSock, (char*)&login + sizeof(DataHeader), sizeof(login) - sizeof(DataHeader), 0);
					std::cout << "收到命令:CMD_LOGIN,用户名:" << login.userName << ",密码:" << login.PassWord << std::endl;

					//此处可以判断用户账户和密码是否正确等等(省略)

					//返回登录的结果给客户端
					LoginResult ret;
					send(_cSock, (const char*)&ret, sizeof(ret), 0);
				}	
				break;
			case CMD_LOGOUT:  //如果是退出
				{
					//接收消息体
					Logout logout = {};
					recv(_cSock, (char*)&logout + sizeof(DataHeader), sizeof(logout) - sizeof(DataHeader), 0);
					std::cout << "收到命令:CMD_LOGOUT,用户名:" << logout.userName << std::endl;

					//返回退出的结果给客户端
					LogoutResult ret;
					send(_cSock, (const char*)&ret, sizeof(ret), 0);
				}
				break;
			default:  //如果有错误
				header.cmd = CMD_ERROR;
				header.dataLength = 0;
				send(_cSock, (const char*)&header, sizeof(header), 0);
				break;
		}
	}
	
	//关闭服务端套接字
	closesocket(_sock);
	WSACleanup();

	std::cout << "服务端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

四、客户端代码如下

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS //for inet_pton()

#include <windows.h>
#include <WinSock2.h>
#include <iostream>
#include <string.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

using namespace std;

enum CMD
{
	CMD_LOGIN,       
	CMD_LOGIN_RESULT, 
	CMD_LOGOUT,      
	CMD_LOGOUT_RESULT,
	CMD_ERROR   
};

struct DataHeader
{
	short cmd;       
	short dataLength;
};

struct Login :public DataHeader
{
	Login() {
		cmd = CMD_LOGIN;
		dataLength = sizeof(Login);
	}
	char userName[32]; 
	char PassWord[32]; 
};

struct LoginResult :public DataHeader
{
	LoginResult() :result(0) {
		cmd = CMD_LOGIN_RESULT;
		dataLength = sizeof(LoginResult);
	}
	int result;
};

struct Logout :public DataHeader
{
	Logout() {
		cmd = CMD_LOGOUT;
		dataLength = sizeof(Logout);
	}
	char userName[32];
};

struct LogoutResult :public DataHeader
{
	LogoutResult():result(0){
		cmd = CMD_LOGOUT_RESULT;
		dataLength = sizeof(LogoutResult);
	}
	int result;
};

int main()
{
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	//建立socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == _sock) {
		std::cout << "ERROR:建立socket失败!" << std::endl;
	}
	else {
		std::cout << "建立socket成功!" << std::endl;
	}

	//声明要连接的服务端地址
	struct sockaddr_in _sin = {};
	_sin.sin_addr.S_un.S_addr = inet_addr("192.168.0.104");
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);

	//连接服务端
	int ret = connect(_sock, (struct sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret) {
		std::cout << "ERROR:连接服务端失败!" << std::endl;
	}
	else {
		std::cout << "连接服务端成功!" << std::endl;
	}

	char cmdBuf[256] = {};
	while (true)
	{
		//输入数据
		std::cout << "cmd#:";
		std::cin >> cmdBuf;

		//判断用户输入的命令类型,并且发送响应的数据
		if (0 == strcmp(cmdBuf, "exit")) {
			break;
		}
		else if (0 == strcmp(cmdBuf, "login")) {
			//先发送登录数据
			Login login;
			strcpy(login.userName, "dongshao");
			strcpy(login.PassWord, "123456");
			send(_sock, (const char*)&login, sizeof(login), 0);

			//然后接收服务端返回的登录确认数据
			LoginResult loginRet = {};
			recv(_sock, (char*)&loginRet, sizeof(loginRet), 0);
			std::cout << "LoginResult:" << loginRet.result << std::endl;
		}
		else if (0 == strcmp(cmdBuf, "logout")) {
			//先发送退出数据
			Logout logout;
			strcpy(logout.userName, "dongshao");
			send(_sock, (const char*)&logout, sizeof(logout), 0);

			//然后接收服务端返回的退出确认数据
			LogoutResult logoutRet = {};
			recv(_sock, (char*)&logoutRet, sizeof(logoutRet), 0);
			std::cout << "LogoutResult:" << logoutRet.result << std::endl;
		}
		else {
			std::cout << "不支持的命令,请重新输入!" << std::endl;
			continue;
		}
	}

	//关闭服务端套接字
	closesocket(_sock);
	WSACleanup();

	std::cout << "客户端停止工作!" << std::endl;
	getchar();  //防止程序一闪而过
	return 0;
}

五、演示效果

  • 启动服务端

  • 启动客户端,可以看到服务端接收到请求

  • 客户端输入相应的命令

六、扩展:关于数据包的拆分与合并等问题

  • 在网络传输中会经常遇到网络数据包的拆包与黏包的问题,这些问题我们会在后面的数据传输高级部分进行讲解,此处只是简单的进行一个介绍
  • 在服务端中我们需要先获取客户端发送的信息的类型(login的还是logout的),因此我们可以将客户端发送的数据所有存放到一个缓冲区中,然后从中获取其DataHeader部分,然后判断信息的类型,再进一步获取数据包的实体部分,从实体部分获取更多详细的信息

服务端代码修改如下

  • 我们将服务端代码中的while循环修改为如下所示的样子,其余代码不变
  • 我们先把客户端发来的数据存放到一个缓冲区中,然后将数据进行拆分处理,先获取DataHeader部分的数据,然后根据头部信息中的cmd字段再进一步获取实体部分的数据,之后从实体部分的数据中获取相关信息
//循环处理客户端的数据
	while (true)
	{
		//设置接收缓冲区,并接收命令
		char szRecv[1024];
        //先接收DataHeader大小的数据,实体部分的数据留到下一次recv函数进行接收
		int _nLen = recv(_cSock, szRecv, sizeof(DataHeader), 0);
		if (_nLen <= 0) {
			std::cout << "客户端已退出" << std::endl;
			break;
		}

		//在此处还应该判断少包黏包的问题,但是现在处于单机处理状态,后面介绍到复杂的消息通信时再介绍

		//获取消息中头部中的信息
		DataHeader* header = (DataHeader*)szRecv;
		switch (header->cmd)
		{
			case CMD_LOGIN: //如果是登录
				{
					//接收消息体
					recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
					Login *login = (Login*)szRecv;
					std::cout << "收到命令:CMD_LOGIN,用户名:" << login->userName << ",密码:" << login->PassWord << std::endl;

					//此处可以判断用户账户和密码是否正确等等(省略)

					//返回登录的结果给客户端
					LoginResult ret;
					send(_cSock, (const char*)&ret, sizeof(ret), 0);
				}	
				break;
			case CMD_LOGOUT:  //如果是退出
				{
					//接收消息体
					recv(_cSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
					Logout *logout = (Logout*)szRecv;
					std::cout << "收到命令:CMD_LOGOUT,用户名:" << logout->userName << std::endl;

					//返回退出的结果给客户端
					LogoutResult ret;
					send(_cSock, (const char*)&ret, sizeof(ret), 0);
				}
				break;
			default:  //如果有错误
				{
					DataHeader header = { CMD_ERROR,0 };
					send(_cSock, (const char*)&header, sizeof(header), 0);
				}
				break;
		}
	}
  • 重新编译服务端代码,运行结果与之前的一致 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值