六、Socket网络通信基础(三):网络报文消息数据收发

前言

在上一篇中Socket网络通信基础(二):结构化的网络消息数据收发
我们将消息收发优化成结构化的网络消息
接下来,我们将使用网络报文来进行消息的收发

  • 本文优化内容
    • 使用网络报文来进行消息收发
    • 统一server和client的消息结构

一、网络数据报文格式

1、为什么要使用网络报文

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

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

  • 报文有两个部分,包头和包体,是网络消息的基本单元
  • 包头:描述本次消息包的大小,描述数据的作用
  • 包体:数据
//消息头
struct DataHeader
{
	short dataLength;//数据长度
	short cmd;//命令
};

二、优化内容

1、增加枚举定义:登录、登录结果、登出、登出结果、错误

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

2、消息头定义

struct DataHeader
{
	short dataLength;//数据长度
	short cmd;//命令
};

3、消息定义:登录、登录结果、登出、登出结果

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

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

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

4、server中循环处理消息

	while (true)
	{
		char szRecv[1024] = {};
		int nLen = recv(sock_client, szRecv, sizeof(DataHeader), 0);
		if (nLen <= 0)
		{
			printf("客户端已退出,任务结束。\n");
			break;
		}
		DataHeader* header = (DataHeader*)szRecv;
		switch (header->cmd)
		{
		case CMD_LOGIN:
		{
			recv(sock_client, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Login* login = (Login*)szRecv;
			printf("收到命令:CMD_LOGIN 数据长度:%d userName=%s passWord=%s\n", login->dataLength, login->userName, login->passWord);
			LoginResult ret;
			send(sock_client, (char*)&ret, sizeof(LoginResult), 0);
		}
		break;
		case CMD_LOGOUT:
		{
			recv(sock_client, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Logout* logout = (Logout*)szRecv;
			printf("收到命令:CMD_LOOUT 数据长度:%d userName=%s\n", logout->dataLength, logout->userName);
			//忽略判断用户密码是否正确的过程
			LogoutResult ret;
			send(sock_client, (char*)&ret, sizeof(LogoutResult), 0);
		}
		break;
		default:
		{
			DataHeader header = { 0,CMD_ERROR };
			send(sock_client, (char*)&header, sizeof(header), 0);
		}
		break;
		}
	}

5、client中消息的处理

	while (true)
	{
		char cmdBuf[128] = {};
		scanf("%s", cmdBuf);
		if (0 == strcmp(cmdBuf, "exit"))
		{
			printf("收到exit命令,任务结束。\n");
			break;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login login;
			strcpy(login.userName, "lyd");
			strcpy(login.passWord, "lydmima");
			send(sock_client, (const char*)&login, sizeof(login), 0);
			LoginResult loginRet = {};
			recv(sock_client, (char*)&loginRet, sizeof(loginRet), 0);
			printf("LoginResult: %d \n", loginRet.result);
		}
		else if (0 == strcmp(cmdBuf, "logout"))
		{
			Logout logout;
			strcpy(logout.userName, "lyd");
			send(sock_client, (const char*)&logout, sizeof(logout), 0);
			LogoutResult logoutRet = {};
			recv(sock_client, (char*)&logoutRet, sizeof(logoutRet), 0);
			printf("LogoutResult: %d \n", logoutRet.result);
		}
		else
		{
			printf("不支持的命令,请重新输入。\n");
		}
	}

三、关于数据包的拆分与合并等问题

在网络传输中会经常遇到网络数据包的拆包与黏包的问题

  • 在服务端中我们需要先获取客户端发送的信息的类型(login的还是logout的)
  • 因此我们可以将客户端发送的数据所有存放到一个缓冲区中
  • 然后从中获取其DataHeader部分,然后判断信息的类型
  • 再进一步获取数据包的实体部分,从实体部分获取更多详细的信息

四、关于server中消息接收的长度

  • 首先:int nLen = recv(sock_client, szRecv, sizeof(DataHeader), 0);server从缓冲区接收了DataHeader长度的消息
  • 这时候只是为了取出DataHeader中的cmd命令类型
  • 然后根据不同的命令类型recv(sock_client, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
  • 以login消息举例:szRecv是指向缓冲区的指针
    • 第一次recv已经接收了DataHeader的长度
    • 第二次recv则需要从szRecv+sizeof(DataHeader)开始接收
    • 第二次接收的长度就是总的header->dataLength再减去sizeof(DataHeader)的长度
      在这里插入图片描述

五、测试结果

在这里插入图片描述

六、完整源码

1、服务端server.cpp

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <Windows.h>
#include <WinSock2.h>
#include<stdio.h>

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

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

//消息头
struct DataHeader
{
	short dataLength;//数据长度
	short cmd;//命令
};

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

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

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

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

	SOCKET sock_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == sock_server)
	{
		printf("socket 创建失败\n");
		WSACleanup();
		return -1;
	}

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	_sin.sin_addr.S_un.S_addr = INADDR_ANY;// INADDR_ANY:这个代表本机的地址都可以访问,如IPV4,IPV6
	//_sin.sin_addr.s_addr = inet_addr("127.0.0.1");// 指定地址的绑定

	if (SOCKET_ERROR == bind(sock_server, (sockaddr*)&_sin, sizeof(_sin)))
	{
		printf("ERROR,绑定网络端口失败...\n");
		closesocket(sock_server);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("绑定网络端口成功...\n");
	}
	if (SOCKET_ERROR == listen(sock_server, 5))
	{
		printf("ERROR,监听网络端口失败...\n");
		closesocket(sock_server);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("监听网络端口成功...\n");
	}

	sockaddr_in clientAddr = {};
	int nAddrLen = sizeof(clientAddr);
	SOCKET sock_client = INVALID_SOCKET;

	sock_client = accept(sock_server, (sockaddr*)&clientAddr, &nAddrLen);
	if (INVALID_SOCKET == sock_client)
	{
		printf("错误,接受到无效客户端SOCKET...\n");
		closesocket(sock_server);
		WSACleanup();
		return -1;
	}
	printf("新客户端加入:socket = %d, IP = %s \n", (int)sock_client, inet_ntoa(clientAddr.sin_addr));

	while (true)
	{
		char szRecv[1024] = {};
		int nLen = recv(sock_client, szRecv, sizeof(DataHeader), 0);
		if (nLen <= 0)
		{
			printf("客户端已退出,任务结束。\n");
			break;
		}
		DataHeader* header = (DataHeader*)szRecv;
		switch (header->cmd)
		{
		case CMD_LOGIN:
		{
			recv(sock_client, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Login* login = (Login*)szRecv;
			printf("收到命令:CMD_LOGIN 数据长度:%d userName=%s passWord=%s\n", login->dataLength, login->userName, login->passWord);
			LoginResult ret;
			send(sock_client, (char*)&ret, sizeof(LoginResult), 0);
		}
		break;
		case CMD_LOGOUT:
		{
			recv(sock_client, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
			Logout* logout = (Logout*)szRecv;
			printf("收到命令:CMD_LOOUT 数据长度:%d userName=%s\n", logout->dataLength, logout->userName);
			//忽略判断用户密码是否正确的过程
			LogoutResult ret;
			send(sock_client, (char*)&ret, sizeof(LogoutResult), 0);
		}
		break;
		default:
		{
			DataHeader header = { 0,CMD_ERROR };
			send(sock_client, (char*)&header, sizeof(header), 0);
		}
		break;
		}
	}

	closesocket(sock_server);
	WSACleanup();
	printf("已退出,任务结束。\n");
	getchar();
	return 0;
}

2、客户端client.cpp

#define _CRT_SECURE_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS


#include <Windows.h>
#include <WinSock2.h>
#include<stdio.h>

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

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

//消息头
struct DataHeader
{
	short dataLength;//数据长度
	short cmd;//命令
};

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

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

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

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

	SOCKET sock_client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == sock_client)
	{
		printf("socket 创建失败\n");
		WSACleanup();
		return -1;
	}

	sockaddr_in _sin = {};
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);
	_sin.sin_addr.s_addr = inet_addr("127.0.0.1");

	int ret = connect(sock_client, (sockaddr*)&_sin, sizeof(_sin));
	if (SOCKET_ERROR == ret)
	{
		printf("错误,连接服务器失败...\n");
		closesocket(sock_client);
		WSACleanup();
		return -1;
	}
	else
	{
		printf("连接服务器成功...\n");
	}

	while (true)
	{
		char cmdBuf[128] = {};
		scanf("%s", cmdBuf);
		if (0 == strcmp(cmdBuf, "exit"))
		{
			printf("收到exit命令,任务结束。\n");
			break;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login login;
			strcpy(login.userName, "lyd");
			strcpy(login.passWord, "lydmima");
			send(sock_client, (const char*)&login, sizeof(login), 0);
			LoginResult loginRet = {};
			recv(sock_client, (char*)&loginRet, sizeof(loginRet), 0);
			printf("LoginResult: %d \n", loginRet.result);
		}
		else if (0 == strcmp(cmdBuf, "logout"))
		{
			Logout logout;
			strcpy(logout.userName, "lyd");
			send(sock_client, (const char*)&logout, sizeof(logout), 0);
			LogoutResult logoutRet = {};
			recv(sock_client, (char*)&logoutRet, sizeof(logoutRet), 0);
			printf("LogoutResult: %d \n", logoutRet.result);
		}
		else
		{
			printf("不支持的命令,请重新输入。\n");
		}
	}

	closesocket(sock_client);
	WSACleanup();
	printf("已退出,任务结束。\n");
	getchar();
	return 0;
}
  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无休止符

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

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

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

打赏作者

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

抵扣说明:

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

余额充值