2-6 socket客户端、服务端以【报文方式】收发数据

socket客户端、服务端以【报文方式】收发数据

0-前言

【C++百万并发网络通信】系列是跟着【张远东】老师的视频来复现的

希望能通过博客的方式不断坚持学习,也希望偶然间看到这篇博客的你也能一起加油!

笔记目录:【C++百万并发网络通信-笔记目录】

1 - 网络数据报文的格式

报文 = 包头 + 包体

包头:表示消息大小

包体:携带数据

我们用结构体来表示包头

//包头
struct DataHeader {
    
    
	short dataLen;//数据长度
	short cmd;//命令
};

用户命令分为两个:loginlogout。对应的结构体分别为LoginLogout。除此之外,服务端接收到这两种命令后还要返回相应的结果LoginResLogoutRes,结构体定义如下所示:

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

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

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

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

【分析】:结构体实现继承(如Login继承DataHeader)是为了发送一体化的报文,将数据包的包头和包体整合起来,避免了繁琐的顺序发送(先发包头,再发包体,接收端也要先接收包头,再接受包体)

2 - 客户端程序

下面是client的完整程序:

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

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

using namespace std;
#pragma comment(lib, "ws2_32.lib")//加入静态链接库

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

//包头
struct DataHeader {
    
    
	short dataLen;//数据长度
	short cmd;//命令
};

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

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

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

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

int main() {
    
    
	WORD ver = MAKEWORD(2, 2);//WORD版本号
	WSADATA dat;//一种数据结构
	//启动windows socket 2.x环境
	WSAStartup(ver, &dat);
	//-------------------
	//--建立简易TCP客户端
	// 1 建立socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);//0:不规定协议类型
	if (INVALID_SOCKET == _sock)
		cout << "Error:建立Socket失败!" << endl;
	else
		cout << "建立Socket成功..." << endl;

	// 2 连接服务器 connect
	sockaddr_in _sin = {
    
    }; //能够将结构体快速初始化
	_sin.sin_family = AF_INET;
	_sin.sin_port = htons(4567);//客户端想要连接服务器的哪个端口
	//连接服务器的ip地址,127.0.0.1是本机地址
	_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	int res = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == res)
		cout << "Error:连接失败!" << endl;
	else
		cout << "连接成功..." << endl;

	while (true) {
    
    
		//3 输入请求命令
		char cmdBuf[128] = {
    
    };
		cin >> cmdBuf;
		//4 处理请求
		if (0 == strcmp(cmdBuf, "exit")) {
    
    
			cout << "收到exit命令,任务结束..." << endl;
			break;
		}
		else if (0 == strcmp(cmdBuf, "login")) {
    
    
			//5 向服务器发送数据包
			Login login;
			strcpy_s(login.userName, "cze");
			strcpy_s(login.passWord, "czemm");
			send(_sock, (const char*)&login, sizeof(Login), 0);
			//6 接收服务器信息 recv
			LoginRes loginRet = {
    
    };
			recv(_sock, (char*)&loginRet, sizeof(LoginRes), 0);
			cout << "LoginResult: " << loginRet.result << endl;
		}
		else if (0 == strcmp(cmdBuf, "logout")) {
    
    
			//5 向服务器发送数据包
			Logout logout;
			strcpy_s(logout.userName, "cze");
			send(_sock, (const char*)&logout, sizeof(Logout), 0);
			//6 接收服务器信息 recv
			LogoutRes logoutRet = {
    
    };
			recv(_sock, (char*)&logoutRet, sizeof(LogoutRes), 0);
			cout << "LoginoutResult: " << logoutRet.result << endl;
		}
		else {
    
    
			cout << "不支持的命令,请重新输入。" << endl;
		}
	}

	// 7 关闭socket closesocket
	closesocket(_sock);
	//-------------------
	//清除Windows socket环境
	WSACleanup();//关闭windows socket网络环境
	cout << "已退出。" << endl;
	system("pause");
	return 0;
}

【分析】:client的主要改动就在于(以发送login为例):循环发送login命令时,将信息都封装在Login类的实例中,注意字符串以strcpy()拷贝,但是VS2019提示我这个函数不安全,让我用strcpy_s()函数

strcpy()strcpy_s()的主要差别看这里:

https://blog.csdn.net/leowinbow/article/details/82380252

3 - 服务端程序

下面直接上server完整程序:

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS

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

using namespace std;

#pragma comment(lib, "ws2_32.lib")//加入静态链接库

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

//包头
struct DataHeader {
    
    
	short dataLen;//数据长度
	short cmd;//命令
};

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

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

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

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

int main() {
    
    
	WORD ver = MAKEWORD(2, 2);//WORD版本号
	WSADATA dat;//一种数据结构
	//启动windows socket 2.x环境
	WSAStartup(ver, &dat);
	//-------------------

	//--建立简易TCP服务端
	// 1 建立socket
	SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	// 2 绑定端口 bind
	sockaddr_in _sin = {
    
    };//网络端口地址
	_sin.sin_family = AF_INET;
	//host to net unsigned short,服务器使用该端口进行监听
	_sin.sin_port = htons(4567);
	//随意ip地址//inet_addr("127.0.0.1");//本机地址,防止外网访问
	_sin.sin_addr.S_un.S_addr = INADDR_ANY;
	if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin))) {
    
    
		cout << "Error:绑定用于接收客户端连接的网络端口失败" << endl;
	}
	else {
    
    
		cout << "Success:绑定网络端口成功..." << endl;
	}

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

	// 4 等待客户端连接 accept
	sockaddr_in clientAddr = {
    
    };//远程客户端地址
	int nAddrLen = sizeof(clientAddr);//结构长度
	SOCKET _csock = INVALID_SOCKET;//无效的socket地址

	_csock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);//新加入的客户端socket
	if (INVALID_SOCKET == _csock) {
    
    
		cout << "Error:接收到无效客户端socket..." << endl;
	}
	cout << "新客户端加入:socket = " << (int)_csock << " , IP = " << inet_ntoa(clientAddr.sin_addr) << endl;

	while (true) {
    
    
		//5 接收客户端数据-数据头
		DataHeader header = {
    
    };
		int nLen = recv(_csock, (char*)&header, sizeof(DataHeader), 0);//返回客户端发送的数据长度
		if (nLen <= 0) {
    
    
			cout << "客户端已退出,任务结束!" << endl;
			break;
		}

		//6 处理客户端请求,按照请求向客户端发送数据
		switch (header.cmd) {
    
    
		case CMD_LOGIN: {
    
    
			Login login = {
    
    };
			recv(_csock, (char*)&login + sizeof(DataHeader), sizeof(Login) - sizeof(DataHeader), 0);
			cout << "收到命令:CMD_LOGIN"
				<< "  数据长度:" << login.dataLen
				<< " 用户名 = " << login.userName
				<< " 密码 = " << login.passWord << endl;
			//忽略判断用户名、密码
			LoginRes ret;
			send(_csock, (char*)&ret, sizeof(LoginRes), 0);
		} break;
		case CMD_LOGOUT: {
    
    
			Logout logout = {
    
    };
			recv(_csock, (char*)&logout + sizeof(DataHeader), sizeof(Logout) - sizeof(DataHeader), 0);
			cout << "收到命令:CMD_LOGOUT"
				<< "  数据长度:" << logout.dataLen
				<< " 用户名 = " << logout.userName << endl;
			//忽略判断用户名、密码
			LogoutRes ret;
			send(_csock, (char*)&ret, sizeof(LogoutRes), 0);
		} break;
		default:
			header.cmd = CMD_ERROR;
			header.dataLen = 0;
			send(_csock, (char*)&header, sizeof(DataHeader), 0);
		}
	}

	// 7 关闭socket closesocket
	closesocket(_sock);
	//-------------------
	//清除Windows socket环境
	WSACleanup();//关闭windows socket网络环境
	cout << "已退出,任务结束..." << endl;
	system("pause");
	return 0;
}

【分析】:主要的改动就在于循环接收(还是以接收login为例)。注意while循环刚开始,我们先读了一个包头,那么接下来switch语句再进行接受的时候,要从刚才包头的结束位置开始,这个位置仍在login对象内。这是很重要的,如果下面这一句改成这样:

switch (header.cmd) {
    
    
		case CMD_LOGIN: {
    
    
			Login login = {
    
    };
			recv(_csock, (char*)&login + sizeof(DataHeader), sizeof(Login) - sizeof(DataHeader), 0);
            ...
如果变成:
switch (header.cmd) {
    
    
		case CMD_LOGIN: {
    
    
			Login login = {
    
    };
			recv(_csock, (char*)&login, sizeof(Login), 0);
会导致接收数据的login对象在原本包头的位置存上了包体

recv(_csock, (char*)&login + sizeof(DataHeader), sizeof(Login) - sizeof(DataHeader), 0);这句代码里面,recv函数第二个参数是要将当前接收到的数据存放在login对象从sizeof(DataHeader)开始的位置,第三个参数是本次将要接受的数据的大小。这样做的目的就是将数据放回它发送时的位置上。

猜你喜欢

转载自blog.csdn.net/weixin_44484715/article/details/115271443
2-6
今日推荐