client网络模块的开发和client与server端的部分联动调试

2024-08-24 11:20

本文主要是介绍client网络模块的开发和client与server端的部分联动调试,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

客户端网络模块的开发

我们需要先了解socket通信的流程

socket通信

server端的流程

client端的流程

对于closesocket()函数来说

closesocket()是用来关闭套接字的,将套接字的描述符从内存清除,并不是删除了那个套接字,只是切断了联系,所以我们如果重复调用,不closesocket()就会报错

创建网络模块类

我们依然采用的是单例模式

学会套用server端网络模块类的代码

添加一个ClientSocket类

对于这里面代码的修改

我们修改初始化代码

	bool InitSocket(const std::string& strIPAddress) {if (m_sock != INVALID_SOCKET) CloseSocket();m_sock = socket(PF_INET, SOCK_STREAM, 0);//TODO,校验if (m_sock == -1) return false;sockaddr_in serv_adr; //服务器地址memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = inet_addr(strIPAddress.c_str());serv_adr.sin_port = htons(9527);if (serv_adr.sin_addr.s_addr == INADDR_NONE) {AfxMessageBox("指定的ip地址,不存在");return false;}int ret = connect(m_sock, (sockaddr*)&serv_adr, sizeof(serv_adr));if (ret == -1) {AfxMessageBox("连接失败!!!重新连接");TRACE("连接失败,%d %s\r\n", WSAGetLastError(), GetErrInfo(WSAGetLastError()).c_str());return false;}return true;}

然后我们删除了accept类

然后我们客户端连接失败我们需要打印出连接失败的原因

WSAGetLastError()

使用 WSAGetLastError() 函数 来获得上一次的错误代码

返回值指出了该线程进行的上一次 Windows Sockets API 函数调用时的错误代码

WSAGetLastError()函数返回值表格,在下面文章里面

“WSAGetLastError()使用”讲解

然后我们需要一个可以将错误码格式化的函数

这个函数不用深究,记住这个模板以后直接用

std::string GetErrInfo(int wsaErrCode)
{std::string ret;LPVOID lpMsgBuf = NULL; //这个函数需要自己开辟缓冲区FormatMessage( //系统函数,把错误码格式化的函数FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER,NULL,wsaErrCode,MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),(LPTSTR)&lpMsgBuf, 0, NULL);ret = (char*)lpMsgBuf;LocalFree(lpMsgBuf); //Free()掉return ret; //把这个缓冲区地址返回出去
}

然后咱们需要编辑client界面

对于这个我们不用添加类,直接双击那个连接测试,就会自动生成一个类在Dlg文件中,因为我们一开始创建这个项目时候就给client图形化界面了

void CRemoteClientDlg::OnBnClickedBtnTest()
{ClientSocket *pClient = ClientSocket::getInstance();bool ret = pClient->InitSocket("127.0.0.1");//后续加返回值的处理if (!ret) {AfxMessageBox("网络初始化失败!!!");return;}CPacket pack(1981,NULL,0);ret = pClient->Send(pack);TRACE("Send ret %d\r\n",ret);int cmd  = pClient->DealCommand();TRACE("ack:%d\r\n",cmd);pClient->CloseSocket();
}

server端和client端联动调试

启动客户端时候会报一个错

添加上那个报错信息

我们需要加上处理包的while循环

            CserverSocket* pserver = CserverSocket::getInstance();int count = 0;if (pserver->InitSocket() == false) {MessageBox(NULL, _T("网络初始化异常未能成功初始化,请检查网络状"), _T("网络初始化异常未能成功初始化,请检查网络状态"), MB_OK | MB_ICONERROR);exit(0);}while (CserverSocket::getInstance() != NULL) { // 相当于while(true)if (pserver->AcceptClient() == false) {if (count >= 3) {MessageBox(NULL, _T("多次无法正常接入程序,结束程序"), _T("接入用户失败!"), MB_OK | MB_ICONERROR);exit(0);}MessageBox(NULL, _T("无法正常接入用户,自动重试"), _T("接入用户失败!"), MB_OK | MB_ICONERROR);count++;}TRACE("AcceptClient true");int ret = pserver->DealCommand(); //获取命令TRACE("DealCommand ret:%d", ret);if (ret > 0) {ret = ExcuteCommand(ret);if (ret != 0) {TRACE("执行命令失败%d ret = %d\r\n", pserver->GetPacket().sCmd, ret);}pserver->CloseClient(); //短连接}}

ExcuteCommand()

int ExcuteCommand(int nCmd)
{int ret;switch (nCmd) {case 1://查看磁盘分区ret = MakeDriveInfo();break;case 2: //查看指定目录下的文件ret = MakeDirectoryInfo();break;case 3: //打开文件ret = RunFile();break;case 4: //下载文件ret = DownloadFile();break;case 5://鼠标操作ret = MouseEvent();break;case 6://发送屏幕内容 ==>发送屏幕的截图ret = SendScreen();break;case 7://锁机上锁(网吧可以用上)ret = LockMC();break;case 8:ret = UnlockMC();//解锁break;case 1981:ret = TestConnect();break;}return ret;
}

case 1981:是我们用来测试包的

int TestConnect()
{CPacket pack(1981, NULL, 0);CserverSocket::getInstance()->Send(pack);return 0;}
//CRemoteClientDlg::OnBnClickedBtnTest()函数	CPacket pack(1981,NULL,0);ret = pClient->Send(pack);TRACE("Send ret %d\r\n",ret);int cmd  = pClient->DealCommand(); //这也仅仅是为了测试TRACE("ack:%d\r\n",cmd);pClient->CloseSocket();

设置双项目调试启动

然后将两个项目的操作地方设置为启动,也可以设置为不调试启动

然后我们使用TRACE来追踪调试信息

我们还需要注意一点 ,就是server端初始化socket(初始化自己的socket等别人连接,基本上是不用改变的),可以等到析构时候在closesocket掉,但是client端不一样,client端可能需要连接不同的server端,所以需要不断的Init,也就是需要不断的将套接字和server端的IP连接

所以server端的m_sock初始化可以放在构造函数里面,client端的m_sock初始化需要放在Init函数里面,同时需要在里面closesocket

你会发现就算终止了,但是这个socket并没有close

证据

再次运行一遍,然后单步

你会发现程序进入了closesocket()函数,代表m_sock并不是INVALID_SOCKET

在遥远的2002年,有程序员碰到了同样的问题

然后就是我们选择持久连接还是非持久连接

client是我们在操控,向服务器发命令很少(间隔几秒),每次都是一个包,所以client端对server端应该采用非持久的连接,也就是

在每次包发完都进行pClient->CloseSocket();关闭socket连接

但是我们client端会向server端请求下载文件,远程桌面之类的命令,我们肯定不止要接收一个包,所以server端对client端要采用长连接

这里面我们需要注意野指针引起的内存泄漏问题

野指针引起的内存泄漏

内存泄漏是指我们在堆中申请(new/malloc)了一块内存,但是没有去手动的释放(delete/free)内存,导致指针已经消失,而指针指向的东西还在,已经不能控制这块内存,

例子

void remodel(std::string &str)
{//创建了一个局部指针变量,函数调用结束后,指针变量消失,但堆中内存仍然被占用,没有被释放,导致内存泄漏std::string *ps = new std::string(str); //内存泄漏了
}

如果发生了内存泄露又没有及时发现,随着程序运行时间的增加,程序越来越大,直到消耗完系统的所有内存,然后系统崩溃

在我们那个DealCommand()函数里面

我们申请了缓冲区去recv由那个套接字收到的数据

但是我们一开始设计时候是为了长连接作准备的,因为我们考虑的是双方都能收到很多包

因为client对server是短连接,所以server端的deal函数只用处理一个包,可以随着过程释放new出来的空间

server端的deal函数

	int DealCommand() { //无限循环if (m_client == -1) return -1;//char buffer[1024] = "";char* buffer = new char[4096]; //if (buffer == NULL) {TRACE("内存不足");return -2;}memset(buffer, 0, 4096);size_t index = 0;while (true) {size_t len = recv(m_client, buffer+index, 4096-index, 0);if (len <= 0) {delete[]buffer;return -1;}TRACE("recv %d\r\n", len);index += len; //可能收到2000个字节的包len = index;m_packet = CPacket ((BYTE*)buffer, len);if (len > 0) {memmove(buffer, buffer + len, 4096-len);//从buffer + len复制4096-len个字节到bufferindex -= len; //可能只用1000个delete[]buffer;return m_packet.sCmd;}}delete[]buffer;return -1;}

client端的deal函数,我们需要处理server端发来的很多包,但是我们又要防止内存泄漏,我们也不知道server端一次性给client端发了多少包,就不知道在这个函数哪个地方释放掉这个内存,但是我们知道的是client端的socket释放时候,我们那个包肯定处理完了,所以我们搞一个成员变量,让其在析构时候自动delete掉,我们想到了vecter,随对象的释放而析构

private: std::vector<char> m_buffer; //也是用的new,内存不需要管理,可以直接取地址用ClientSocket() {if (InitSockEnv() == FALSE) {MessageBox(NULL, _T("无法初始化套接字环境,请检查网络设置"), _T("初始化错误!!!"), MB_OK | MB_ICONERROR);exit(0);}m_buffer.resize(4096); //设置大小}
	int DealCommand() { //无限循环if (m_sock == -1) return -1;//char buffer[1024] = "";char* buffer = m_buffer.data(); //memset(buffer, 0, 4096);size_t index = 0;while (true) {size_t len = recv(m_sock, buffer + index, 4096 - index, 0);if (len <= 0) {return -1;}index += len; //可能收到2000个字节的包len = index;m_packet = CPacket((BYTE*)buffer, len); if (len > 0) {memmove(buffer, buffer + len, 4096 - len);//从buffer + len复制4096-len个字节到bufferindex -= len; //可能只用1000个return m_packet.sCmd;}}return -1;}

添加IP地址和端口控件

这个控件在下面添加

然后分别右击两个控件,添加上变量m_server_address,m_nPort

然后我们需要在程序里面使用这个变量

UpdateData(默认是true)

对话框数据交换

如果使用 DDX 机制,则可设置对话框对象的成员变量的初始值(通常在 OnInitDialog 处理程序或对话框构造函数中)。 就在显示对话框之前,框架的 DDX 机制会将成员变量的值传输到对话框中的控件,当对话框本身为响应 DoModalCreate 而出现时,这些控件将会显示在对话框中。 OnInitDialog 中的 CDialog 的默认实现调用 UpdateData 类的 CWnd 成员函数以在对话框中初始化控件

当用户单击“确定”按钮时(或在你每次使用 TRUE 自变量调用 UpdateData 成员函数时),同一个机制都会将值从控件传输到成员变量。 对话框数据验证机制将验证为其指定了验证规则的所有数据项

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说人话就是TRUE参数时候,会将控件里面的值赋值给成员变量

然后我们需要将端口调整为默认的9527

添加上如下代码:

然后改变入口点的代码

int CRemoteClientDlg::SendCommandPacket(int nCmd, bool bAutoClose,BYTE* pData, size_t nLength)
{UpdateData();ClientSocket* pClient = ClientSocket::getInstance();bool ret = pClient->InitSocket(m_server_address, atoi((LPCTSTR)m_nPort));//后续加返回值的处理``````
}

因为m_nPort是编辑框来的,所以默认是string,需要将其转化为int

bool InitSocket(int nIP,int nPort) {```serv_adr.sin_addr.s_addr = htonl(nIP); //bug,主机字节序转成网络字节序serv_adr.sin_port = htons(nPort);```
}

htonl是将主机字节序转为网络字节序

这篇关于client网络模块的开发和client与server端的部分联动调试的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1102339

相关文章

PyQt5 GUI 开发的基础知识

《PyQt5GUI开发的基础知识》Qt是一个跨平台的C++图形用户界面开发框架,支持GUI和非GUI程序开发,本文介绍了使用PyQt5进行界面开发的基础知识,包括创建简单窗口、常用控件、窗口属性设... 目录简介第一个PyQt程序最常用的三个功能模块控件QPushButton(按钮)控件QLable(纯文本

SQL Server 中的 WITH (NOLOCK) 示例详解

《SQLServer中的WITH(NOLOCK)示例详解》SQLServer中的WITH(NOLOCK)是一种表提示,等同于READUNCOMMITTED隔离级别,允许查询在不获取共享锁的情... 目录SQL Server 中的 WITH (NOLOCK) 详解一、WITH (NOLOCK) 的本质二、工作

SQL Server安装时候没有中文选项的解决方法

《SQLServer安装时候没有中文选项的解决方法》用户安装SQLServer时界面全英文,无中文选项,通过修改安装设置中的国家或地区为中文中国,重启安装程序后界面恢复中文,解决了问题,对SQLSe... 你是不是在安装SQL Server时候发现安装界面和别人不同,并且无论如何都没有中文选项?这个问题也

在IntelliJ IDEA中高效运行与调试Spring Boot项目的实战步骤

《在IntelliJIDEA中高效运行与调试SpringBoot项目的实战步骤》本章详解SpringBoot项目导入IntelliJIDEA的流程,教授运行与调试技巧,包括断点设置与变量查看,奠定... 目录引言:为良驹配上好鞍一、为何选择IntelliJ IDEA?二、实战:导入并运行你的第一个项目步骤1

基于Python开发一个图像水印批量添加工具

《基于Python开发一个图像水印批量添加工具》在当今数字化内容爆炸式增长的时代,图像版权保护已成为创作者和企业的核心需求,本方案将详细介绍一个基于PythonPIL库的工业级图像水印解决方案,有需要... 目录一、系统架构设计1.1 整体处理流程1.2 类结构设计(扩展版本)二、核心算法深入解析2.1 自

SQL server数据库如何下载和安装

《SQLserver数据库如何下载和安装》本文指导如何下载安装SQLServer2022评估版及SSMS工具,涵盖安装配置、连接字符串设置、C#连接数据库方法和安全注意事项,如混合验证、参数化查... 目录第一步:打开官网下载对应文件第二步:程序安装配置第三部:安装工具SQL Server Manageme

C#连接SQL server数据库命令的基本步骤

《C#连接SQLserver数据库命令的基本步骤》文章讲解了连接SQLServer数据库的步骤,包括引入命名空间、构建连接字符串、使用SqlConnection和SqlCommand执行SQL操作,... 目录建议配合使用:如何下载和安装SQL server数据库-CSDN博客1. 引入必要的命名空间2.

Python通用唯一标识符模块uuid使用案例详解

《Python通用唯一标识符模块uuid使用案例详解》Pythonuuid模块用于生成128位全局唯一标识符,支持UUID1-5版本,适用于分布式系统、数据库主键等场景,需注意隐私、碰撞概率及存储优... 目录简介核心功能1. UUID版本2. UUID属性3. 命名空间使用场景1. 生成唯一标识符2. 数

SQL Server配置管理器无法打开的四种解决方法

《SQLServer配置管理器无法打开的四种解决方法》本文总结了SQLServer配置管理器无法打开的四种解决方法,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录方法一:桌面图标进入方法二:运行窗口进入检查版本号对照表php方法三:查找文件路径方法四:检查 S

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.