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

发布于:2024-08-23 ⋅ 阅读:(103) ⋅ 点赞:(0)

客户端网络模块的开发

我们需要先了解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个字节到buffer
				index -= 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个字节到buffer
				index -= len; //可能只用1000个
				return m_packet.sCmd;
			}
		}
		return -1;
	}


网站公告

今日签到

点亮在社区的每一天
去签到