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

相关文章

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Python中logging模块用法示例总结

《Python中logging模块用法示例总结》在Python中logging模块是一个强大的日志记录工具,它允许用户将程序运行期间产生的日志信息输出到控制台或者写入到文件中,:本文主要介绍Pyt... 目录前言一. 基本使用1. 五种日志等级2.  设置报告等级3. 自定义格式4. C语言风格的格式化方法

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

Debian 13升级后网络转发等功能异常怎么办? 并非错误而是管理机制变更

《Debian13升级后网络转发等功能异常怎么办?并非错误而是管理机制变更》很多朋友反馈,更新到Debian13后网络转发等功能异常,这并非BUG而是Debian13Trixie调整... 日前 Debian 13 Trixie 发布后已经有众多网友升级到新版本,只不过升级后发现某些功能存在异常,例如网络转

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

基于Java开发一个极简版敏感词检测工具

《基于Java开发一个极简版敏感词检测工具》这篇文章主要为大家详细介绍了如何基于Java开发一个极简版敏感词检测工具,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录你是否还在为敏感词检测头疼一、极简版Java敏感词检测工具的3大核心优势1.1 优势1:DFA算法驱动,效率提升10

SQL Server 查询数据库及数据文件大小的方法

《SQLServer查询数据库及数据文件大小的方法》文章介绍了查询数据库大小的SQL方法及存储过程实现,涵盖当前数据库、所有数据库的总大小及文件明细,本文结合实例代码给大家介绍的非常详细,感兴趣的... 目录1. 直接使用SQL1.1 查询当前数据库大小1.2 查询所有数据库的大小1.3 查询每个数据库的详