客户端网络模块的开发
我们需要先了解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()函数返回值表格,在下面文章里面
然后我们需要一个可以将错误码格式化的函数
这个函数不用深究,记住这个模板以后直接用
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是将主机字节序转为网络字节序