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

客户端网络模块的开发

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

socket通信

server端的流程

client端的流程

对于closesocket()函数来说

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

创建网络模块类

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

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

添加一个ClientSocket类

对于这里面代码的修改

我们修改初始化代码

c 复制代码
	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()使用"讲解

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

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

c++ 复制代码
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图形化界面了

c++ 复制代码
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循环

c++ 复制代码
            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()

c++ 复制代码
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:是我们用来测试包的

c++ 复制代码
int TestConnect()
{
    CPacket pack(1981, NULL, 0);
    CserverSocket::getInstance()->Send(pack);
    return 0;

}
c++ 复制代码
//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)内存,导致指针已经消失,而指针指向的东西还在,已经不能控制这块内存,

例子

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

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

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

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

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

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

server端的deal函数

c++ 复制代码
	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个字节到buffer
				index -= len; //可能只用1000个
				delete[]buffer;
				return m_packet.sCmd;
			}
		}
		delete[]buffer;
		return -1;
	}

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

c++ 复制代码
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); //设置大小
	}
c++ 复制代码
	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个字节到buffer
				index -= 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

添加上如下代码:

然后改变入口点的代码

c++ 复制代码
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

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

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

相关推荐
天行健,君子而铎9 小时前
数据分类分级的范式转换:从规则匹配到场景化高准确率一键部署
网络·数据库·安全
深圳市快瞳科技有限公司13 小时前
宠物门禁端侧AI落地解析:画质检测、活体甄别与离线推理
网络
一条泥憨鱼15 小时前
【从0开始学习计算机网络】| ARP 协议:地址解析,ARP 欺骗,链路层寻址
网络·后端·学习·计算机网络
m0_7156744315 小时前
数据分类分级的范式转换:从规则匹配到场景化高准确率一键部署
大数据·网络·人工智能·网络安全
dog25016 小时前
带宽,时延,抖动的不可能三角
网络·tcp/ip
砚凝霜16 小时前
【软考信息安全】第十六章 网络安全风险评估过程与技术方法
网络·安全·web安全
Eloudy17 小时前
MLNX_OFED 的 install.pl 安装流程分析
网络·rdma·roce
对讲机数码科普19 小时前
从电路原理到协议栈:模拟对讲机与数字对讲机的技术内核拆解
运维·网络·架构·软件工程
我送炭你添花19 小时前
ASA原理与电子维修实战:前言 一路修来,一路学习
运维·网络·学习
张小姐的猫20 小时前
【Linux】网络编程 —— 多路转接:epoll
linux·运维·网络