BCB快速打造多线程端口扫描器
文章作者:灰狐
原始出处:灰狐's Blog ( www.HuiHu32.cN )
很久没发些什么了,整天只来学习而不奉献总感觉有点什么,呵呵,就把这篇拙作发出来吧,通过本文您能学到的东西是理解BCB中对于多线程类的详细使用方法。最近已经陆续开始考试了,唉,一个比一个头大。
注:本文已发表在2007年《黑客防线》第12期,网络首发地址为作者灰狐's Blog(www.huihu32.cn),
转载请注明出处!
最近复习Winsocket编程,才发现自己以前很多东西都忘的差不多了,以前只是简单地看书,然后把书上那段TCP的代码抄下来运行一遍成功后就自认为已经理解了网络通信编程。现在才知道自己是多么可笑,唉,希望大家引以为戒喔。
对于编程,根据个人的学习经验:多写是必须的,不管你看过多少代码。通常可以先定下一个难度不算太大(起码你知道大概可以怎样实现)的任务,然后就着手认真去做,遇到了问题就查资料,或参考别人的某个功能实现思路,坚持做完后你就会进步很多。
今天我们就要做一个扫描TCP端口是否开放的程序,原理很简单,就是写一个TCP的客户端程序,然后connect某个端口,根据是否响应来确认是否开放。当然了,它很容易被误导(譬如有防火墙的因素),不过我们重在练习,先做出来再说。(其实,本文的重点在于后面的多线程类使用,千万别到这里就扔下了哦)
首先用VC写一个演示的控制台程序,查看是否能达到目的,代码如下:(注:本文所有演示代码均有不同程度删减,完整版本请查看附带的源工程)
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32")
#define START 80 //起始端口
#define END 1025 //终止端口
int main(int argc,char *argv[])
{
int i;
WSADATA ws;
SOCKET sockfd;
struct sockaddr_in their_addr;
//初始化加载库
WSAStartup(MAKEWORD(2,2),&ws);
//设置连接信息
their_addr.sin_family = AF_INET;
their_addr.sin_addr.S_un.S_addr = inet_addr(argv[1]); //根据命令行参数1确定扫描IP
for(i=START;i<=END;i++)
{ //循环建立socket后连接
sockfd = socket(AF_INET,SOCK_STREAM,0);
their_addr.sin_port = htons(i);
printf("正在扫描端口:%d \n",i);
if(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == SOCKET_ERROR)
{ //如果连接失败直接进行下个端口的扫描
continue;
}
//否则认为此端口开放
printf("\n\t端口 %d 开放!\n\n",i);
}
closesocket(sockfd);
WSACleanup();
return 0;
}
整个代码的大体流程就是这样,非常简单,估计没人看不懂,先看一下效果,进入程序目录,拿www.sohu.com来测试吧,看我的截图图1:
不过这个程序中存在着一些非常明显的问题:程序执行速度极慢(即使是在扫描本机的时候每个端口也需要将近1秒的时间);受网络影响明显,比如我在测试sohu的时候一次扫描的很正常,但另一次却卡在81端口了;界面不够美观,呵呵,总得考虑用户体验度吧。
为了解决以上问题,今天我完全使用BCB 6.0来做这个程序,开发平台是Win XP SP2,采用多线程技术。(程序完整源工程已经打包附上,附有详细注释,可直接编译运行)
在附带的源程序中我做了详细的注释,本文中着重要讲的地方是怎样利用BCB中提供的多线程功能来使我们的程序执行速度得到大幅度提高。
直接使用API进行多线程编程通常来说要考虑的东西比较多,幸运的是BCB提供了一个功能强大的多线程类,但我们不能直接使用它,需要先派生出一个子类,然后在这个子类中完成具体的功能。派生子类的方法是使用向导:File|New|Thread Object,在Class Name中输入TScanThread后直接确定,进入代码编辑,这时可以看到向导生成的类中包含了两个方法:TScanThread和Execute,分别用来进行初始化和执行具体代码。下面我详细讲一下怎样使用它,因为这个类并不能直接使用主窗体中的控件,但很显然我们通常都需要与窗体中的各种控件进行联系。
假如在线程类中你要向主窗体的ScanResultMemo控件中添加扫描结果,可以这样做:首先在线程类的头文件中声明一个局部变量TMemo *AMemo;然后修改TScanThread这个构造函数,给添加一个形参改成如下:__fastcall TScanThread(bool CreateSuspended,TMemo *ResultMemo),并在实现过程中加入代码AMemo = ResultMemo;将构造函数的参数传递给局部变量AMemo,而在此类中是可以不受限制地使用AMemo这个变量的;在主窗体代码中创建线程的时候把实参传递给线程类的构造函数如下:
TScanThread test=new TScanThread(false,ScanResultMemo);
这样就完成了主窗体与线程类的联系。
从上面的过程中可以看出是比较麻烦的,所以我们通常需要实现做好规划,尽量减少需要传递的参数数量,否则如果线程类比较多的话最后它们之间错综复杂的交错关系一定会让你大脑崩溃的。
另外,通常不要把需要完成的功能代码直接写在线程类的Execute方法中,比较好的做法是先写一个成员函数,然后在Execute方法中调用这个函数;比如本程序中就是先声明了函数void __fastcall ScanPort();完成扫描功能,然后在Execute方法中调用它。在线程类的函数中除了构造函数外其他函数尽量不要带参数,否则很容易出错,需要使用参数的地方就使用成员局部变量解决。
汗,上面说的似乎比较混乱(什么似乎,本来就乱,该打~~),不过大家一定要理顺关系哦,不然就麻烦了,连它是怎么运作的都不懂怎么控制它呢?这种机制初学起来似乎很是让人迷茫,但只要搞懂了它运行的流程就一点难度都没有了,真正的RAD啊。
说了这么多,估计有些人已经头晕了,还是看代码直接感受一下吧:
线程类的构造函数如下,注意参数已经修改了,头文件中也要修改:
__fastcall TScanThread::TScanThread(bool CreateSuspended,int iPort,char *sIp,TMemo *ResultMemo) : TThread(CreateSuspended)
{ //port、AMemo、IpBuffer是分别在头文件中定义的局部变量
port = iPort;
AMemo = ResultMemo;
sprintf(IpBuffer,"%s",sIp);
}
新添加一个执行核心功能(即完成扫描)的函数如下:
void __fastcall TScanThread::ScanPort()
{
WSAStartup(MAKEWORD(2,2),&ws);
sockfd = socket(AF_INET,SOCK_STREAM,0);
their_addr.sin_family = AF_INET;
their_addr.sin_addr.S_un.S_addr = inet_addr(IpBuffer);
their_addr.sin_port = htons(port);
if(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == SOCKET_ERROR)
{
return;
}
AMemo->Lines->Add(" 开放端口:"+AnsiString(port));
closesocket(sockfd);
WSACleanup();
return;
}
跟最上面的那段测试代码重复了,这里仍然贴出来是为了让大家真正从代码中理解参数传递的过程,这里使用的是构造函数中初始化后的局部变量。
Execute方法的实现为:
void __fastcall TScanThread::Execute()
{
ScanPort();
}
很简单,只有一句函数调用。其实Borland推荐的做法是Synchronize(ScanPort);这样调用可以自动管理防止不同线程实体访问资源冲突,但也可以不用这种方法。
我们再来看一下在主窗体中该怎样启动这些线程,我们需要将IP,端口等传递给它们。注意不要忘了包含线程类的头文件。
在头文件中定义:TScanThread *thread[1000];
实现启动线程的核心代码为:
for(i=StrToInt(StartPortEdit->Text);i<=StrToInt(EndPortEdit->Text);i++)
{
thread[i-StrToInt(StartPortEdit->Text)]=new TScanThread(false,i,
IpAddressEdit->Text.c_str(),ScanResultMemo);
}
这样可同时启动1000个线程,很快就能搞定,线程又是并发执行的,所以扫描速度提高了上百倍。我测试的结果是扫描1000个端口不到2秒。但线程的数量可不是没有限制的,经我的测试似乎最多只能开2047个线程,超过的话就会提示存储空间不足引发异常。
这里有两点需要注意:由于启动线程的速度非常快,并且启动后不管,所以有时就会出现程序已经提示扫描结束了之后才会显示出扫描结果;还有就是这里任务最重的其实还是主窗口,如果要扫描全部65535个端口的话它就要不停地循环启动线程,但每个线程的工作却很快,所以这时仍然会造成界面一时无法响应用户的情况。第一个问题可以尝试采取其他的扫描方法,第二个问题可以加重线程的工作量,比如在每个线程中扫描10个连续端口,这样主线程就可以只做1/10的工作了,呵呵,好轻松,但这样会使得返回结果延迟的现象更加严重。
在我给出的实例程序中,由于同时只能开1000个线程,所以需要判断连续端口是否超过了1000,没有的话直接用循环启动线程就可以了,但如果超过 1000就需要一点小小的算法功底了,我设计了一种算法在运行时当数量比较大的时候就会有问题,BUG实在太大,又跟踪不出来问题出在什么地方(汗,数据结构没学好),就暂时把这个功能去掉了,这段代码没有删除,只给注释掉了,大家可以参考一下吧。
OK,一个简单但快速的多线程TCP端口扫描模型就完成了,快拿去炫耀吧,嘿嘿,还可以完善下使之具备扫描整个网段的功能哦,欢迎大家共同交流、进步,联系QQ:544341822,Blog:www.huihu32.cn (32位灰狐,嘿嘿)。
代码在哪里呢?