使用MFC编写internet查询程序
杜经农
在VC++ 5.0中,MFC的WinInet类包装了相关的用于internet 客户机程序编程的win32 API函数。这样,无需了解winsock或TCP\IP的细节就可以编制出internet客户机程序。本文中,我们将探讨如何使用WinInet类来编写一个internet查询程序,该程序起名为“探路者”。该程序能使用各种协议来查询网络,包括古老的FINGER和WHOIS。
顾名思义,“探路者”用于探查一部internet服务器的情况,查看它能提供哪些服务。例如说,你知道某人的e-mail地址,假设是wang@eop.com,我们就可以通过该程序,尝试用各种办法建立到eop.com的链接,随后对该指定域依次采用WWW查询、FTP查询、GOPHER查询、FINGER查询和WHOIS查询。根据返回的结果,可以判断该域能提供哪些服务。此外,如果你想给自己的服务器起某个域名,你也可以通过本程序知道该域名是否已被采用。
一、查询程序界面的实现
本程序的用户界面将基于对话框来实现。建立界面的步骤如下:
在developer studio中,
1、选择file-new…,高亮MFC appwizard(exe),在Project name栏内给程序取名为query ,单击ok。
2、在step-1中,单击dialog based,选English做为资源语言。单击next。
3、在step-2中,取消ActiveX Controls的选中标记。不要选择windows socket(该程序不直接调用socket函数),在对话框标题栏中键入“探路者”。
4、在step-3中,选择yes,please和As a ststically linked library
5、在step-4中,不要改变appwiard所生成的任何类名,appwizard结束后,我们准备着手编写程序的核心部分。
appwizard建立了一个空的对话框,我们将从它的基础上开始工作。下面编辑对话框:
选resource view ,展开query resources-dialogs,双击IDD-QUERY-DIALOG 资源,下面编辑对话框:
1、将OK按钮改为“查询主机”,右击该按钮,选择properties,将该按钮的ID名改为id-query。
2、将CANCEL按钮改为“结束查询”。
3、删除to do等静态文本。
4、将对话框增大到300像素宽
5、在对话框顶端增加一个编辑框,资源id名为IDC-HOST,将编辑框拉伸到最宽。
6、给编辑框增加一个名为“地址名”的标签。
7、将对话框拉伸到150像素高。
8、在对话框底部增加另一个编辑框,id名为IDC-OUT,将它拉伸到尽可能的大,填满对话框的剩余空间。
9、给该编辑框增加Multi-line,Horizontal scroll,Vertical scroll,Border,Read-only等特性。
当用户单击“查询主机”按钮时,程序将使用各种方法查询网址。因此,需使用classwizard,把实现查询功能的代码链接到query按钮。
1、选view-classwizard。
2、选CQueryDlg类,主机名将由该类接受。
3、高亮ID-query,高亮右边列表栏里的BN-CLICKED。
4、按Add Fuction按钮,增加一个函数。
5、Class Wizard建议的名字为OnOk,将它改为OnQuery ,,单击ok
6、单击成员变量栏,准备将edit控制连接到对话框类的变量
7、高亮IDC-HOST并单击Add Variable。将把EDIT控制连接到对话框类的Cstring成员变量,名为m_host
8、类似以上步骤,将IDC-OUT链接到CString变量m_out
点击ok关闭classwizard,剩下的工作是编写CQueryDlg::OnQuery()函数,它将使用m_host值进行查询并将结果输出到m_out变量。
二、编写代码,进行http查询
查询一个internet地址时,首先应尝试建立http链接。这是因为大量的地址都含有web页面。使用http建立链接的最简单的办法是使用winlnet类CInternetSession,并调用其成员函数OpenURL()。该函数将返回一个文件,我们在m_out中显示该文件的头几行文字。首先,在QueryDLG.CPP的开始加入以下行:
#include‘afxinet.h’
这将定义访问Winlnet类的代码。由于本程序将访问大量的URLS,在CQueryDlg中增加一个名为TryURL的函数,它含有一个CString类的参数,名为URL,返回值为void。右击CQueryDLG类,选择Add Member function…。增加函数TryURL(),将它定义为protected类型。这个新函数将从CQueryDlg::OnQuery()中调用。在OnQuery()中增加以下代码:
void CQueryDlg::OnQuery()
{ const CString http = "http://";
UpdateData(TRUE);
m_out = "";
UpdateData(FALSE);
TryURL(http + m_host);
TryURL(http + "www." + m_host);
}
UpdateData(TRUE)的调用将给m_host赋予用户定义的值。UpdateData(FALSE)语句将清空输出编辑框变量m_out的内容。接下来调用两次TryURL(),例如说,当用户输入microsoft.com,程序将首先试一试http://microsoft.com,然后试一试http://www.microsoft.com。以下是TryURL的代码。
void CQueryDlg::TryURL(CString URL)
{
CInternetSession session;
m_out+="正在链接"+URL+"\r\n";
UpdateData(FALSE);
CInternetFile* file=NULL;
try
{
file=(CInternetFile*)session.OpenURL(URL);
}
catch(CInternetException* pEx)
{
file=NULL;
pEx->Delete();
}
if(file)
{
m_out+="已建立链接。\r\n";
CString line;
for(int i=0;i<20&&file->ReadString(line);i++)
{
m_out+=line+"\r\n";
}
file->Close();
delete file;
}
else
{
m_out+="本地址没有发现http主机\r\n";
}
m_out+="------------------------------------------------------\r\n";
UpdateData(FALSE);
}
下面,我们对以上代码进行逐段分析。首先,建立一个internet区,这要定义一个CInternetSession的对象。其构造函数的原形如下:
CInternetSession( LPCTSTR pstrAgent = NULL, DWORD dwContext = 1, DWORD dwAccessType = INTERNET_OPEN_TYPE_PRECONFIG, LPCTSTR pstrProxyName = NULL, LPCTSTR pstrProxyBypass = NULL, DWORD dwFlags = 0 );这需要定义很多参数,但是,本程序都使用缺省值,即“=”号后的值。CInternetSession构造函数参数说明如下:
LPCTSTR pstrAgent-应用程序名,如果为NULL,它将替你填入你在AppWizard中给定的程序名。
DWORD dwContext-本操作的设备关联符定义。
DWORD dwAccessType-访问类型,为以下参数之一,INTERNET_OPEN_TYPE_PRECONFIG (default), INTERNET_OPEN_TYPE_DIRECT, 或 INTERNET_OPEN_TYPE_PROXY
LPCTSTR pstrProxyName-如访问类型为INTERNET_OPEN_TYPE_PROXY,则给该参数赋予协议名称。
LPCTSTR pstrProxyBypass-如访问类型为INTERNET_OPEN_TYPE_PROXY,则该参数为不通过协议服务器而直接链接的一系列地址。
DWORD dwFlags-可为以下参数,INTERNET_FLAG_DONT_CACHE, INTERNET_FLAG_ASYNC, 和INTERNET_FLAG_OFFLINE.
dwAccessType值缺省时将使用系统注册簿定义的值。显然,程序允许使用者定义访问类型将比由程序内部直接定义要好。因此,要正确使用本程序,必需先在windows系统中定义好网络访问类型,步骤如下:
1、双击桌面上“my computer"图标。
2、点击“contyol panel".
3、点击“internet ”。
4、在随后弹出的对话框中,选“connection"栏,然后填写网络连接属性。如果你是拨号上网,选中“dial”选择项,并填写相关属性。如果你是通过proxy服务器上网,选中“proxy”选项,点击“setting”按钮,设置proxy服务器地址和端口号。如果你是直接连入internet,应使所有的选项均为非选中状态。
本程序在构造CInternetSession对象时使用缺省值,因此,构造函数将不带任何参数。如下所示:
CInternetSession session;
在构造对象session后,我们需写两行程序作一些输出,表示程序已开始工作。
m_out+="正在链接"+URL+"\r\n";
UpdateData(FALSE);
接下来我们使用session对象的成员函数OpenURL()来打开一个URL资源。该函数返回一个文件的指针,文件类型为以下四种之一:
file:// 如果访问的是本地机器,函数返回一个CStudioFile类对象的指针。
ftp:// 如果访问的是一ftp地址,函数返回一个CInternetFile类对象的指针。
gopher:// 如果访问的是一gopher地址,函数返回一个CGopherFile类对象的指针。
http:// 如果访问的是一http地址,函数返回一个CHttpFile类对象的指针。
本程序用于访问远程机器,因此,函数不会返回一个file://类型的本地文件。而CGopherFile和CHttpFile均派生自CInternetFile,所以将该函数返回值赋给CInternetFile类的指针是安全的。当OpenURL()不能正常打开URL资源时,该函数将会抛出一个异常(exception),从而导致程序运行时错误。由于本程序用于探查未知的网址,该网址可能不提供相应服务或根本不存在,因此OpenURL()有时可能不会正常运行。为了避免程序异常终止,我们需使用try-catch结构来处理异常。本段程序代码如下:
CInternetFile* file=NULL;
try
{
file=(CInternetFile*)session.OpenURL(URL);
}
catch(CInternetException* pEx)
{
file=NULL;//如果发生运行时错误,给file赋空值,程序将继续运行
pEx->Delete();
}
通过以上代码,程序将使用用户指定的地址来试图打开Http网址,如果失败,返回的文件为空,程序将继续运行,尝试用其它协议打开网址。随后,无论网址是否成功打开,我们都应作相应输出以提示用户。如果成功,我们用一个for循环读出返回文件的头20句并输出,如果失败,也要给出相应的提示。程序如下:
if(file) //判定链接是否成功
{
m_out+="已建立链接。\r\n";
CString line;
for(int i=0;i<20&&file->ReadString(line);i++)
{
m_out+=line+"\r\n";
}
file->Close();
delete file;
}
else
{
m_out+="本地址没有发现http主机\r\n";
}
m_out+="------------------------------------------------------\r\n";
UpdateData(FALSE);
现在我们可以编译该程序并运行,键入地址microsof.com并查询,输出的信息表明,程序在http://microsoft.com和http://www.microsoft.com处均发现了www网页,并输出了主页的HTML文件的头20行。
三、ftp查询的实现
现在我们继续编程,来探查用户输入的网址是否提供ftp服务。ftp是internet上的一种文件传输协议,主要用于在服务器和客户机之间传送文件。重新调用TryURL()来达到目的是不可能的,因为TryURL()假设该网址指向一个文件,而一个ftp网址指向一系列文件。必需重新编写一个函数,仿照以前步骤,给CQueryDlg类增加另一个成员函数void TryFtp(CString host)。如果所探查的网址提供ftp服务,该函数将找出ftp缺省目录并显示出来。代码如下:
void CQueryDlg::TryFTP(CString host)
{
CInternetSession session;
m_out += "正在链接FTP地址 " + host + "\r\n";
UpdateData(FALSE);
CFtpConnection* connection = NULL;
try
{
connection = session.GetFtpConnection(host);
}
catch (CInternetException* pEx)
{
connection = NULL;
pEx->Delete();
}
if (connection)
{
m_out += "已建立链接。 \r\n";
CString line;
connection->GetCurrentDirectory(line);
m_out += "缺省目录为 " + line + "\r\n";
connection->Close();
delete connection;
}
else
{
m_out += "本地址没有发现ftp主机 。\r\n";
}
m_out += "------------------------------------------------------\r\n";
UpdateData(FALSE);
}
本函数和TryURL()很相似,不过,它不使用OpenURL()来打开一个文件,而是用GetFtpConnection()来与ftp服务器建立链接。如果链接成功,将使用GetCurrentDirectory()来得到服务器的缺省目录。
大多数ftp地址前有ftp.的前缀,但一些老的地址没有此前缀。我们可以在OnQuery()函数的末尾增加两行来调用这个新的函数:
TryFTP(m_host);
TryFTP("ftp."+m_host);
重新编译程序并运行,可以发现,在ftp.microsoft.com处提供ftp服务。在开始查询到出结果将有一些延迟,因为程序等结果全部探查到后再一次显示,如果我们希望结果实时地显示,必需使用异步套接字(asynchronous sockets)或多线程编程。
四、gopher查询的实现
GOPHER是一种基于文本的协议,它和WWW相似,可以通过点击文字内容,实现网络内的链接和浏览。但是,GOPHER是通过逐级文字菜单来组织链接和内容的,它不象WWW那样有丰富的多媒体页面。要实现GOPHER查询,我们应给CQueryDlg类增加另一个成员函数void TryGopher(CString host)。如果查询成功,该函数将返回查询地址的第一个Gopher位置(locator)。位置是gopher协议的概念,任何gopher客户程序必需先得到一个gopher位置,然后才能进行相应的gopher操作。TryGopher()函数如下:
void CQueryDlg::TryGopher(CString host)
{
CInternetSession session;
m_out += "正在链接gopher地址 " + host + "\r\n";
UpdateData(FALSE);
CGopherConnection* connection = NULL;
try
{
connection = session.GetGopherConnection(host);
}
catch (CInternetException* pEx)
{
connection = NULL;
pEx->Delete();
}
if (connection)
{
m_out += "已建立链接。 \r\n";
CString line;
CGopherLocator locator = connection->CreateLocator(NULL, NULL, GOPHER_TYPE_DIRECTORY);
line = locator;
m_out += "第一个Gopher位置是" + line + "\r\n";
connection->Close();
delete connection;
}
else
{
m_out += "本地址没有发现gopher主机 。 \r\n";
}
m_out += "------------------------------------------------------\r\n";
UpdateData(FALSE);
}
本函数和前两个函数大致相似,在通过调用connection = session.GetGopher Connection(host);来建立Gopher链接后,通过语句CGopherLocator locator = connection-> CreateLocator(NULL, NULL, GOPHER_TYPE_DIRECTORY);来建立一个Gopher位置(locator),函数CreateLocator()有多个版本,现在我们使用的是其含三个参数的版本。其原形为CGopherLocator CreateLocator( LPCTSTR pstrDisplayString, LPCTSTR pstrSelectorString, DWORD dwGopherType );。其中参数pstrDisplayString指明要查询的服务器上的具体文件或目录名,如果它为NULL,则返回服务器缺省目录名;参数pstrSelectorString是发送给服务器的字符命令,以便检索某个项目,它可设为空;参数dwGopherType指明Gopher访问类型,本例中定义为GOPHER_TYPE_DIRECTORY,指明要访问的是目录。它的其它可取值请参考VC++5.0文档。Gopher位置(locator)建立后,我们把它强制转换为Cstring类型,并把该位置显示出来。在OnQuery()函数的末尾加入以下行:
TryGopher(m_host);
TryGopher("gopher." + m_host);
重新编译程序,输入地址harvard.edu,程序将会探查出它是一个Gopher地址,并显示出第一个Gopher位置。
五、使用Gopher来实现Finger查询
Finger协议的作用是给你提供一个网址的具体情况,它是Internet上最古老的协议之一。在一个Finger服务器上,你可以查询它的某一个用户或整个网址的情况。当然,这对网络的安全是不利的,实际上,有经验的黑客们在攻击一个未知网络时,第一步就是向它发送Finger和Whois查询,这也是黑客网址上的黑客教程中建议的步骤。为了安全,许多网络服务器不提供Finger服务,然而,当它接受到Finger查询请求时,仍然会返回一些其它的有用信息。
在MFC和WIN32 API中,没有提供直接实现Finger查询的函数,但是,我们仍有变通的办法来实现它。所有的internet链接都需要一个宿主名和端口号,所有的著名的服务都有其特定的端口号,例如:http服务使用远程宿主机上的端口80,ftp服务使用端口21,gopher服务使用端口70。对于finger服务来说,它使用端口79。finger是一种简单的协议,如果你向远程宿主机的端口79发送字符串,finger服务器在端口79侦听到后,将会发送出一个finger回答。如果你发送的字符串仅仅包含\r\n,服务器通常将会把本服务器上所有用户的列表及相关信息(如用户真实姓名等)做为应答返回。因此,如果我们不使用缺省的端口70,而是使用端口79来建立gopher链接,我们就能发出finger查询。给CQueryDlg类增加一个成员函数void TryFinger(CString host)如下:
void CQueryDlg::TryFinger(CString host)
{
CInternetSession session;
m_out += "正在链接finger地址 " + host + "\r\n";
UpdateData(FALSE);
CGopherConnection* connection = NULL;
try
{
connection = session.GetGopherConnection(host,NULL,NULL,79);
}
catch (CInternetException* pEx)
{
connection = NULL;
pEx->Delete();
}
if (connection)
{
m_out += "已建立链接。 \r\n";
CGopherLocator locator = connection->CreateLocator(NULL, NULL, GOPHER_TYPE_TEXT_FILE);
CGopherFile* file =NULL;
try
{
file = connection->OpenFile(locator);
}
catch (CInternetException* pEx)
{
file = NULL;
pEx->Delete();
}
if (file)
{
CString line;
for (int i=0; i < 20 && file->ReadString(line); i++)
{
m_out += line + "\r\n";
}
file->Close();
delete file;
}
else
{
m_out+="finger查询失败。\r\n";
}
connection->Close();
delete connection;
}
else
{
m_out += "本地址没有发现finger主机 。 \r\n";
}
m_out += "------------------------------------------------------\r\n";
UpdateData(FALSE);
}
本函数中,语句connection = session.GetGopherConnection(host,NULL,NULL,79);用于建立finger链接。随后,我们创立一个文本文件类型的Gopher位置用来操作服务器返回的信息:CGopherLocator locator = connection->CreateLocator(NULL, NULL, GOPHER_TYPE_TEXT_FILE);。使用该Gopher位置打开文件并使用一个for循环来读出该文件的头20行,随后将它显示出来。
在OnQuery()函数的末尾加入以下行:TryFinger(m_host);。编译程序,输入地址whitehouse.gov,程序将会返回该服务器的e-mail地址,从返回的信息可知,出于安全考虑,该服务器的其它finger服务已被取消了。如果你输入的网址没有提供finger服务,程序将有较长一段时间的延迟,最后弹出一个消息框,通知用户链接出现超时错误,单击ok按钮即可。
六、使用gopher协议发送whois查询
还有一个协议也能提供网址的相关信息,它也是一种古老的协议,MFC并不直接支持它,这就是whois协议。在整个internet上,只有少数服务器提供whois服务。whois服务建立了internet上的域名数据库,如果对某个域名进行whois查询,服务器将会返回拥有该域的机构或个人的实际姓名、地址、电话号码等信息。国际上的域名注册机构拥有whois服务器,例如,域名结尾为.com的域都在一个称为InterNIC的机构中注册,该机构拥有一个whois服务器称为rs.internic.net。whois协议和finger协议一样,是一种简单的协议,它使用端口43,如果向whois服务器的端口43发送包含域名的字符串,则whois服务器将会返回该域拥有者的情况。给CQueryDlg类增加一个成员函数void TryWhois(CString host)如下:
void CQueryDlg::TryWhois(CString host)
{
CInternetSession session;
m_out += "正在链接Whois地址 " + host + "\r\n";
UpdateData(FALSE);
CGopherConnection* connection = NULL;
try
{
connection = session.GetGopherConnection("rs.internic.net",NULL,NULL,43);
}
catch (CInternetException* pEx)
{
connection = NULL;
pEx->Delete();
}
if (connection)
{
m_out += "已建立链接。 \r\n";
CGopherLocator locator = connection->CreateLocator(NULL, host, GOPHER_TYPE_TEXT_FILE);
CGopherFile* file = NULL;
try
{
file = connection->OpenFile(locator);
}
catch (CInternetException* pEx)
{
file = NULL;
pEx->Delete();
}
if (file)
{
CString line;
for (int i=0; i < 20 && file->ReadString(line); i++)
{
m_out += line + "\r\n";
}
file->Close();
delete file;
}
else
{
m_out+="Whois查询失败。\r\n";
}
connection->Close();
delete connection;
}
else
{
m_out += "Whois查询失败。\r\n";
}
m_out += "------------------------------------------------------\r\n";
UpdateData(FALSE);
}
在本函数中,语句connection = session.GetGopherConnection("rs.internic.net",NULL, NULL,43);使程序链接到了whois服务器rs.internic.net。随后,我们建立一个gopher位置来查询用户输入的域:CGopherLocator locator = connection->CreateLocator(NULL, host, GOPHER_TYPE_TEXT_FILE);由于链接的域为rs.internic.net,所以,本函数只能查询结尾为.com的域。读者可以对本段程序进行扩充,链接到其它相关whois服务器,以查询其它类型的域。
在OnQuery()函数的末尾加入以下行:TryFinger(m_host);。重新编译程序,现在,我们可以对结尾为.com的域进行whois查询了。
至此,本程序已全部结束。当然,有兴趣的读者可以对它进一步扩充,以完成更多的功能。可以对WinInet类进行一些小小的扩充,以便能通过特定的端口访问e-mail和news服务。此外,也可以链接到一些著名的网络搜索引擎上并提交查询,使程序具有搜索功能。这一切,就取决于你的创造力了。