原文链接:http://blog.renren.com/blog/bp/Q782_Jqytx
当顺利的连接到主控端之后,按照程序的一个执行逻辑,被控端会将本机上的一些反映本机状态的一个信息发送到主控端,这个过程其实涉及到了被控端与主控端间信息的交互过程。
*******************************************************************************
我们需要从sendLoginInfo这个函数讲起。先看看这个函数的实现过程
讲解这个函数,我们不得不先去认识一个结构体,这个结构体就是传说中的上线包,很多杀软现在已经拦截这个上线包,在安装杀软的机子上如果有这种上线包数据在往外传输,那么杀软会认为这是由远控在控制你的电脑,因此,杀软会拦截这个上线包的传输。因此,如果我们想要我们的远控发挥作用的话,我们必须将上线包进行一个修改,让杀软去匹配我们的上线包的时候无法匹配到。这样,就会躲过杀毒软件防黑墙的拦截。
此上线包的结构如下图所示:
该函数的一整套操作就是填充这个数据结构里的各个值。
首先,声明该上线包的类型为上线包
LoginInfo.bToken = TOKEN_LOGIN;
接着获取操作系统的版本信息
BOOL GetVersionEx(LPOSVERSIONINFO lpVersionInformation);
The GetVersionEx function obtains extended information about the version of the operating system that is currently running.调用这个函数可以获得当前运行的操作系统的扩展版本信息。 获取主机名 int gethostname (char FAR * name,
int namelen
);
The Windows Sockets gethostname function returns the standard host name for the local machine。调用这个函数返回本机的标准主机名。 获取连接的IP地址 int getsockname (SOCKETs,
struct sockaddr FAR* name,
int FAR* namelen
);
The Windows Sockets getsockname function retrieves the local name for a socket.这个函数会返回用于连接的指定套接字的一个本地的IP地址。 获取CPU的主频 LoginInfo.CPUClockMhz = CPUClockMhz();我们接下来看看获取CPU主频的,CPU主频的获取是通过查询注册表里的键值来取得的。
获取CPU个数 VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);
The GetSystemInfo function returns information about the current system. 调用这个函数会返回当前系统的一些信息。
获取视频信息
LoginInfo.bIsWebCam = IsWebCam();
获取视频信息的过程是通过遍历十个视频驱动是否存至少存在一个来判断。
BOOL VFWAPI capGetDriverDescription(
WORD wDriverIndex,
LPSTRlpszName,
INT cbName,
LPSTR lpszVer,
INTcbVer
);
The capGetDriverDescription function retrieves the version description of the capture driver. Index of the capture driver. The index can range from 0 through 9.调用这个函数返回视频驱动的版本信息,如果不存在就返回FALSE。 获取CPU的处理速度,这个参数是通过一个时间段来考量的。
LoginInfo.dwSpeed = GetTickCount()_1 - GetTickCount()_2
获取一个备注信息,备注信息是存储在受控端的注册表里的
获取一个版本信息,版本信息,版本信息是自己填写的。 lstrcpy(LoginInfo.Ver,"0.2");对以上的过程做一个总结
组织好以上数据之后,开始向主控端发送上线包,接下来,我们去看一下这个发送数据的过程。CClientSocket::Send 这个函数就是组织待发送的数据包的过程,我们接下来详细的看看这个组织数据的过程。 首先,待发送的数据是在参数:lpData中,而待发送的数据的大小由参数:nSize指定。 首先,将需要承载发送数据的缓冲区先清空:m_WriteBuffer.ClearBuffer(); 然后,将数据进行压缩处理。 这个地方要注意:在压缩前,这个值的计算公式为: unsigned long destLen = (double)nSize * 1.001 + 12;这个是固定的。因为,内存中压缩数据的时候是需要一定大小的工作空间的,因此按照这个公式计算出工作需要的空间大小,然后申请这么大小的一个内存空间作为压缩过的数据的存储空间。然后使用compress进行压缩处理:int nRet = compress(pDest, &destLen, lpData, nSize);注意这个函数执行完成之后,在参数destLen中会返回数据被压缩过之后的一个大小。 接下来,开始往发送缓冲区中组织欲发送的数据 LONG nBufLen = destLen + HDR_SIZE; m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag));m_WriteBuffer.Write((PBYTE) &nBufLen, sizeof(nBufLen)); m_WriteBuffer.Write((PBYTE) &nSize, sizeof(nSize));m_WriteBuffer.Write(pDest, destLen); 数据包发送标记+整个数据包的大小+未压缩前数据包的大小+被压缩后的数据 再然后,备份要发送的数据到m_ResendWriteBuffer缓冲区中。 当然,在这个组织发送的数据的函数中还有一个小的功能,即当在接收数据的过程中,如果出现了差错,则可以提醒主控端重新发送数据。上述描述的代码体现如下:当受控端接收数据的时候,调用CClientSocket::OnRead这个函数,而在执行这个函数的过程中,一旦有错误发生则会执行下面的操作:
当出现Send(NULL,0)这种调用的时候,就会执行 m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag));m_ResendWriteBuffer.ClearBuffer(); m_ResendWriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); 仅仅向主控端发送一个传输标志的数据包,主控端在收到这种特殊的数据的时候,会有重新发送数据的逻辑处理过程。
关于这个发送的过程,我们在这里简单的描述一下: 首先会在一个大的循环中发送nSplitSize*N个数据包,发送完这些数据包之后,要么还剩下少于nSplitSize大小的数据包,要么还剩下大小为0的数据包,然后再在下面的这个循环中完成了数据包的发送过程。 上述过程已完成,即完成了受控端向主控端发送数据的过程。 接下来,我们需要看看主控端是怎么接收到这些这些数据,并进行处理的呢? 在主控端,我们注意IOCPServer::OnAccpet这个函数,在这个函数的最后进行了一个这样的调用指令:PostRecv(pContext);在这里我们再次回顾一下这个函数的实现过程:
如果稍微了解一点IOCP高性能服务器的话就会对这个操作觉得十分面熟。比如关联完成端口的时候使用的句柄唯一数值——CllientContext,比如OVERLAPPEDPLUS这个结构为什么要这么设计:class OVERLAPPEDPLUS {public:OVERLAPPED m_ol;IOType m_ioType;OVERLAPPEDPLUS(IOType ioType) {ZeroMemory(this, sizeof(OVERLAPPEDPLUS));m_ioType = ioType;}};OVERLAPPED m_ol这个变量之后的m_ioType就是传说中的Per-IO数据,根据这个数据,我们可以知道,到底是哪种请求被投递到了完成端口,并且可以被处理了。 这里就是向完成端口投递了一个读取数据的请求,当有数据到达完成端口的时候,等待在该完成端口上,为该完成端口服务的线程函数会被唤醒,继续往下执行。这里,我们就分析下,这个为完成端口服务的线程函数,这个工作线程的个数是跟系统CPU核心数有关的。接下来,我们分析下这个工作线程的执行逻辑。
在函数的前面就是一些变量的定义,这些变量的含义我们在前面的课程中都已经讲解过,因此,在这里我们就不再重述。我们重点来看两句指令的调用:InterlockedIncrement(&pThis->m_nCurrentThreads);InterlockedIncrement(&pThis->m_nBusyThreads);在这里将变量:m_nCurrentThread与m_nBusyThreads以原子的方式进行加1的操作。这两个变量的作用是什么,我们看下面引用这两个变量的一些地方。1:在CIOCPServer::CIOCPServer构造函数中 m_nCurrentThreads= 0;m_nBusyThreads = 0;2:在CIOCPServer::ThreadPoolFunc开始的部分,即刚进入工作线程函数的时候 InterlockedIncrement(&pThis->m_nCurrentThreads);InterlockedIncrement(&pThis->m_nBusyThreads);3:在CIOCPServer::ThreadPoolFunc结束的部分,即将要离开工作线程函数的时候 InterlockedIncrement(&pThis->m_nCurrentThreads);InterlockedIncrement(&pThis->m_nBusyThreads);因此,这两个变量就是记录当前进程中为完成端口服务的工作线程的数量这么一个值,只不过m_nCurrentThreads是用来记录所有为该完成端口服务的工作线程的数量,而另外的一个m_nBusyThreads则是用来记录所有为完成端口工作的线程数量中处于唤醒状态,而非阻塞状态的线程数量,这里有一个对m_nBusyThreads的引用,可以看出它的作用。在进入GetQueuedCompletionStatus这个函数进行等待完成端口上发生请求之前,先对这个值进行了递减的操作:InterlockedDecrement(&pThis->m_nBusyThreads);而当某个线程等待到请求发生时,即GetQueuedCompletionStatus这个函数返回的时候,会对这个变量的值进行了一个递增的操作:InterlockedIncrement(&pThis->m_nBusyThreads)。 接下来程序进入一个无限的循环,在这个循环中就是等待并处理到来的投递请求。
这个循环的主题就是GetQueuedCompletionStatus这个函数,这个函数就是等待在完成端口上,等待完成端口上到来的请求。该函数使用的两个参数对理解本段代码特别重要,一个是lpClientContext,另一个是lpOverlapped。下面我们对这两个参数的重要性进行论述。第一:lpClientContext这个参数是在CIOCPServer::AssociateSocketWithCompletionPort这个函数里被设定的:HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, 0);这其中的第三个参数,就是lpClientContext。当在这个完成端口上有了事件发生的时候,用GetQueuedCompletionStatus的第三个参数返回的数值与CreateIoCompletionPort的第三个参数的数值是相对应的,或者说他们就是指的同一个ClientContext。第二:lpOverlapped这个参数是的设定位置有三个,分别是被控端刚上线,并且将主控端与被控端交互的socket已经与完成端口相关联之后,会发送一个IOInitialize作为附加数据的OVERLAPPED。OVERLAPPEDPLUS *pOverlap = new OVERLAPPEDPLUS(IOInitialize);BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol); 当需要接受数据的时候,先要投递一个接收数据的请求,这个时候会投递一个IORead作为附加数据的OVERLAPPEDOVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IORead);ULONG ulFlags = MSG_PARTIAL;DWORD dwNumberOfBytesRecvd;UINT nRetVal = WSARecv(pContext->m_Socket, &pContext->m_wsaInBuffer, 1, &dwNumberOfBytesRecvd, &ulFlags, &pOverlap->m_ol, NULL);除了了解在这里投递了一个IORead类型的请求之外,在这里还要说一点,这个WSARecv指明当有数据到来的时候,要将数据接收到ClientContext::m_wsaInBuffer这个缓冲区中。 当需要发送数据的时候,先要投递一个发送数据的请求,这个时候会投递一个IOWrite作为附加数据的OVERLAPPEDOVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite);PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);附加数据的用处在哪里呢?看看这个循环里对附加数据的处理:pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol);这个CONTAINING_RECORD就是将指针的范围进行一个扩大,指向整个的定义结构,这样就能指向带有附加数据的整个OVERLAPPEDPLUS类型,并且根据附加的数据的m_ioType域的不同,进行不同的处理:ProcessIOMessage(pOverlapPlus->m_ioType, lpClientContext, dwIoSize);接下来是对GetQueuedCompletionStatus这个函数的返回值的一个判断,主要分为以下的几种情况。第一种情况:对方关闭掉了这个通信的套接字,也就是说被控端掉线了,这个时候的处理方案就是直接将被控端的数据结构摘除,并且从列表框中删除之。第二种情况:这种情况是本程序中的设计,并非是单纯的对GetQueuedCompletionStatus这个函数的返回值进行一个处理。这种情况就是根据当前CPU的一个负载能力进行工作线程的一个调整:如果当前的工作线程都处于忙碌状态,并且当前CPU还有能力运行新的线程,那好就开启一个新的线程为完成端口服务。如果,当前CPU的负载能力受不了了,则适当的Kill掉自身线程函数。我以为在这个调整自身的地方应该还有一个处理主动结束自身线程的逻辑分支,因为当要关闭掉套接字的时候,有以下这么些操作while (m_nWorkerCnt){ PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) NULL, NULL); Sleep(100);}其实这个循环就是要求为完成端口工作的线程主动结束自身的过程。
3:就是传输正确的情况下,对不同的请求进行不同的处理,这个差异化的处理函数就是ProcessIOMessage这个过程的实现我们在Gh0st通信协议分析(2)里面有过专门的说明。在这里我们再来回顾一下: 先前发送的投递请求最终是由CIOCPServer::ProcessIOMessage这个函数来完成的,
关于这个函数的定义,不得不去看一组宏定义:
enum IOType
{
IOInitialize,
IORead,
IOWrite,
IOIdle
};
#define BEGIN_IO_MSG_MAP() \
public: \
Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize = 0)\
{ \
bool bRet = false;
#define IO_MESSAGE_HANDLER(msg, func) \
if (msg == clientIO) \
bRet = func(pContext, dwSize);
#define END_IO_MSG_MAP() \
return bRet; \
}
接下来,我们需要看看使用这个宏的地方的定义:
BEGIN_IO_MSG_MAP()
IO_MESSAGE_HANDLER(IORead, OnClientReading)
IO_MESSAGE_HANDLER(IOWrite, OnClientWriting)
IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing)
END_IO_MSG_MAP()
对这组宏调用进行宏展开,展开之后的情形为:
public:
Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize = 0)\
{
bool bRet = false;
if (IORead == clientIO) \
bRet = OnClientReading(pContext, dwSize);
if (IOWrite == clientIO) \
bRet = OnClientWriting(pContext, dwSize);
if (IOInitialize == clientIO) \
bRet = OnClientInitializing(pContext, dwSize);
return bRet;
}
因此,本次由被控端投递过来的上线包的接收就应该由OnClientReadling来处理咯,我们接下来需要看看这个函数的实现过程。
在开始处,给该段代码增加了一段临界区,以使得该段代码在多线程环境中可以以独占的方式被访问。这段代码应该必须被独占的方式访问,因为此函数的调用者是为完成端口工作的线程去调用,而我们知道为完成端口工作的线程不止一个。 接下来是为反映传输速度而进行的一些编码,在这个地方唯一需要我们注意的是静态变量的一个用法。static DWORD nLastTick = GetTickCount();static DWORD nBytes = 0;关于静态局部变量的说明:1:静态局部变量在该函数被多次调用的时候只在第一次调用的时候执行定义语句。2:静态局部变量的生存周期不会因为该函数被执行完而结束,它在全局内存存储。因此,上述反应传输速度的代码应该这么理解,当第一次调用OnClientReadling
函数的时候,static DWORD nLastTick = GetTickCount();static DWORD nBytes = 0;这两句会被执行,接下来通过判断采样时间是否超过一秒钟,第一次调用的时候肯定不会超过一秒钟,但是当第二次调用该函数的时候,那两句定义静态变量的语句不会被执行,这个时候nBytes的值会是上次传输的大小+本次传输的大小,并且nLastTick的值还是上次采样的时间,由于本次采样的时间也许会超过了一秒钟,因此,反映接收速度的参数m_nRecvKbps会被重新改写值,并且这个时候,nBytes与nLastTick的值会被重新改写。 如果本次接收的数据大小为0的话,说明在数据传输的过程中遇到了不可预知的错误,这个时候就将这个客户端删除掉去它的连接,让它在重新连接上来,在看到这里的时候我突然想到了,Gh0st是如何判断受控端已经下线的问题,在这里我们再回顾一下,因为主控端与被控端进行交互通信的套接字都被设置了保活机制,没过一定的时间就进行探测对方是否还存活,连续探测几次,如果对方都无应答(这个应答是在TCP STACK里完成的,与应用层无关),则说明对方已经下线。这个时候,如果是被控端掉线,则主控端与被控端进行连接的SOCKET并不通知主控端被控端已经下线,当下一次主控端向被控端发送数据的时候,会发生下面这种情况:int nRetVal = WSASend(pContext->m_Socket, &pContext->m_wsaOutBuffer, 1, &pContext->m_wsaOutBuffer.len, ulFlags, &pOverlap->m_ol, NULL);if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING ){ RemoveStaleClient( pContext, FALSE );}这个时候,就说明被控端已经下线了,需要清除主控端与被控端的连接数据。 我们继续看OnClientReading这个函数的其他部分:
这个if语句发生的条件是如果被控端在接收主控端发送过去的数据的时候出现了错误,它会要求主控端重新发送数据,而这种要求重发数据的操作正是通过被控端向主控端仅仅发送传输标志。如果收到的是这样的数据包,则主控端会将备份区的数据重新发送到被控端。主控端的数据发送过程,跟被控端的数据发送过程相差无几,在这里我们仅仅贴上代码并简要的总结几句。
首先,如果是错误的调用直接返回如果主控端在接收数据的时候出现了异常,这个时候主控端需要让被控端重新发送数据,则仅仅是向被控端准备发送标志数据包,如此,被控端会重新向客户端发送备份的数据。接下来是压缩待发送的数据然后按照 标志+整个包大小+未压缩数据大小+压缩的数据 这种方式组织包,并备份数据WaitForSingleObject(pContext->m_hWriteComplete, INFINITE);无用的然后用PostQueuedCompletionStatus投递一个类型为IOWrite类型的发送请求,注意此函数的第二个参数为0。这个请求会由函数OnClientWriting去完成。我们看看它的实现代码
首先,跟接收数据一样,先更新一下代表发送速度的变量值:m_nSendKbps,这个更新的过程,跟接收的时候的一样,在这里只要注意一点局部静态变量的使用方法就好了,不再赘述。接着,由于dwIOSize的值为0,因此接下来的那个判断语句if代码块永远不会被执行。最后,在设置好WSASend函数的各个参数值之后,将数据直接就发送出去了,这个时候的发送情况基本上是一定能成功的,并且一般不会阻塞。除非被控端掉线。我们合并OnClientReading与OnClientWriting
中都调用的一个函数:
m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_TRANSMIT);m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_RECEIVE);这个函数实际上是由CMainFrame::NotifyProc这个函数来具体执行的,我们这里看看这两个函数的具体的执行情况。case NC_TRANSMIT: break;case NC_RECEIVE: ProcessReceive(pContext);只有对NC_RECEIVE这个做了处理,我们跟进ProcessReceive去看看这个函数都做了哪些处理:
我们发现,它只对需要弹出对话框的处理过程才起作用,因此在这里我们就不提前讲解了,等后续课程中,我们再讲解。 继续回到OnClientReading中剩余的部分。
上述过程就是解析接收到的数据包的过程,分析一下:读出包中的标志与大小,验证接收的包是否合法。取出包中的标志、整个包的大小、未压缩数据的大小。读取出压缩数据,并进行解压处理。将解压后的数据存储到解压缓冲区中。如果上述过程有任意差池则调用Send(pContext, NULL, 0);要求受控端从新发送数据。在这里我们假设,当主控端在接收数据的时候发生了错误,则会仅仅向被控端发送一个含有传输标志的数据包,我们跟踪一下这个数据包的传输过程。这里,我们不得不回到被控端去分析被控端的这个通信DLL。上次,我们已经分析到向主控端发送了上线包的过程,在连接到了主控端之后,新开启了一个工作线程在函数CClientSocket::Connect中m_hWorkerThread = (HANDLE)MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WorkThread, (LPVOID)this, 0, NULL,true);接下来去看看这个工作线程的实现过程:
本工作线程中使用了Windows套接字模型中的选择模型,对选择模型进行一个简单的阐述。Select模型是指:将需要探测的套接字句柄组织到一个套接字集合中,然后在一个循环中调用Select函数,如此当这些需要探测的套接字集合中的任意一个发生网络事件,则该函数会返回发生网络事件的套接字数量,并且将集合中没有发生网络事件的套接字句柄剔除掉,如果在规定的事件内没有发生网络事件,则直接就返回。在本工作线程中的应用是:将用于与主控端进行通信的套接字句柄放到fdSocket中,每次调用Select函数来探测此套接字上是否有数据到来,而等待的时间则设置为无限长的时间,也就是说,如果这个函数返回并且没有出现错误的话,则一定是在这个套接字上有数据到来,因此在本次分析中,要求重新发送数据的数据包到达被控端之后,数据会被接收到buffer中,然后调用OnRead函数来具体的解析这个包中的内容。我们取看看OnRead这个函数的实现。这里的CClientSocket::OnRead函数与主控端的CIOCPServer::OnClientReading比较像。
其实在本次分析中指分析这一小块就够了,因为本次发送过来的是要求被控端重新发送上线包的数据包,因为在这里dwIOSize == FLAG_SIZE 并且包中只有传输标志,因此在这里只是将备份的数据重新发送了一份就ok了,但是为了我们描述的完整性,我们需要分析下这段代码的功能:将接收到得数据转储到压缩缓冲区中,取出传输标志进行校验。
逐一从缓冲区中取出数据,分别为:传输标志、包的总大小、为压缩前数据的大小、压缩数据。然后对压缩数据进行解压缩处理。
然后将解压缩的数据往另外一个鉴别指令类型的函数中派发,这个函数我们在这里就不细看了,等后续课程中会详细分析每一个指令的用途。当然,如果在上述过程中有任何的差错的话,也会要求主控端重新发送控制命令。void CKernelManager::OnReceive(LPBYTE lpBuffer, UINT nSize){ switch (lpBuffer[0]) { case COMMAND_ACTIVED: //激活命令 break; case COMMAND_LIST_DRIVE: // 文件管理 break; case COMMAND_SCREEN_SPY: // 屏幕查看 break; case COMMAND_WEBCAM: // 摄像头 break; case COMMAND_AUDIO: // 摄像头 break; case COMMAND_SHELL: // 远程sehll break; case COMMAND_KEYBOARD: //键盘记录 break; case COMMAND_SYSTEM: //系统管理 break; case COMMAND_DOWN_EXEC: // 下载者 break; case COMMAND_OPEN_URL_SHOW: // 显示打开网页 break; case COMMAND_OPEN_URL_HIDE: // 隐藏打开网页 break; case COMMAND_REMOVE: // 卸载, break; case COMMAND_CLEAN_EVENT: // 清除日志 break; case COMMAND_SESSION: //信息交互 break; case COMMAND_RENAME_REMARK: // 改备注 break; case COMMAND_UPDATE_SERVER: // 更新服务端 break; case COMMAND_OPEN_PROXY: // 开启代理 break; case COMMAND_OPEN_3389: // 开启3389 break; case COMMAND_NET_USER: // 无NET加用户 break; case COMMAND_HIT_HARD: // 硬盘锁 break; case COMMAND_REPLAY_HEARTBEAT: // 回复心跳包 break; } }上述整个过程就完成了被控端向主控端发起连接、并且发送上线包、主控端接收上线包、并假设接收出错并要求被控端重新发送上线包,这整个过程,我们稍后会讨论主控端正确的接收到数据之后,是对数据如何进行处理的,就是将上线包中的各种信息给咱排列到List控件中。在这里我们先把被控端连接到主控端的这整个过程剩余的部分进行一下讲解,然后我们再去主控端看数据的组织过程。
实例化了一个CKernelManager实例。
这个实例化的过程,我们在前面也讲述过,这里只提几个变量的作用:m_strKillEvent:主控端要求结束自身的时候,会创建一个名为m_strKillEvent的事件对象,如此连接到主控端的大循环会推出。m_strMasterHost/m_nMasterPort记录主控端的IP地址与Portm_nThread:记录由自己创建的所有的线程。m_bIsActive:记录是否已经激活,对这个变量我们会在后续详细分析。在这里需要注意,CKernelManager继承自CManager这个类,因此在CKernelManager这个类被初始化的同时,CManager这个类也同时被初始化了,在这里我们看看这个初始化的过程。
这里就是将这两个变量记录在成员变量中。CManager::m_pClient 记录 CClientSocket类的实例化对象。CClientSocket::m_pManager记录CManager实例化的对象,接下来还要修改之。为什么要在CManager::m_pClient中记录CClientSocket。这是因为,在后续文件管理、进程管理、桌面管理、视频管理、远程CMD等功能执行的时候,都会执行类似于文件管理的操作:
创建一个只用于文件传输的socket,注意当这个socket连接到主控端的时候,因为它不是上线包,因此记录被控端数据结构的ClientContext里面的m_bIsMainSocket就是为false。而不像正常的被控端登陆的时候,首先会发送一个上线包,以此才认为这个socket是主代表被控端的主socket。创建了这个用于传输文件的socket之后,还要创建一个用于管理本次文件传输会话的调配中心。调配中心的一个核心任务就是重载了虚函数OnReceive,这样当主控端使用这个连接的套接字进行发送命令的时候,被控端用于文件传输的这个socket在接收到文件传输类的相关命令的时候会将命令发送到文件管理的调配中心。这个过程的实现就是靠虚函数来实现的:在ClientSocket::OnRead中,有如下调用m_pManager->OnReceive(m_DeCompressionBuffer. GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());这个m_pManager在这里就是指代文件管理的调配中心,因此在CClientSocket::m_pManager中需要记录CManager实例化的对象。当文件管理模块需要将信息传输给主控端的时候,此调用:Send(lpPacket, nPacketSize);这个send函数实际上是CManager的,而在CManager的成员函数中,Send的实现又是靠CClientSocket的send函数:int CManager::Send(LPBYTE lpData, UINT nSize){ int nRet = 0; try { nRet = m_pClient->Send((LPBYTE)lpData, nSize); }catch(...){}; return nRet;}因此,需要在CManager中保存CClientSocket实例化的对象,保存该对象的变量的任务就落在了m_pClient的身上。 关于具体的功能分析,我们会在后续文章中详细的指出,在这里我们仅仅是提一点他们通信的一个运行机理,免得我们后面看到这部分内容的时候会吃力。
然后创建了一个初始状态为未授信、人工重置的事件对象,关于这个事件对象的使用方法,我们在后面使用的时候在讲解。 接下来在连接到主控端的这个大循环中,继续调用了socketClient.setManagerCallBack(&manager);这是重新将m_pManager设置为manager,指向没变,只是改变了虚函数表中的虚函数。关于这部分内容,不熟悉的C++不过关啊。以基类之指针指向派生类之对象的特性需要自行回顾。 我们需要回过头来看看,主控端收到上线包之后的操作流程,假设收到的包是正确的。CMainFrame::NotifyProc这个函数会被执行,并且因为我们完了所有的数据,因此会执行Case NC_RECEIVE_COMPLETE: ProcessReceiveComplete(pContext);而在CMainFrame::ProcessReceiveComplete这个函数中,因为我们接受的是上线包,因此会执行
看代码,我们知道如果是上线包上线的话,在这个逻辑段里只执行了三个方面的内容:第一:将到来的客户端添加到ListView控件第二:将该ClientContext的m_bIsMainSocket设置为TRUE第三:向被控端发送激活指令下面我们来一条一条的看这几个功能首先,我们看看向ListView添加一条记录的方式,本例中是通过发送一条自定义的消息——WM_ADDTOLIST,然后由CGh0stView::OnAddToList函数去响应这条消息。
以上所有的操作都很简单,就是逐一解析数据包的内容,然后将各个项插入到ListView控件中。
解决重复上线的问题,如果前三个项目相同,则视为重复上线,删除之,并且更新当前连接的数量:
继续往下看:
准备ToolTips的文本。然后根据是否有无纯真数据库,从数据库中提取地理位置,然后将地理位置增添到ListView的一列中再接下来指定唯一标示,这个唯一标示就是用ListView的一条记录来保存与该条相关的唯一数值,这个值非常有用。然后根据配置信息m_bIsDisablePopTips判断是否应该弹出Tips。关于托盘图标的编程,我们会在后续的Gh0st界面编程中详细讲解。 然后我们看看将m_bIsMainSocket设置为TRUE的作用,因为对任意一个被控端它连接上主控端之后,它与主控端之间并非就一个连接,因为每一种大的功能还需要开启一对新的连接进行数据交互,这样连接到主控端的每一个被控端都有好几个ClientContext,而这多个代表每一个连接的数据结构中只有一个的socket是主socket,其它的都是因为某种功能的需要而临时开启的。而ClientContext种类的划分就是根据这次上线是否发送上线包来判断本次的ClientContext.Socket是否为主socket。 最后,我们来看看主控端向被控端发送激活指令的过程,当然发送的这个过程是相当的复杂的,但是我们已经将这整个的发送过程分析的清晰易懂,大体总结下就是,在Send函数中进行数据包的组织,然后向完成端口提交一个发送数据的请求,然后完成端口调用发送数据的函数将这部分数据发送出去,而这个时候的被控端的行为呢,在ClientSocket的工作线程中,因为有数据的到来,所以Select函数返回,这个时候读取数据,然后对数据进行解析,再然后调用CKernelManager::OnReceive对不同的命令包进行不同的处理,这个时候会执行:switch (lpBuffer[0]){case COMMAND_ACTIVED: InterlockedExchange((LONG *)&m_bIsActived, true); break;将m_bIsActive设置为TRUE。 然后我们继续看连接到主控端的这个大循环:
因为,m_bIsActive被设置为了TRUE。因此,for循环会退出,但是这个时候不能让我们的程序退出啊,必须用一种方法down住这个连接函数的继续执行,因为主线程都退出了,还通信个蛋。所以,后面的这个do——while循环就是为此目的而存在的。这样主线程没有退出,而工作线程不断的接收主控端传输过来的命令并处理,就完美的完成了远控的执行功能。
2012-6-8-01:48
Made By Deadline