上节服务端与客户端1.1——持续处理“单个客户端”发送的请求_贪睡的蜗牛的博客-CSDN博客
目录
使用结构体进行传递数据1.21
使用网络数据报文(增加数据头)1.22
服务端修改
客户端修改
网络数据报文一次发送1.23
客户端代码
服务器修改后代码
使用dataLength来明确具体需要接收多少1.24
服务端修改后的代码
上节是使用字符串的形式充当的请求命令,使用getName获得名字,使用getAge获得年龄,其他命令统一用一个信息进行回复。本节将改进字符串的消息传递方式, 构建结构化的网络消息,使网络传输功能更复杂。这里我们使用结构体,一次性的将数据全部传递
使用结构体进行传递数据1.21
一定要保证服务端和客户端(操作系统)中
- 数据结构字节序和大小保证一致
- 内存对齐(要求结构体里的变量的相对顺序是一样的)
添加结构体
服务端和客户端统一添加同一个结构体,然后交互时传递的是这个结构体
struct DataPackage
{
int age;
char name[32];
};
服务端,将getAge和getName统一命令为getInfo
if (0 == strcmp(_recvBuf, "getInfo"))
{
DataPackage dp = { 24,"Evila" };
send(_clientSock, (const char*)&dp, sizeof(DataPackage), 0);
}
客户端,将返回数据后进行修改
//6. 接受服务器信息 recv
char recvBuf[256] = {};
int nlen = recv(_sock, recvBuf, sizeof(recvBuf), 0); //返回值是接收数据的长度
if (nlen > 0)
{
DataPackage* pDP = (DataPackage*)recvBuf;
cout << "接收到数据: 年龄=" << pDP->age << ",名字=" << pDP->name << endl;
}
注意,这样将会导致如果不是返回的DataPackage结构体,将会出现问题
使用网络数据报文(增加数据头)1.22
本节是先传递数据头,然后再传递结构体
报文有两个部分,包头和包体,是网络消息的基本单元
包头:描述本次消息包的大小(比如固定前4个字节表示报文的大小)
包体:数据内容
传输数据时,客户端发送请求应先发送 包头 再发送包体; 服务器端接收请求时,先接收请求头,再接收请求体;同样的,服务器端返回时和客户端接收时。
这里枚举下命令,添加消息头结构体,添加登录、退出登录、登录结果,退出登录结果四个结构体
下列枚举,从0开始的
enum CMD //消息枚举
{
CMD_LOGIN,
CMD_LOGINOUT,
CMD_ERROR
};
//消息头
struct DataHeader
{
short dataLength; //数据长度 32767字节
short cmd;
};
struct Login
{
char userName[32];
char Password[32];
};
struct Logout
{
char userName[32];
};
struct LoginResult
{
int result;
};
struct LogoutResult
{
int result;
};
服务端修改
服务端在收到DataHeader(里面包含cmd),判断cmd是啥,然后做出处理,先返回一个头,再返回处理结果(这里的头用客户端请求来的头)
#define WIN32_LEAN_AND_MEAN #define _WINSOCK_DEPRECATED_NO_WARNINGS #include#include #include #include using namespace std; #pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库 enum CMD //消息枚举 { CMD_LOGIN, CMD_LOGINOUT, CMD_ERROR }; //消息头 struct DataHeader { short dataLength; //数据长度 32767字节 short cmd; }; struct Login { char userName[32]; char Password[32]; }; struct Logout { char userName[32]; }; struct LoginResult { int result; }; struct LogoutResult { int result; }; int main() { //启动 windows socket 2.x 环境 WORD versionCode = MAKEWORd(2, 2); //创建一个版本号 WSADATA data; WSAStartup(versionCode, &data); //启动Socket网络API的函数 /// //(1) 用Socket API建立简易的TCP服务端 // 1. 建立一个Socket SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议 // 2. 绑定接受客户端连接的端口 bind sockaddr_in _sin = {}; _sin.sin_family = AF_INET; _sin.sin_port = htons(4567); //端口号 host to net sockaddr_in中port是USHORT类型 //网络中port是 unsigend short类型 因此需要Htons进行转换 //_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器绑定的IP地址 127.0.0.1是本地地址 _sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR) //sockaddr 不利于编码 { cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl; } else { cout << "SUCCESS: 绑定端口成功..." << endl; } // 3. 监听网络端口 listen if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量 { cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl; } else { cout << "SUCCESS: 监听端口成功..." << endl; } // 4. 等待接受客户端连接 accept sockaddr_in _clientAddr = {}; int cliendAddrLen = sizeof(_clientAddr); SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端 _clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen); while (true) { DataHeader header = {}; //5 首先接收数据包头 int nlen = recv(_clientSock, (char*)&header, sizeof(header), 0); //接受客户端的数据 第一个参数应该是客户端的socket对象 if (nlen <= 0) { //客户端退出 cout << "客户端已退出,任务结束" << endl; break; } cout << "收到命令:" << header.cmd << "数据长度" << header.dataLength << endl; switch (header.cmd) { case CMD_LOGIN: { Login _login = {}; recv(_clientSock, (char*)&_login, sizeof(Login), 0); //忽略判断用户名密码是否正确的过程 LoginResult _loginres = { 1 }; send(_clientSock, (char*)&header, sizeof(DataHeader), 0); send(_clientSock, (char*)&_loginres, sizeof(LoginResult), 0); cout << "登陆用户: " << _login.userName << endl; }break; case CMD_LOGINOUT: { Logout _logout = {}; recv(_clientSock, (char*)&_logout, sizeof(Logout), 0); LogoutResult _logoutres = { 1 }; send(_clientSock, (char*)&header, sizeof(DataHeader), 0); send(_clientSock, (char*)&_logoutres, sizeof(LogoutResult), 0); cout << "登出用户: " << _logout.userName << endl; }break; default: header.cmd = CMD_ERROR; header.dataLength = 0; send(_clientSock, (char*)&header, sizeof(DataHeader), 0); break; } } // 6. 关闭socket closesocket(_sock); // 清除Windows socket环境 WSACleanup(); return 0; }
客户端修改
客户端先两次发送,再两次接收,两次分别都是包头和具体内容
#define WIN32_LEAN_AND_MEAN #define _WINSOCK_DEPRECATED_NO_WARNINGS #include#include #include #include using namespace std; #pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库 enum CMD //消息枚举 { CMD_LOGIN, CMD_LOGINOUT, CMD_ERROR }; //消息头 struct DataHeader { short dataLength; //数据长度 32767字节 short cmd; }; struct Login { char userName[32]; char Password[32]; }; struct Logout { char userName[32]; }; struct LoginResult { int result; }; struct LogoutResult { int result; }; int main() { //启动 windows socket 2.x 环境 WORD versionCode = MAKEWORd(2, 2); //创建一个版本号 WSADATA data; WSAStartup(versionCode, &data); //启动Socket网络API的函数 /// //(1) 用Socket API建立简易的TCP客户端 // 1. 建立一个Socket 下面第三个参数不需要指定 SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 面向字节流的 tcp协议 if (INVALID_SOCKET == _sock) { cout << "错误,建立socket失败"< > cmdBuf; // 4 处理请求 if (strcmp(cmdBuf, "exit") == 0) { break; } else if (0 == strcmp(cmdBuf, "login")) { Login _login = { "Evila","Evila_Password" }; DataHeader _header = {}; _header.dataLength = sizeof(Login); _header.cmd = CMD_LOGIN; // 5 向服务器发送请求命令 先发送包头 send(_sock, (const char*)&_header, sizeof(DataHeader), 0); //再发送包体 send(_sock, (const char*)&_login, sizeof(Login), 0); //6. 接受服务器信息 recv //先接收 返回数据的包头 DataHeader returnHeader = {}; LoginResult _lgRes = {}; recv(_sock, (char*)&returnHeader, sizeof(DataHeader), 0); //再接收 返回数据的包体 recv(_sock, (char*)&_lgRes, sizeof(LoginResult), 0); cout << "LoginResult: " << _lgRes.result << endl; } else if (0 == strcmp(cmdBuf, "logout")) { Logout _logout = { "Evila" }; DataHeader _header = {}; _header.dataLength = sizeof(Logout); _header.cmd = CMD_LOGINOUT; // 5 向服务器发送请求命令 先发送包头 send(_sock, (const char*)&_header, sizeof(DataHeader), 0); //再发送包体 send(_sock, (const char*)&_logout, sizeof(Logout), 0); //6. 接受服务器信息 recv //先接收 返回数据的包头 DataHeader returnHeader = {}; LogoutResult _lgRes = {}; recv(_sock, (char*)&returnHeader, sizeof(DataHeader), 0); //再接收 返回数据的包体 recv(_sock, (char*)&_lgRes, sizeof(LogoutResult), 0); cout << "LogoutResult: " << _lgRes.result << endl; } else { cout << "不支持的命令,请重新输入。" << endl; } //放到循环中 重复接受服务器的返回信息 //6. 接受服务器信息 recv //char recvBuf[256] = {}; //int nlen = recv(_sock, recvBuf, sizeof(recvBuf), 0); //返回值是接收数据的长度 //if (nlen > 0) //{ // DataPackage* pDP = (DataPackage*)recvBuf; // cout << "接收到数据: 年龄=" << pDP->age << ",名字=" << pDP->name << endl; //} //else //{ // cout << "ERROR: 数据传输失败..." << endl; //} } // 6. 关闭socket closesocket(_sock); // 清除Windows socket环境 WSACleanup(); getchar();//防止一闪而过 return 0; }
网络数据报文一次发送1.23
本节是一次发送数据,大体流程是先接受请求头,根据请求头进入相应的逻辑处理,然后再接收剩下的内容
首先在服务端,客户端补充消息枚举
2、对每个消息添加继承消息头
3、注意下面内容,偏移量
客户端代码
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库
enum CMD //消息枚举
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGINOUT,
CMD_LOGOUT_RESULT,
CMD_ERROR
};
//消息头
struct DataHeader
{
short dataLength; //数据长度 32767字节
short cmd;
};
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char Password[32];
};
struct Logout :public DataHeader
{
Logout()
{
dataLength = sizeof(Logout);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginResult :public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
}
int result;
};
struct LogoutResult :public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LogoutResult);
cmd = CMD_LOGOUT_RESULT;
}
int result;
};
int main()
{
//启动 windows socket 2.x 环境
WORD versionCode = MAKEWORd(2, 2); //创建一个版本号
WSADATA data;
WSAStartup(versionCode, &data); //启动Socket网络API的函数
///
//(1) 用Socket API建立简易的TCP客户端
// 1. 建立一个Socket 下面第三个参数不需要指定
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0); // ipv4 面向字节流的 tcp协议
if (INVALID_SOCKET == _sock)
{
cout << "错误,建立socket失败"<> cmdBuf;
// 4 处理请求
if (strcmp(cmdBuf, "exit") == 0)
{
break;
}
else if (0 == strcmp(cmdBuf, "login"))
{
Login _login;
strcpy_s(_login.userName, "Evila");
strcpy_s(_login.Password, "Evila_Password");
// 5 向服务器发送请求命令
send(_sock, (const char*)&_login, sizeof(Login), 0);
//6. 接受服务器信息 recv
LoginResult _lgRes;
recv(_sock, (char*)&_lgRes, sizeof(LoginResult), 0);
cout << "LoginResult: " << _lgRes.result << endl;
}
else if (0 == strcmp(cmdBuf, "logout"))
{
Logout _logout;
strcpy_s(_logout.userName, "Evila");
// 5 向服务器发送请求命令
send(_sock, (const char*)&_logout, sizeof(Logout), 0);
//6. 接受服务器信息 recv
LogoutResult _lgRes;
//返回数据
recv(_sock, (char*)&_lgRes, sizeof(LogoutResult), 0);
cout << "LogoutResult: " << _lgRes.result << endl;
}
else
{
cout << "不支持的命令,请重新输入。" << endl;
}
}
// 6. 关闭socket
closesocket(_sock);
// 清除Windows socket环境
WSACleanup();
getchar();//防止一闪而过
return 0;
}
服务器修改后代码
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
using namespace std;
#pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库
enum CMD //消息枚举
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGINOUT,
CMD_LOGOUT_RESULT,
CMD_ERROR
};
//消息头
struct DataHeader
{
short dataLength; //数据长度 32767字节
short cmd;
};
struct Login : public DataHeader
{
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char Password[32];
};
struct Logout :public DataHeader
{
Logout()
{
dataLength = sizeof(Logout);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginResult :public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
}
int result;
};
struct LogoutResult :public DataHeader
{
LogoutResult()
{
dataLength = sizeof(LogoutResult);
cmd = CMD_LOGOUT_RESULT;
}
int result;
};
int main()
{
//启动 windows socket 2.x 环境
WORD versionCode = MAKEWORd(2, 2); //创建一个版本号
WSADATA data;
WSAStartup(versionCode, &data); //启动Socket网络API的函数
///
//(1) 用Socket API建立简易的TCP服务端
// 1. 建立一个Socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议
// 2. 绑定接受客户端连接的端口 bind
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(4567); //端口号 host to net sockaddr_in中port是USHORT类型
//网络中port是 unsigend short类型 因此需要Htons进行转换
//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器绑定的IP地址 127.0.0.1是本地地址
_sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP
if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR) //sockaddr 不利于编码
{
cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl;
}
else
{
cout << "SUCCESS: 绑定端口成功..." << endl;
}
// 3. 监听网络端口 listen
if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量
{
cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl;
}
else
{
cout << "SUCCESS: 监听端口成功..." << endl;
}
// 4. 等待接受客户端连接 accept
sockaddr_in _clientAddr = {};
int cliendAddrLen = sizeof(_clientAddr);
SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端
_clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen);
while (true)
{
DataHeader header = {};
//5 首先接收数据包头
int nlen = recv(_clientSock, (char*)&header, sizeof(DataHeader), 0); //经过改进 包头信息已经继承到了包体中 按照内存对齐 首先读取sizeof(DataHeader)的字节序列
if (nlen <= 0)
{
//客户端退出
cout << "客户端已退出,任务结束" << endl;
break;
}
switch (header.cmd)
{
case CMD_LOGIN:
{
Login _login;
recv(_clientSock, (char*)&_login + sizeof(DataHeader), sizeof(Login) - sizeof(DataHeader), 0); //这里要注意 程序已经读取了Dataheader了,因此这里应该从_login + sizeof(DataHeader) 开始读取,读取的长度也需要剪掉包头的长度 实现传输的数据内存对齐
cout << "收到命令:CMD_LOGIN" << " 数据长度 = " << header.dataLength << " UserName = " << _login.userName << " Password = " << _login.Password << endl;
//忽略了判断用户名密码是否正确的过程
LoginResult _loginres;
send(_clientSock, (char*)&_loginres, sizeof(LoginResult), 0);
}break;
case CMD_LOGINOUT:
{
Logout _logout;
recv(_clientSock, (char*)&_logout + sizeof(DataHeader), sizeof(Logout) - sizeof(DataHeader), 0);
cout << "收到命令:CMD_LOGOUT" << " 数据长度 = " << header.dataLength << " UserName = " << _logout.userName << endl;
LogoutResult _logoutres;
send(_clientSock, (char*)&_logoutres, sizeof(LogoutResult), 0);
}break;
default:
header.cmd = CMD_ERROR;
header.dataLength = 0;
send(_clientSock, (char*)&header, sizeof(DataHeader), 0);
break;
}
}
// 6. 关闭socket
closesocket(_sock);
// 清除Windows socket环境
WSACleanup();
return 0;
}
使用dataLength来明确具体需要接收多少1.24
#define WIN32_LEAN_AND_MEAN #define _WINSOCK_DEPRECATED_NO_WARNINGS #include#include #include #include using namespace std; #pragma comment(lib,"ws2_32.lib") //windows socket2 32的lib库 enum CMD //消息枚举 { CMD_LOGIN, CMD_LOGIN_RESULT, CMD_LOGINOUT, CMD_LOGOUT_RESULT, CMD_ERROR }; //消息头 struct DataHeader { short dataLength; //数据长度 32767字节 short cmd; }; struct Login : public DataHeader { Login() { dataLength = sizeof(Login); cmd = CMD_LOGIN; } char userName[32]; char Password[32]; }; struct Logout :public DataHeader { Logout() { dataLength = sizeof(Logout); cmd = CMD_LOGINOUT; } char userName[32]; }; struct LoginResult :public DataHeader { LoginResult() { dataLength = sizeof(LoginResult); cmd = CMD_LOGIN_RESULT; } int result; }; struct LogoutResult :public DataHeader { LogoutResult() { dataLength = sizeof(LogoutResult); cmd = CMD_LOGOUT_RESULT; } int result; }; int main() { //启动 windows socket 2.x 环境 WORD versionCode = MAKEWORd(2, 2); //创建一个版本号 WSADATA data; WSAStartup(versionCode, &data); //启动Socket网络API的函数 /// //(1) 用Socket API建立简易的TCP服务端 // 1. 建立一个Socket SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // ipv4 面向字节流的 tcp协议 // 2. 绑定接受客户端连接的端口 bind sockaddr_in _sin = {}; _sin.sin_family = AF_INET; _sin.sin_port = htons(4567); //端口号 host to net sockaddr_in中port是USHORT类型 //网络中port是 unsigend short类型 因此需要Htons进行转换 //_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器绑定的IP地址 127.0.0.1是本地地址 _sin.sin_addr.S_un.S_addr = INADDR_ANY; //不限定访问该服务端的IP if (bind(_sock, (sockaddr*)&_sin, sizeof(_sin)) == SOCKET_ERROR) //sockaddr 不利于编码 { cout << "ERROR: 绑定用于接受客户端连接的网络端口失败..." << endl; } else { cout << "SUCCESS: 绑定端口成功..." << endl; } // 3. 监听网络端口 listen if (listen(_sock, 5) == SOCKET_ERROR)//第二个参数 backbag 最大允许连接数量 { cout << "ERROR: 监听用于接受客户端连接的网络端口失败..." << endl; } else { cout << "SUCCESS: 监听端口成功..." << endl; } // 4. 等待接受客户端连接 accept sockaddr_in _clientAddr = {}; int cliendAddrLen = sizeof(_clientAddr); SOCKET _clientSock = INVALID_SOCKET; // 初始化无效的socket 用来存储接入的客户端 _clientSock = accept(_sock, (sockaddr*)&_clientAddr, &cliendAddrLen); while (true) { DataHeader header = {}; //5 首先接收数据包头 int nlen = recv(_clientSock, (char*)&header, sizeof(DataHeader), 0); //经过改进 包头信息已经继承到了包体中 按照内存对齐 首先读取sizeof(DataHeader)的字节序列 if (nlen <= 0) { //客户端退出 cout << "客户端已退出,任务结束" << endl; break; } switch (header.cmd) { case CMD_LOGIN: { Login _login; recv(_clientSock, (char*)&_login + sizeof(DataHeader), sizeof(Login) - sizeof(DataHeader), 0); //这里要注意 程序已经读取了Dataheader了,因此这里应该从_login + sizeof(DataHeader) 开始读取,读取的长度也需要剪掉包头的长度 实现传输的数据内存对齐 cout << "收到命令:CMD_LOGIN" << " 数据长度 = " << header.dataLength << " UserName = " << _login.userName << " Password = " << _login.Password << endl; //忽略了判断用户名密码是否正确的过程 LoginResult _loginres; send(_clientSock, (char*)&_loginres, sizeof(LoginResult), 0); }break; case CMD_LOGINOUT: { Logout _logout; recv(_clientSock, (char*)&_logout + sizeof(DataHeader), sizeof(Logout) - sizeof(DataHeader), 0); cout << "收到命令:CMD_LOGOUT" << " 数据长度 = " << header.dataLength << " UserName = " << _logout.userName << endl; LogoutResult _logoutres; send(_clientSock, (char*)&_logoutres, sizeof(LogoutResult), 0); }break; default: header.cmd = CMD_ERROR; header.dataLength = 0; send(_clientSock, (char*)&header, sizeof(DataHeader), 0); break; } } // 6. 关闭socket closesocket(_sock); // 清除Windows socket环境 WSACleanup(); return 0; }
使用dataLength来明确具体需要接收多少1.24
dataLength的作用是告知收发文件的大小,本次改进通过使用dataLength提供的长度进行收发。
1、创建一个缓冲区首先只接收数据头
2、通过将数据头转换为标准数据头去判断请求的类型,然后进去相应的处理流程,继续将后续的数据读入缓冲区内



