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 机制会将成员变量的值传输到对话框中的控件,当对话框本身为响应 DoModalCreate 而出现时,这些控件将会显示在对话框中。 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是将主机字节序转为网络字节序

相关推荐
安步当歌1 小时前
【WebRTC】视频发送链路中类的简单分析(下)
网络·音视频·webrtc·视频编解码·video-codec
米饭是菜qy1 小时前
TCP 三次握手意义及为什么是三次握手
服务器·网络·tcp/ip
yaoxin5211232 小时前
第十九章 TCP 客户端 服务器通信 - 数据包模式
服务器·网络·tcp/ip
鹿鸣天涯2 小时前
‌华为交换机在Spine-Leaf架构中的使用场景
运维·服务器·网络
星海幻影2 小时前
网络基础-超文本协议与内外网划分(超长版)
服务器·网络·安全
WeeJot嵌入式2 小时前
网络百问百答(一)
网络
湖南罗泽南2 小时前
p2p网络介绍
网络·网络协议·p2p
IPdodo全球网络3 小时前
解析“ChatGPT网络错误”:从网络专线到IP地址的根源与解决方案
网络·tcp/ip·chatgpt
腾科张老师4 小时前
为什么要使用Ansible实现Linux管理自动化?
linux·网络·学习·自动化·ansible
Sweet_vinegar4 小时前
Wireshark
网络·测试工具·安全·wireshark·ctf·buuctf