本文主要是介绍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 机制会将成员变量的值传输到对话框中的控件,当对话框本身为响应 DoModal
或 Create
而出现时,这些控件将会显示在对话框中。 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端的部分联动调试的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!