📄 23. 领略internet.txt
字号:
else
EditPrintf (hwndEdit, TEXT ("Could NOT set new date and time.")) ;
}
void FormatUpdatedTime ( HWND hwndEdit, SYSTEMTIME * pstOld, SYSTEMTIME * pstNew)
{
TCHAR szDateOld [64], szTimeOld [64], szDateNew [64], szTimeNew [64] ;
GetDateFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
pstOld, NULL, szDateOld, sizeof (szDateOld)) ;
GetTimeFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE |
TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,
pstOld, NULL, szTimeOld, sizeof (szTimeOld)) ;
GetDateFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE | DATE_SHORTDATE,
pstNew, NULL, szDateNew, sizeof (szDateNew)) ;
GetTimeFormat (LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE |
TIME_NOTIMEMARKER | TIME_FORCE24HOURFORMAT,
pstNew, NULL, szTimeNew, sizeof (szTimeNew)) ;
EditPrintf (hwndEdit, TEXT ("System date and time successfully changed ")
TEXT ("from\r\n\t%s, %s.%03i to\r\n\t%s, %s.%03i."),
szDateOld, szTimeOld, pstOld->wMilliseconds,
szDateNew, szTimeNew, pstNew->wMilliseconds) ;
}
void EditPrintf (HWND hwndEdit, TCHAR * szFormat, ...)
{
TCHAR szBuffer [1024] ;
va_listpArgList ;
va_start (pArgList, szFormat) ;
wvsprintf (szBuffer, szFormat, pArgList) ;
va_end (pArgList) ;
SendMessage (hwndEdit, EM_SETSEL, (WPARAM) -1, (LPARAM) -1) ;
SendMessage (hwndEdit, EM_REPLACESEL, FALSE, (LPARAM) szBuffer) ;
SendMessage (hwn dEdit, EM_SCROLLCARET, 0, 0) ;
}
NETTIME.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
// Dialog
SERVERS DIALOG DISCARDABLE 20, 20, 274, 202
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "NIST Time Service Servers"
FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON "OK",IDOK,73,181,50,14
PUSHBUTTON "Cancel",IDCANCEL,150,181,50,14
CONTROL
"time-a.timefreq.bldrdoc.gov (132.163.135.130) NIST, Boulder, Colorado",
IDC_SERVER1,"Button",BS_AUTORADIOBUTTON,9,7,256,16
CONTROL
"time-b.timefreq.bldrdoc.gov (132.163.135.131) NIST, Boulder, Colorado",
IDC_SERVER2,"Button",BS_AUTORADIOBUTTON,9,24,256,16
CONTROL
"time-c.timefreq.bldrdoc.gov (132.163.135.132) Boulder, Colorado",
IDC_SERVER3,"Button",BS_AUTORADIOBUTTON,9,41,256,16
CONTROL
"utcnist.colorado.edu (128.138.140.44) University of Colorado, Boulder",
IDC_SERVER4,"Button",BS_AUTORADIOBUTTON,9,58,256,16
CONTROL
"time.nist.gov (192.43.244.18) NCAR, Boulder, Colorado",
IDC_SERVER5,"Button",BS_AUTORADIOBUTTON,9,75,256,16
CONTROL
"time-a.nist.gov (129.6.16.35) NIST, Gaithersburg, Maryland",
IDC_SERVER6,"Button",BS_AUTORADIOBUTTON,9,92,256,16
CONTROL
"time-b.nist.gov (129.6.16.36) NIST, Gaithersburg, Maryland",
IDC_SERVER7,"Button",BS_AUTORADIOBUTTON,9,109,256,16
CONTROL
"time-nw.nist.gov (131.107.1.10) Microsoft, Redmond, Washington",
IDC_SERVER8,"Button",BS_AUTORADIOBUTTON,9,126,256,16
CONTROL
"utcnist.reston.mci.net (204.70.131.13) MCI, Reston, Virginia",
IDC_SERVER9,"Button",BS_AUTORADIOBUTTON,9,143,256,16
CONTROL
"nist1.data.com (209.0.72.7) Datum, San Jose, California",
IDC_SERVER10,"Button",BS_AUTORADIOBUTTON,9,160,256,16
END
NETTIME DIALOG DISCARDABLE 0, 0, 270, 150 STYLE WS_CHILD FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON "Set Correct Time",IDOK,95,129,80,14
PUSHBUTTON "Close",IDC_CLOSE,183,129,80,14
PUSHBUTTON "Select Server...",IDC_SERVER,7,129,80,14
EDITTEXT IDC_TEXTOUT,7,7,253,110,ES_MULTILINE | ES_AUTOVSCROLL |
ES_READONLY | WS_VSCROLL | NOT WS_TABSTOP
END
RESOURCE.H (摘录)
// Microsoft Developer Studio generated include file.
// Used by NetTime.rc
#define IDC_TEXTOUT 101
#define IDC_SERVER1 1001
#define IDC_SERVER2 1002
#define IDC_SERVER3 1003
#define IDC_SERVER4 1004
#define IDC_SERVER5 1005
#define IDC_SERVER6 1006
#define IDC_SERVER7 1007
#define IDC_SERVER8 1008
#define IDC_SERVER9 1009
#define IDC_SERVER10 1010
#define IDC_SERVER 1011
#define IDC_CLOSE 1012
在结构上,NETTIME程序建立了一个依据NETTIME.RC中的NETTIME所建立的非系统模态对话框。程序重新定义了窗口的尺寸,以便非系统模态对话框可以覆盖程序的整个窗口显示区域。对话框包括一个只读编辑区(程序用于写入文字信息)、一个「Select Server」按钮、一个「Set Correct Time」按钮和一个「Close」按钮。「Close」按钮用于终止程序。
MainDlg中的szIPAddr变量用于储存服务器地址,内定是字符串「132.163.135.130」。「Select Server」按钮启动依据NETTIME.RC中的SERVERS模板建立的对话框。szIPAddr变量作为最后一个参数传递给DialogBoxParam。「Server」对话框列出了10个服务器(都是从NIST网站上逐字复制来的),这些服务器提供了我们感兴趣的服务。当使用者单击一个服务器时,ServerDlg将分析按钮文字,以获得相应的IP地址。新地址储存在szIPAddr变量中。
当使用者按下「Set Correct Time」按钮时,按钮将产生一个WM_COMMAND消息,其中wParam的低字组等于IDOK。MainDlg中的IDOK处理是大部分Socket初始行为发生的地方。
使用Windows Sockets API时,任何Windows程序必须呼叫的第一个函数是:
iError = WSAStartup (wVersion, &WSAData) ;
NETTIME将第一个参数设定为0x0200(表示2.0版本)。传回时,WSAData结构包含了Windows Sockets实作的相关信息,而且NETTIME将显示szDescription字符串,并简要提供了一些版本信息。
然后,NETTIME如下呼叫socket函数:
sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
第一个参数是一个地址种类,表示此处是某种Internet地址。第二个参数表示数据以数据流的形式传回,而不是以数据封包的形式传回(我们需要的数据只有4个字节长,而数据封包适用于较大的数据块)。最后一个参数是一个协议,我们指定使用的Internet协议是TCP。它是RFC-868所定义的两个协议之一。socket函数的传回值储存在SOCKET型态的变量中,以便后面的Socket函数的呼叫。
NETTIME下面呼叫的WSAAsynchSelect是另一个Windows特有的Socket函数。此函数用于避免因Internet响应过慢而造成应用程序当住。在WinSock文件中,有些函数与「阻碍性(blocking)」有关。也就是说,它们不能保证立即把控件权传回给程序。WSAAsyncSelect函数强制阻碍性的函数转为非阻碍性的,即在函数执行完之前把控件传回给程序。函数的结果以消息的形式报告给应用程序。WSAAsyncSelect函数让应用程序指定消息和接收消息的窗口的数值。通常,函数的语法如下:
WSAAsyncSelect (sock, hwnd, message, iConditions) ;
为此任务,NETTIME使用程序定义的一个消息,该消息称为WM_SOCKET_NOTIFY。它也用WSAAsyncSelect的最后一个参数来指定消息发送的条件,特别在连结和接收资料时(FD_CONNECT | FD_READ)。
NETTIME呼叫的下一个WinSock函数是connect。此函数需要一个指向Socket地址结构的指针,对于不同的协议来说,此Socket地址结构是不同的。NETTIME使用为TCP/IP设计的结构版本:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
} ;
其中in_addr是用于指定Internet地址,它可以用4个字节,或者2个无正负号短整数,或者1个无正负号长整数来表示。
NETTIME将sin_family字段设定为AF_INET,用于表示地址种类。将sin_port设定为埠号,这里是时间协议的埠号,RFC-868显示为37。但不要像我最初时那样,将此字段设为37。当大多数数字通过Internet时,结构的这个端口号字段必须是「big endian」的,即最高的字节排第一个。Intel微处理器是little endian。幸运的是,htons(「host-to-network short」)函数使字节翻转,因此NETTIME将sockaddr_in结构的sin_port字段设定为:
htons (IPPORT_TIMESERVER)
WINSOCK2.H中将常数定义为37。NETTIME用inet_addr函数将储存在szIPAddr字符串中的服务器地址转化为无正负号长整数,该整数用于设定结构的sin_addr字段。
如果应用程序在Windows 98下呼叫connect,而且目前Windows没有连结到Internet,那么将显示「拨号联机」对话框。这就是所谓的「自动拨号」。在Windows NT 4.0中没有实作「自动拨号」,因此如果在NT环境下执行,那么在执行NETTIME之前,就必须先连结上Internet。
connect函数通常已经会阻碍着后面程序的执行,这是因为连结成功以前需要花些时间。然而,由于NETTIME呼叫了WSAAsyncSelect,所以connect不会等待连结,事实上,它会立即传回SOCKET_ERROR的值。这并不是出现了错误,这只是表示现在还没有联机成功而已。NETTIME也不会检查这个传回值,只是呼叫WSAGetLastError而已。如果WSAGetLastError传回WSAEWOULDBLOCK(即函数的执行通常要受阻,但这里并没有受阻),那就一切都还很正常。NETTIME将「Set Correct Time」按钮改成「Cancel」,并设定了一个1秒的定时器。WM_TIMER的处理方式只是在程序窗口中显示句点,以告诉使用者程序仍在执行,系统没有当掉。
连结最终完成时,MainDlg由WM_SOCKET_NOTIFY消息-NETTIME在WSAAsyncSelect函数中指定的程序自订消息所通知。lParam的低字组等于FD_CONNECT,高字组表示错误。这时的错误可能是程序不能连结到指定的服务器。NETTIME还列出了其它9个服务器,供您选择,让您可以试试其它的服务器。
如果一切顺利,那么NETTIME将呼叫recv(「receive:接收」)函数来读取数据:
recv (sock, (char *) &ulTime, 4, MSG_PEEK) ;
这意味着,用4个字节来储存ulTime变量。最后一个参数表示只是读此数据,并不将其从输入队列中删除。像connect函数一样,recv传回一个错误代码,以表示函数通常受阻,但这时没有受阻。理论上来说(当然这不大可能),函数至少能传回数据的一部分,然后透过再次呼叫以获得其余的32个字节值。那就是呼叫recv函数时带有MSG_PEEK选项的原因。
与connect函数类似,recv函数也产生WM_SOCKET_NOTIFY消息,这时带有FD_READ的事件代码。NETTIME通过再次呼叫recv来对此响应,这时最后的参数是0,用于从队列中删除数据。我将简要讨论一下程序处理接收到的ulTime的方法。注意,NETTIME通过向自己发送WM_COMMAND消息来结束处理,该消息中wParam等于IDCANCEL。对话框程序通过呼叫closesocket和WSACleanup来响应。
再次呼叫NETTIME接收的32位的ulTime值是从1990年1月1日开始的0:00 UTC秒数。但最高顺序的字节是第一个字节,因此该值必须通过ntohl(「network-to-host long」)函数处理来调整字节顺序,以便Intel微处理器能够处理。然后,NETTIME呼叫ChangeSystemTime函数。
ChangeSystemTime首先取得目前的本地时间-即,使用者所在时区和日光节约时间的目前系统时间。将SYSTEMTIME结构设定为1900年1月1日午夜(0时)。并将这个SYSTEMTIME结构传递给SystemTimeToFileTime,将此结构转化为FILETIME结构。FILETIME实际上只是由两个32位的DWORD一起组成64位的整数,用来表示从1601年1月1日至今间隔为100奈秒(nanosecond)的间隔数。
ChangeSystemTime函数将FILETIME结构转化为LARGE_INTEGER。它是一个union,允许64位的值可以被当成两个32位的值使用,或者当成一个__int64数据型态的64位整数使用(__int64数据型态是Microsoft编译器对ANSI C标准的扩充)。因此,此值是1601年1月1日到1900年1月1日之间间隔为100奈秒的间隔数。这里,添加了1900年1月1日至今间隔为100奈秒的间隔数-ulTime的10,000,000倍。
然后通过呼叫FileTimeToSystemTime将作为结果的FILETIME值转换回SYSTEMTIME结构。因为时间协议传回目前的UTC时间,所以NETTIME通过呼叫SetSystemTime来设定时间,SetSystemTime也依据UTC。基于显示的目的,程序呼叫GetLocalTime来获得更新时间。最初的本地时间和新的本地时间一起传递给FormatUpdatedTime,这个函数用GetTimeFormat函数和GetDateFormat函数将时间转化为ASCII字符串。
如果程序在Windows NT下执行,并且使用者没有取得设定时间的权限,那么SetSystemTime函数可能失败。如果SetSystemTime失败,则NETTIME将发出一个新时间未设定成功的消息来指出问题所在。
WinInet 和 FTP
WinInet(「Windows Internet」)API是一个高阶函数集,帮助程序写作者使用三个常见的Internet协议,这三个协议是:用于World Wide Web全球信息网的超文字传输协议(HTTP:Hypertext Transfer Protocol)、文件传输协议(FTP:File Transfer Protocol)和另一个称为Gopher的文件传输协议。WinInet函数的语法与常用的Windows文件函数的语法类似,这使得使用这些协议就像使用本地磁盘驱动器上的文件一样容易。WinInet API的文件位于/Platform SDK/Internet, Intranet, Extranet Services/Internet Tools and Technologies/WinInet API。
下面的范例程序将展示如何使用WinInet API的FTP部分。许多有网站的公司也都有「匿名FTP」服务器,这样使用者可以在不输入使用者名称和密码的情况下下载文件。例如,如果您在Internet Explorer的地址栏输入ftp://ftp.microsoft.com,那么您就可以浏览FTP服务器上的目录并下载文件。如果进入ftp://ftp.cpetzold.com/cpetzold.com/ProgWin/UpdDemo,那么您将在我的匿名FTP服务器上发现与待会要提到的范例程序一块使用的文件列表。
虽然现今FTP服务对大多数的Web使用者来说并不是那么方便使用,但它仍然相当有用。例如,应用程序能利用FTP从匿名FTP服务器上取得数据,这些取得数据的运作程序几乎完全在台面下处理,而不需要使用者操心。这就是我们将讨论的UPDDEMO(「update demonstration:更新范例」)程序的构想。
FTP API概况
使用WinInet的程序必须在所有呼叫WinInet函数的源文件中包括表头文件WININET.H。程序还必须连结WININET.LIB。在Microsoft Visual C++中,您可以在「Project Settings」对话框的「Link」页面卷标中指定。执行时,程序将和WININET.DLL动态链接库连结。
在下面的论述中,我不会详细讨论函数的语法,因为某些函数有很多选项,这让它变得相当复杂。要掌握WinInet,您可以将UPDDEMO原始码当成食谱来看待。这时最重要的是了解有关的各个步骤以及FTP函数的范围。
要使用Windows Internet API,首先要呼叫InternetOpen。然后,使用WinInet支持的任何一种协议。InternetOpen给您一个Internet作业句柄,并储存到HINTERNET型态的变量中。用完WinInet API以后,应该通过呼叫InternetCloseHandle来关闭句柄。
要使用FTP,您接下来就要呼叫InternetConnect。此函数需要使用由InternetOpen建立Internet作业句柄,并且传回FTP作业的句柄。您可将此句柄作为名称开头为Ftp的所有函数的第一个参数。InternetConnect函数的参数指出要使用的FTP,还提供了服务器名称,例如,ftp.cpetzold.com。此函数还需要使用者名称和密码。如果存取匿名FTP服务器,这些参数可以设定为NULL。如果应用程序呼叫InternetConnect时PC并没有连结到Internet,Windows 98将显示「拨号联机」对话框。当使用FTP的应用程序结束时,呼叫InternetCloseHandle来关闭句柄。
这时可以开始呼叫有Ftp前缀的函数。您将发现这些函数与标准的Windows文件I/O函数很相似。为了避免与其它协议重复,一些以Internet为前缀的函数也可以处理FTP。
下面四个函数用于处理目录:
fSuccess = FtpCreateDirectory (hFtpSession, szDirectory) ;
fSuccess = FtpRemoveDirectory (hFtpSession, szDirectory) ;
fSuccess = FtpSetCurrentDirectory (hFtpSession, szDirectory) ;
fSuccess = FtpGetCurrentDirectory (hFtpSession, szDirectory, &dwCharacterCount) ;
注意,这些函数很像我们所熟悉的Windows提供用于处理本地文件系统的CreateDirectory、RemoveDirectory、SetCurrentDirectory和GetCurrentDirectory函数。
当然,存取匿名FTP的应用程序不能建立或删除目录。而且,程序也不能假定FTP目录具有和Windows文件系统相同的目录结构型态。特别是用相对路径名设定目录的程序,不能假定关于新的目录全名的一切。如果程序需要知道最后所在目录的整个名称,那么呼叫了SetCurrentDirectory之后必须再呼叫GetCurrentDirectory。GetCurrentDirectory的字符串参数至少包含MAX_PATH字符,并且最后一个参数应指向包含该值的变量。
下面两个函数让您删除或者重新命名文件(但不是在匿名FTP服务器上):
fSuccess = FtpDeleteFile (hFtpSession, szFileName) ;
fSuccess = FtpRenameFile (hFtpSession, szOldName, szNewName) ;
经由先呼叫FtpFindFirstFile,可以查找文件(或与含有万用字符的文件名样式相符的多个文件)。此函数很像FindFirstFile函数,甚至都使用了相同的WIN32_FIND_DATA结构。该文件为列举出来的文件传回了一个句柄。您可以将此句柄传递给InternetFindNextFile函数以获得额外的文件名称信息。最后通过呼叫InternetCloseHandle来关闭句柄。
要打开文件,可以呼叫FtpFileOpen。这个函数传回一个文件句柄,此句柄可以用于InternetReadFile、InternetReadFileEx、InternetWrite和InternetSetFilePointer呼叫。最后可以通过呼叫最常用的InternetCloseHandle函数来关闭句柄。
最后,下面两个高级函数特别有用:FtpGetFile呼叫将文件从FTP服务器复制到本地内存,它合并了FtpFileOpen、FileCreate、InternetReadFile、WriteFile、InternetCloseHandle和CloseHandle呼叫。FtpGetFile的另一个参数是一个旗标,如果本地已经存在同名文件,那么该旗标将导致函数呼叫失败。FtpPutFile与此函数类似,用于将文件从本地内存复制到FTP服务器。
更新展示程序
UPDDEMO,如程序23-2所示,展示了用WinInet FTP函数在第二个线程执行期间从匿名FTP服务器上下载文件的方法。
程序23-2 UPDDEMO
UPDDEMO.C
/*---------------------------------------------------------------------------
UPDDEMO.C -- Demonstrates Anonymous FTP Access
(c) Charles Petzold, 1998
----------------------------------------------------------------------------*/
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -