界面篇等我先搞完这个通信协议解析再说,要不我老觉得自己是在扯淡。在这里我也给自己这两天搞的协议解析找个网络存储做一下备份。
Gh0st通信协议解析(1)
正所谓蛇打七寸,今天我们对gh0st的通信协议进行一个完整的解析,看看gh0st这款远控的核心技术的来龙去脉。
*******************************************************************************
从主控端初始化IOCP服务器开始讲起
IOCP服务器是在CGh0stApp::InitInstance这个函数中被调用的,实际上是调用了CMainFrame的一个成员函数:CMainFrame::Active。看看这个函数都做了哪些事情。
首先判断这个m_iocpServer全局变量是否已经指向了一个CIOCPServer,如果是的话,就要先关闭它,并且删除掉这个CIOCPServer所占的内存空间。到这里我有些犹豫要不要去追踪这个CIOCPServer::Shutdown函数呢,一方面,如果去追踪的话,我们将不得不深入到CIOCPServer这个类里面去抽丝剥茧,寻找Shutdown函数,以及这个函数内部所调用的一系列的函数,这样我不得不深度遍历整个调用过程。另一方面,如果追踪吧,会使得这篇文章的主线凌乱掉,如果不追踪吧,我自己会凌乱掉,思前想后,宁可让大家疯掉,也不能让我疯掉,因为我疯掉了就写不出这个分析文章了……
好了,我们接下来看看这个CIOCPServer::Shutdown这个函数内部的一个执行逻辑。
先来看看这个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,如果为false那CIOCPServer本身就没有开启,还关闭个屁,如果为true的话,接下来就将其运行状态设为false。
然后看看CIOCPServer::m_bTimeToKill这个变量。这个变量的作用:
1:在CIOCPSserver的构造函数中有这么一句:m_bTimeToKill = false;
2:在CIOCPServer::Shutdown这个函数中有这么一句:m_bTimeToKill = true;
3:在核心的完成端口循环中有这么一句:
for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false; )
可以看出,m_bTimeToKill这个值就是一个记录是否结束掉在完成端口的所有等待线程的这么一个哨兵值,如果该值被设置为true,那么所有等待在完成端口上的线程都将结束并退出,这样,就不会再有工作线程来处理这个完成端口上的所有Read、Write、Initize的请求,在这里要明白一点:工作线程数是跟运行主控端的机子上的核心数相关的。
接下来,我们程序的流程就到了CIOCPSserver::Stop这个函数里了,这个函数的一个主要的功能就是结束掉这个监听的socket,已达到后续的连接请求不再受理。我们看看在这个函数里CIOCPServer都做了哪些处理:
首先来看看这个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::Stop中SetEvent(m_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个线程,不断的去查询完成端口上是否有读、写的这些请求+多个向此完成端口上投递读取或者写入请求的操作+真正的去处理这些读、写请求的操作。以上就是一个大概的完成端口执行情况。
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();
进行了一个赋值的操作,创建的是一个自动重置、非受信的一个事件对象。接下来是
int nRet = 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,看看在这个函数中都进行了哪些操作。
首先,先看看CIOCPServer::m_nWorkerCnt这个变量。
1:在CIOCPServer::InitializeIOCP这个函数里,对这个变量有这么个操作
可以看到,这个值是代表了,为完成端口服务的线程的数量。
2:在CIOCPServer::ThreadPoolFunc函数中,对这个变量有这么个操作
InterlockedDecrement(&pThis->m_nWorkerCnt);
这个操作是在某个为完成端口服务的线程即将结束的时候的一个操作
3:在CIOCPServer::CloseCompletionPort函数中,对这个变量的操作就不多少了
通过以上的分析,我们可以得出这样一个结论:这个m_nWorkerCnt的值就是代表了为完成端口服务的线程数量,因为此值是全局变量,因此各个线程对此值的访问时共享的,必须对此值的访问进行一个同步处理—原子操作。
接下来,我们重点看看这个很有意思的循环
while (m_nWorkerCnt)
{
PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD) NULL, NULL);
Sleep(100);
}
为什么说这个循环有意思呢,因为这个循环的这个循环体是给每一个为这个完成端口服务的线程发送一条结束自身的指令。它是如何实现的呢?且听我慢慢的道来……
首先,在CIOCPServer::ThreadPoolFun这个服务线程中,有这么一块代码
这个for循环的结束条件有两个,一个是m_bTimeToKill被置为True,再一个是bStayInPool被置为False.第一种条件在前面我们已经讨论过了,现在讨论第二种情况,在什么情况下bStayInPool会被置为False?
本人所述以下内容因为跟作者的源码有出入,不敢苟同原作者,但是有没有好的证据来证明我的正确性。因此,我对以下红字部分的内容不负责,有错误之处请谅解…………
以上代码就是我认为作者逻辑极度混乱的地方。分析这块代码的上下文我们可以发现,这段代码的用意就是对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这个函数里往下看。
关闭掉这个完成端口句柄,这个就没啥说的了。
接下来,看看CIOCPServer::CloseCompletionPort函数中对它自身维护的这个ContextList的一个处理的过程。
1:m_listContexts被实例化的地方
ContextList m_listContexts; //记录当前CIOCPServer保存的ClientContext的一个链表
ContextList m_listFreePool; //记录当前释放的ClientContext的一个链表
这两个变量非常有意思,看看ContextList的一个原型
typedef CList
这一行代码包含的信息量蛮大的,首先我们需要明白几个概念,以及这几个概念之间的联系:模板类、类、对象。不明白的童鞋翻翻C++的基础教材,都有讲。我用一句话概括三者之间的关系:类是类模板的实例化、对象是类的实例化。
上面这句代码的含义,就是将类模板给实例化成一个ContextList类。
然后在CIOCPServer的构造函数中将这个类实例化成两个对象:m_ListContexts以及m_ListFreePool。
2:在CIOCPServer::OnAccept函数中,对该变量有以下的操作
m_listContexts.AddTail(pContext);
由此,我们可以得出这样一个结论,m_listContexts就是保存了当前与主控端保持连接的被控端实例,这个链表中的一个元素就代表了一个被控端;而m_listFreePool则保存了当前被释放掉的ClientContext结构,以便当有新的连接到来时,快速的分配此结构用于保存客户端数据结构,由下图可以看出此用途
回收此结构
分配此结构
讲到这里,我们必须要简略的提一个重要的数据结构,这个数据结构描述了受控端的各种信息。
关于此数据结构各个字段的详细含义,我们会在后续的new ClientContext的操作中进行讲解,在这里我们先对此数据结构有一个感性的认识,对各个字段有个大体的印象,因为在接下来我们分析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的通信协议,关于界面的在这里我们就简略带过。
首先,我们需要对这款远控的一个核心技术进行回顾,此款远控的主控端是可以根据核心数来有针对性的创建为完成端口服务的工作线程的,并且会根据当前cpu的一个工作状态来调整这些工作线程的数量,再加上监听线程,再加上一个主线程,我们可以得出这样的结论,在这个远控主控端进程中是允许着很多个线程的,这就牵扯到了一个让人容易蛋疼的问题,多线程编程的时候要注意对全局变量访问的时候的互斥保障,以及当多个线程都有可能执行某一段代码的时候,还要对这段代码添加一个互斥访问的锁(本程序对这种情况的解决问题就是利用临界区)。
说到多线程编程,我又想到当年我写一款软件的时候的情形,当时每弹出一个对话框的时候,都会在这个对话框的OnInitDialog函数中开启一个线程,在这个线程中接收服务端发送过来的数据,但是由于当时在对话框退出的时候没能够及时的结束掉开启的这个线程,以至于再次开启对话框接收数据的时候,接收到得数据总是不可控的,当时就是找不出问题的所在,现在看来错误是很明显的了,就是因为对多线程编程不熟悉~
罗嗦了那么多就是想引出这个Clock结构体,正是因为这个结构体的存在,才保证了多线程运行的时候对关键代码的一个序列化执行,从而保证了执行结果是可信的。
注意在这个结构体的构造函数中调用了Clock::Lock函数,这个函数里有一个进入临界区的函数调用,因此在本例的应用中,在函数的开始处就调用了
CLock cs(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为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。
解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET。
接下来的这个while循环比较有意思:
while (!HasOverlappedIoCompleted((LPOVERLAPPED)pContext))
Sleep(0);
这里是直接将pContext这个指针类型给强制转换成了LPOVERLAPPED这个指针类型,并且当做HasOverlappedIoCompleted这个宏的参数。这个地方这样的强制类型转换为什么会成功,并且能正常的执行呢?
看ClientContext的数据结构定义
再去看看这个OVERLAPPED数据结构的定义
再来看看SOCKET的定义:
typedef UINT_PTR SOCKET;
接下来我们再看看HasOverlappedIoCompleted这个宏的定义:
#define HasOverlappedIoCompleted(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);
其实这个函数的真身是在这里:
为使我们的讲解清晰性,在这里我们不会对这个函数功能进行一个系统的讲解,我们在这里只取我们需要的地方进行讲解,即case NC_CLIENT_DISCONNECT:
在这里是清除所有受控端的连接,这里包含两层意思:第一呢,是结束掉先前建立起来的socket连接,这包括在该socket上的一些悬而未决的处理请求。第二呢,就是在界面上也要去除掉在ListView控件上的一系列的上线记录。接下来要讨论的东西其实已经涉及到了界面编程的知识,我们在这里稍微一提,不作重点介绍……
PostMessage函数向框架发送一条WM_REMOVEFROMLIST消息,接下来看看处理这条消息的消息响应函数的定义。
以上这一段代码就是遍历ListView控件中各个记录存储的数据,从中找出要从控件中移走的记录的过程,这里涉及到界面编程中ListView控件的使用方法。移走的这个操作也非常简单,就是调用了ListView的一个成员函数:DeleteItem。
这里还要摧毁在某一次的会话过程中创建的一些Dialog。在前面我们已经对ClientContext这个结构有了一个基本的了解。以及知道了各个字段的一个大概含义。接下来我们需要重点看这两个字段:m_Dialog[0]、m_Dialog[1]。由于使用这两个变量的地方很多都是重复的,所以,在这里我们取一个典型的操作来阐述。
1:在ClientContext中这两个变量被定义 int m_Dialog[2];
2:在CGh0stView::OnOpenShellDialog中被赋值
LRESULT CGh0stView::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()。这个函数是用来更新连接数量的。
至此,这个OnRemoveFromList这个函数已经结束了,我们按照我们来的路回滚回去。我们到达这么一个地方:CIOCPServer::RemoveStaleClient这个函数中的最末端。看看这个函数原型,其实在先前我们已经见识了这个函数的原型,在这里我们重新细致分析一下。
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);
}
}
这个Clock cs的功能我们已经分析过了,就是保证此段代码必须是独享方式访问。接下来就是找到这个pContext在m_listContexts这个结构中的位置,并且将ClientContext里面的所有的缓冲区都清掉,然后从m_listContexts移动到m_listFreePool中。到这个地方,我们对函数CIOCPServer::RemoveStaleClient这个函数的分析也就到这里了。继续回滚……
CIOCPServer:: CloseCompletionPort这个函数也就分析完毕了。继续回滚到这个函数——
void CIOCPServer::Shutdown(),还剩下这么一点东西:
DeleteCriticalSection(&m_cs);
while (!m_listFreePool.IsEmpty())
delete m_listFreePool.RemoveTail();
删除互斥量+将m_listFreePool这个链表清空。至此CIOCPServer::Shutdown()这个函数也已经分析完
其实接下来,我们的工作只有两点,一个是分析CIOCPServer这个类的构造函数,再一个是分析CIOCPServer::Initialize这个函数。
走起,先看这个类的构造函数:CIOCPServer::CIOCPServer()
初始化套接字,创建互斥量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 (BOOL bStayInPool = 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);
这两个变量,一个用来记录当前进程中为完成端口服务的所有的线程,一个用于记录为完成端口服务的线程中当前处于工作状态中的线程数量。为什么有的线程是不处于工作状态中呢,看下面这段代码:
所有为完成端口服务的工作线程,他们都会有这么一个瓶颈,会等待在这个完成端口上,一直到这个完成端口有了读、写的请求,才会被唤醒。因此,在所有的工作线程中会有一大部分的线程是处于挂起状态的。
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列表控件处理.如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。
上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现,看下面的解释:
接收数据的时候,可能已经掉线
发送数据的时候,可能已经掉线。这两个地方就是对不保活的受控端进行一个清除的处理过程。在源码中的说明文件中,说是启用了“心跳包机制防止意外掉线..”其实源码中的这个机制并没有启用:
这两个功能都没有启用,其实启用了SIO_KEEPALIVE_VALS,这个功能,回复心跳包这个东西是由TCP栈来处理的,不用劳烦应用层自己去实现保证心跳包机制,所以在这一点,原作者有点多虑了。在后续的被控端上线的时候我们还要做一番阐述。
bPacketFlag:
传输的数据包中携带的标记字符串。主控端与被控端交互的过程中所发的数据包中含有标记。关于传输的过程中的一些数据包交互,我们在后面的内容中会作为重点去讲解,在这里我们就不再深入。
接下来,我们开始分析让主控端处于监听上线的部分,这个部分执行完之后,主控端如果没有上线主机主动来撩拨它的话,应该就算一个完整的启动过程了
以上就是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 (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这个功能回头再论述。
这一段代码的含义就是创建一个完成端口,这个完成端口的创建是为了以后将上线的被控端的socket与这个完成端口相关联起来,注意看MSDN上关于此函数的说明:
The CreateIoCompletionPort function can associate an instance of an opened file with a newly created or an existing input/output (I/O) completion port; or it can create an I/O completion port without associating it with a file。
也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图中所示的用法仅仅是创建了一个完成端口,而并没有与任何的句柄相关联。至于关联的情况,请看下面的例子:
这里是将各个上线的受控端与主控端的连接套接字关联到前面已经创建的这个完成端口上。
关于这部分的知识,我们会在后续的课程中加以详述。
接下来,我们需要继续看CIOCPServer::InitializeIOCP这个函数的剩余部分内容:
这部分内容就是查询出计算机的核心数,并且根据核心数自适应调整运行于本机器上的最大、最小线程数。
还有一个功能就是初始化了查询CPU使用状态的这么一个类,关于这个类的详细说明如下:
功能就一句话,就是查看当前CPU的一个使用情况的。
具体,我们来学习几个监视API函数。
1:PdhOpenQuery:先来看看MSDN上对这个函数的解释:
The PdhOpenQuery function creates and initializes a unique query structure that is used to manage collection of performance data.
这个函数创建并初始化一个用于管理性能数据集合的一个唯一的查询结构。
PDH_STATUS PdhOpenQuery(
IN LPVOID pReserved, // reserved
IN DWORD dwUserData, // a value associated with this query
IN HQUERY *phQuery // pointer to a buffer that will receive the
// query handle
);
最重要的就是phQuery返回来的这个句柄,因为在后续的查询中这个句柄会被频繁用到。
2:PdhAddCounter这个函数的使用方法。
PDH_STATUS PdhAddCounter(
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
);
这个函数的作用就是将自己关心的查询结构体添加到查询句柄中。
3:PdhCollectQueryData这个函数的使用方法。
PDH_STATUS PdhCollectQueryData(
IN HQUERY hQuery // handle of the query
);
这个函数的作用就是收集我们将要查询的性能参数数据。
4:PdhGetFormattedCounterValue这个函数的使用方法。
PDH_STATUS PdhGetFormattedCounterValue(
IN HCOUNTER hCounter, // handle of the counter
IN DWORD dwFormat, // formatting flag
IN LPDWORD lpdwType, // counter type
IN PPDH_FMT_COUNTERVALUE pValue
);
格式化输出我们查询的信息。
5:PdhCloseQuery这个函数的用法。
PDH_STATUS PdhCloseQuery(
IN HQUERY hQuery // handle of the query to close and delete.
);
关闭掉查询句柄。 综上所述,对某个性能信息的查询主要分以下五个步骤进行。
1:打开计数器 PdhOpenQuery;
2:把感兴趣的计数器添加进来 PdhAddCounter;
3:收集数据 PdhCollectQueryData;
4:得到计数器的数值 PdhGetFormattedCounterValue;
5:关闭计数器 PdhCloseQuery;
我们继续看CIOCPServer::InitializeIOCP这个函数的剩余部分。
就是创建了几个可以为这个完成端口进行服务的工作线程。至于线程的数量是根据当前机器的核心数相关的。
至此,被控端连接到主控端之前的所有通信协议基本上都已经分析完了。当然了在这里确实还剩下两个重要的函数没有去分析,一个就是刚才创建的这个线程的线程函数,而另外一个地方就是在监听线程里面当有被控端连接来的时候,调用的CIOCPSserver::OnAccept这个函数。好了就到这里,在下面的课程中我们会讲述有被控端机子主动来连接的时候,到正常的通讯过程,那个时候才真正的涉及到通信协议的传输。