客户端的磁盘获取和文件目录获取
RemoteControl: 基于VS2022环境下利用C/C++和MFC实现的远程控制项目 (gitee.com)
本次主要实现了客户端初步基本界面的布局。和联调实现客户端获取到服务被控端的磁盘分区信息和可以获取到对应的目录下的文件目录信息。这里是本次的实现过程中的发现问题解决问题的记录。
客户端Dialog界面
RemoteControl_ClientDlg.cpp:当页面布置好我设置了控件数据初始化
BOOL CRemoteControlClientDlg::OnInitDialog(){
// TODO: 在此添加额外的初始化代码
UpdateData();//当该函数没有参数是默认参数为TRUE,作用为同步控件的数据给控件的变量
m_server_address = 0x7F000001;//当前默认本地127.0.0.1
m_nPort = _T("9527");
UpdateData(FALSE);//参数为FALSE时为控件的变量传给控件
}
RemoteControl_ClientDlg.cpp:所有的客户端发送数据包交由客户端Dialog发送
int CRemoteControlClientDlg::SendCommandPacket(int Cmd, BYTE* pData, size_t dataSize)
{
UpdateData();
// TODO: 在此添加控件通知处理程序代码
CClientSocket* pClient = CClientSocket::GetInstance();
bool ret = pClient->InitSkt(m_server_address, atoi((LPCSTR)m_nPort));
if (!ret) {
AfxMessageBox("Network Init Failed!");
return -1;
}
CPacket pack(Cmd, pData, dataSize);
//CClientSocket::GetInstance()->GetPacket() = pack;//存疑?类指针访问不了私有?
ret = pClient->Send(pack);
TRACE("Send ret = %d\r\n", ret);
int cmd = pClient->DealCommand();
TRACE("ack:%d\r\n", cmd);
return cmd;
}**ClientSocket.cpp**:修改InitSkt函数使得控件的IP和端口数据传入函数
BOOL CClientSocket::InitSkt(int nIP, short port)
{
if (m_skt == -1) {
TRACE(_T("m_skt never existed\r\n"));
return false;
}
sockaddr_in serv_adr;// IPv4 地址的结构体
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET; //地址族设置(AF_INET 表示 IPv4)
serv_adr.sin_addr.s_addr = htonl(nIP);//服务端可以通过任何网络接口连接,而不用担心特定的网络接口或 IP 地址
serv_adr.sin_port = htons(port);//设置连接的端口号
if (serv_adr.sin_addr.s_addr == INADDR_NONE) {
AfxMessageBox("指定的IP地址不存在");
return false;
}
int ret = 0;
if (sign == false) {
ret = connect(m_skt, (sockaddr*)&serv_adr, sizeof(serv_adr));
sign = true;
}
if (ret == -1) {
AfxMessageBox("connect failed");
TRACE("connect failed: %d, %s\r\n",WSAGetLastError(), GetErrInfo(WSAGetLastError()).c_str());
sign = false;
return false;
}
//一切正常
return true;
}
htonl():主机字节序(host byte order)转换为网络字节序(network byte order)
为什么我们要转换字节序呢?:因为本地控件数据在内存是小端存储。而网络字节序是大端存储的字节序解析规则,二者是反过来的。
同理**htons()**是一样的道理。只不过前者htonl的最后一个字母是l,即对应long类型的32位。而htons的最后一个字母是s,即对应的short类型的16位。
ClientSocket.h:新增的成员属性和行为
class CClientSocket
{
private:
size_t lastRecv;//记录上次recv接收了多少数据
size_t nowIndex = 0;//记录实际的接收缓冲区数据的右边界指针 即 表示当前接收缓冲区的数据大小
bool bufferSizeSmallerPacket = true;
public:
void InitLastRecvAndbufferSizeSmallerPacket() {lastRecv = BUFFER_SIZE; bufferSizeSmallerPacket = true;}
};
因为我的设定是长连接,所以要保证缓冲区的更新,同时要注意recv可能引起的阻塞
ClientSocket.cpp:改善后的处理命令行为
#define BUFFER_SIZE 4096
int CClientSocket::DealCommand()
{
TRACE(_T("Processing commands\r\n"));
if (m_skt == -1) {
TRACE(_T("Client socket = -1 is disconnected\r\n"));
return -1;
}
char* buffer = m_buffer.data();//让容器去管理内存泄漏
if (buffer == NULL) {
TRACE("no heap memory\r\n");
return -2;
}
while (1) {
//***
//setNonBlocking(m_skt,1);//设置recv函数为非阻塞型
size_t recvLength;
if (bufferSizeSmallerPacket && lastRecv > 0) {//如果上次有收到数据,并且接收缓冲区的数据解析不出来一个包时
bufferSizeSmallerPacket = false;
recvLength = recv(m_skt, buffer + nowIndex, BUFFER_SIZE - nowIndex, 0);//获得接收缓冲区的增量值
nowIndex += recvLength; //更新当前接收缓冲区大小
lastRecv = recvLength;//记录上次接收数据的大小
}
//setNonBlocking(m_skt, 0);//设置recv函数为阻塞型
recvLength = nowIndex;//此时的该变量已然没用,就充当当前接收缓冲区大小,保护nowIndex值
packet = CPacket((BYTE*)buffer, recvLength);//解析包后更新recvLength为接收一个数据包具体接收了接收缓冲区的多少
if (recvLength > 0) {
memmove(buffer, buffer + recvLength, BUFFER_SIZE - recvLength);//把已经被解析成包的数据覆盖掉,后面往前推。然后补 '\0'
nowIndex -= recvLength;//更新右边界
return packet.PacketCommand;
}
else {
bufferSizeSmallerPacket = true;
}
}
return -1;
}
优化改进点:可以尝试Nagle算法来保证发送缓冲区尽可能一次发送send多些数据来使得recv一次多接收些数据。这样可以利用好网络带宽以及这样的话效率更高。
查看磁盘分区信息按钮
设计:为了后续能够区分是文件还是目录,我补充设计了如果是目录属性,则会在添加目录的一个空的子节点
RemoteControl_ClientDlg.cpp
void CRemoteControlClientDlg::OnBnClickedBtnDiskinfo()
{
// TODO: 在此添加控件通知处理程序代码
CClientSocket* pClient = CClientSocket::GetInstance();
pClient->InitLastRecvAndbufferSizeSmallerPacket();
if (SendCommandPacket(6) == -1) {
AfxMessageBox("Command Processing Failed");
return;
}
std::string Drivers = pClient->GetPacket().PacketData;
std::string Dr;
m_Tree.DeleteAllItems();
for (size_t i = 0; i < Drivers.size(); i++) {
if (Drivers[i] == ',') {
Dr += ':';
//把当前的磁盘插入到文件树的根下(TVI_ROOT),在末尾(TVI_LAST)插入。
HTREEITEM hTemp = m_Tree.InsertItem(Dr.c_str(), TVI_ROOT, TVI_LAST);
//获得插入的文件树该项的句柄
m_Tree.InsertItem(NULL, hTemp, TVI_LAST);//因为磁盘必定是目录,所以在项后末尾插入空结点作为目录的标志
Dr.clear();
continue;
}
Dr += Drivers[i];
}
//数据记得处理哦
if (Dr.size() != 0) {
Dr += ':';
HTREEITEM hTemp = m_Tree.InsertItem(Dr.c_str(), TVI_ROOT, TVI_LAST);
m_Tree.InsertItem(NULL, hTemp, TVI_LAST);
Dr.clear();
}
}
双击获得对应目录下的文件和目录信息
RemoteControl_ClientDlg.cpp
void CRemoteControlClientDlg::OnNMDblclkTreeDir(NMHDR* pNMHDR, LRESULT* pResult)
{
// TODO: 在此添加控件通知处理程序代码
*pResult = 0;
CPoint ptMouse;
GetCursorPos(&ptMouse);//获取鼠标当前位置
m_Tree.ScreenToClient(&ptMouse);//转换为树形控件的坐标
HTREEITEM hTreeSelected = m_Tree.HitTest(ptMouse, 0);//利用坐标获取树形控件被点击的结点
if (hTreeSelected == NULL)
return;
if (m_Tree.GetChildItem(hTreeSelected) == NULL)//如果点击结点即为文件结点直接返回
return;
DeleteThreeChildrenItem(hTreeSelected);//删除它的所有子孙结点
CString strPath = GetTreeItemPath(hTreeSelected);
CClientSocket* pClient = CClientSocket::GetInstance();
pClient->InitLastRecvAndbufferSizeSmallerPacket();//如何不设置断点,为什么会不执行
int cmd = SendCommandPacket(7, (BYTE*)(LPCSTR)strPath, strPath.GetLength());//
//1
CPacket& p = CClientSocket::GetInstance()->GetPacket();
PFILEINFO pInfo = (PFILEINFO)p.PacketData.c_str();
//2
//PFILEINFO pInfo = (PFILEINFO)CClientSocket::GetInstance()->GetPacket().PacketData.c_str();//右值临时对象被释放的***乱码问题
while (pInfo->HasNext) {
TRACE("[%s] IsDir %d(1=true)\r\n", pInfo->szFileName, pInfo->IsDirectory);
if (pInfo->IsDirectory) {
if (CString(pInfo->szFileName) == "." || (CString(pInfo->szFileName) == "..")) {
int cmd = pClient->DealCommand();
TRACE("ack:%d\r\n", cmd);
if (cmd < 0)
break;
p = CClientSocket::GetInstance()->GetPacket();
pInfo = (PFILEINFO)p.PacketData.c_str();
continue;
}
}
HTREEITEM hTemp = m_Tree.InsertItem(pInfo->szFileName, hTreeSelected, TVI_LAST);
if (pInfo->IsDirectory) {
m_Tree.InsertItem("", hTemp, TVI_LAST);
}
int cmd = pClient->DealCommand();
TRACE("ack:%d\r\n", cmd);
if (cmd < 0)
break;
p = CClientSocket::GetInstance()->GetPacket();
pInfo = (PFILEINFO)p.PacketData.c_str();
}
}
实现过程中发现的问题
//1
CPacket& p = CClientSocket::GetInstance()->GetPacket();
PFILEINFO pInfo = (PFILEINFO)p.PacketData.c_str();
//2
PFILEINFO pInfo = (PFILEINFO)CClientSocket::GetInstance()->GetPacket().PacketData.c_str();
在写法2的类型转换后的pInfo指向的那块内存数据是乱码问题。但是写法1却不会乱码。为什么呢?
我思考查阅后得知是右值被释放后的问题。
如上述反汇编易知,临时的包最后调用的析构,哪怕该析构什么也没做,可是编译器根据析构的设定就会保护该内存区域。当用强转类型指针指向也只能访问到乱的数据。
写法2明确的在对应的栈区引用了该区域使得该内存区域的生命周期和pInfo的生命周期一致
测试
数据显示完全正确,却前面的TestConnect功能依旧正常