23. 领略Internet
现在,往往只需要拨单一支电话就可以连结整个Internet,而且可以和有电子邮件位址的人进行全球通信。特别是在World Wide Web上,超文字、图形和多媒体(包括声音、音乐和视讯)的使用已经扩展了线上资讯的范围和功能。
如果要提供涵盖Windows中所有与Internet相关程式设计问题的彻底介绍,可能还需要再加上几本书才够。所以,本章实际上主要集中在如何让小型的Microsoft Windows应用程式能够有效地从Internet上取得资讯的两个领域。这两个领域分别是Windows Sockets (Winsock) API和Windows Internet(WinInet)API支援的档案传输协定(FTP:File Transfer Protocol)的部分。
Windows Sockets
Socket是由University of California在Berkeley分校开发的概念,用於在UNIX作业系统上添加网路通讯支援。那里开发的API现在称为「Berkeley socket interface」。
Sockets和TCP/IP
Socket通常(但不专用於)与主宰Internet通信的传输控制项协定/网际网路协定(TCP/IP:Transmission Control Protocol/Internet Protocol)牵连在一起。网际网路协定(IP:Internet Protocol),作为TCP/IP的组成部分之一,用来将资料打包成「资料封包(datagram)」,该资料封包包含用於标识资料来源和目的地的表头资讯。而传输控制协定(TCP:Transmission Control Protocol)则提供了可靠的传输和检查IP资料封包正确性的方法。
在TCP/IP下,通讯端点由IP位址和埠号定义。IP位址包括4个位元组,用於确定Internet上的伺服器。IP位址通常按「由点连结的四个小於255的数字」的格式显示,例如「209.86.105.231」。埠号确定了特定的服务或伺服器提供的服务。其中一些埠号已经标准化,以提供众所周知的服务。
当Socket与TCP/IP合用时,Socket就是TCP/IP的通讯端点。因此,Socket指定了IP位址和埠号。
网路时间服务
下面给出的范例程式与提供时间协定(Time Protocol)的Internet伺服器相连结。此程式将获得目前准确的日期和时间,并用此资讯设定您的PC时钟。
在美国,国家标准和技术协会(National Institute of Standards and Technology)(以前称为国家标准局(National Bureau of Standards))负责维护准确时间,该时间与世界各地的机构相联系。准确时间可用於无线电广播、电话号码、电脑拨号电话号码以及Internet,关於这些的所有文件都位於网站
http://www.bldrdoc.gov/timefreq (网域名称「bldrdoc」指的是Boulder、Colorado、NIST Time的位置和Frequency Division)。
我们只对NIST Network Time Service感兴趣,其详细的文件位於http://www.bldrdoc.gov/timefreq/service/nts.htm 。此网页列出了十个提供NIST时间服务的伺服器。例如,第一个名称为time-a.timefreq.bldrdoc.gov,其IP位址为132.163.135.130。
(我曾经编写过一个使用非Internet NIST电脑拨接服务的程式,并发表於《PC Magazine》,您也可以在Ziff-Davis的网站http://www.zdnet.com/pcmag/pctech/content/16/20/ut1620.001.html 中找到。此程式对於想学习如何使用Windows Telephony API的人很有帮助。)
在Internet上有三个不同的时间服务,每一个都由Request for Comment(RFC)描述为Internet标准。日期协定(Daytime Protocol)(RFC-867)提供了一个ASCII字串用於指出准确的日期和时间。该ASCII字串的准确格式并不标准,但人们可以理解其中的含义。时间协定(RFC-868)提供了一个32位元的数字,用来表示从1900年1月1日至今的秒数。该时间是UTC(不考虑字母顺序,它表示世界时间座标(Coordinated Universal Time)),它类似於所谓的格林威治标准时间(Greenwich Mean Time)或者GMT-英国格林威治时间。第三个协定称为网路时间协定(Network Time Protocol)(RFC-1305),该协定很复杂。
对於我们的目的,即包括分析Socket和不断更新PC时钟,时间协定RFC-868已经够用了。RFC-868只是一个两页的简短文件,主要是说用TCP获得准确时间的程式应该有如下步骤:
现在我们已经知道了编写存取时间服务的Socket应用程式的每个细节。
NETTIME程式
Windows Sockets API,通常也称为WinSock,与Berkeley Sockets API相容,因此,可以想像UNIX Socket程式码可以顺利地拿到Windows上使用。Windows下更进一步的支援由对Berkeley Socket扩充的功能提供,其函式的形式是以WSA(「WinSock API」)为字首。相关的概述和参考位於/Platform SDK/Networking and Distributed Services/Windows Sockets Version 2。
NETTIME,如程式23-1所示,展示了使用WinSock API的方法。
程式23-1 NETTIME
NETTIME.C
/*----------------------------------------------------------------------------
NETTIME.C -- Sets System Clock from Internet Services
(c) Charles Petzold, 1998
-----------------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
#define WM_SOCKET_NOTIFY (WM_USER + 1)
#define ID_TIMER 1
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL CALLBACK MainDlg (HWND, UINT, WPARAM, LPARAM) ;
BOOL CALLBACK ServerDlg (HWND, UINT, WPARAM, LPARAM) ;
void ChangeSystemTime (HWND hwndEdit, ULONG ulTime) ;
void FormatUpdatedTime (HWND hwndEdit, SYSTEMTIME * pstOld,
SYSTEMTIME * pstNew) ;
void EditPrintf (HWND hwndEdit, TCHAR * szFormat, ...) ;
HINSTANCE hInst ;
HWND hwndModeless ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("NetTime") ;
HWND hwnd ;
MSG msg ;
RECT rect ;
WNDCLASS wndclass ;
hInst = hInstance ;
wndclass.style = 0 ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = NULL ;
wndclass.hbrBackground = NULL ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Set System Clock from Internet"),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
WS_BORDER | WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
// Create the modeless dialog box to go on top of the window
hwndModeless = CreateDialog (hInstance, szAppName, hwnd, MainDlg) ;
// Size the main parent window to the size of the dialog box.
// Show both windows.
GetWindowRect (hwndModeless, &rect) ;
AdjustWindowRect (&rect, WS_CAPTION | WS_BORDER, FALSE) ;
SetWindowPos ( hwnd, NULL, 0, 0, rect.right - rect.left,
rect.bottom - rect.top, SWP_NOMOVE) ;
ShowWindow (hwndModeless, SW_SHOW) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
// Normal message loop when a modeless dialog box is used.
while (GetMessage (&msg, NULL, 0, 0))
{
if (hwndModeless == 0 || !IsDialogMessage (hwndModeless, &msg))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_SETFOCUS:
SetFocus (hwndModeless) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BOOL CALLBACK MainDlg ( HWND hwnd, UINT message, WPARAM wParam,
LPARAM lParam)
{
static char szIPAddr[32] = { "132.163.135.130" } ;
static HWND hwndButton, hwndEdit ;
static SOCKET sock ;
static struct sockaddr_in sa ;
static TCHAR szOKLabel[32] ;
int iError, iSize ;
unsigned long ulTime ;
WORD wEvent, wError ;
WSADATA WSAData ;
switch (message)
{
case WM_INITDIALOG:
hwndButton = GetDlgItem (hwnd, IDOK) ;
hwndEdit = GetDlgItem (hwnd, IDC_TEXTOUT) ;
return TRUE ;
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDC_SERVER:
DialogBoxParam (hInst, TEXT ("Servers"), hwnd, ServerDlg, (LPARAM) szIPAddr) ;
return TRUE ;
case IDOK:
// Call "WSAStartup" and display description text
if (iError = WSAStartup (MAKEWORD(2,0), &WSAData))
{
EditPrintf (hwndEdit, TEXT ("Startup error #%i.\r\n"), iError) ;
return TRUE ;
}
EditPrintf (hwndEdit, TEXT ("Started up %hs\r\n"),
WSAData.szDescription);
// Call "socket"
sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
if (sock == INVALID_SOCKET)
{
EditPrintf (hwndEdit, TEXT ("Socket creation error #%i.\r\n"), WSAGetLastError ()) ;
WSACleanup () ;
return TRUE ;
}
EditPrintf (hwndEdit, TEXT ("Socket %i created.\r\n"), sock) ;
// Call "WSAAsyncSelect"
if (SOCKET_ERROR == WSAAsyncSelect (sock, hwnd, WM_SOCKET_NOTIFY, FD_CONNECT | FD_READ))
{
EditPrintf ( hwndEdit, TEXT ("WSAAsyncSelect error #%i.\r\n"), WSAGetLastError ()) ;
closesocket (sock) ;
WSACleanup () ;
return TRUE ;
}
// Call "connect" with IP address and time-server port
sa.sin_family = AF_INET ;
sa.sin_port = htons (IPPORT_TIMESERVER) ;
sa.sin_addr.S_un.S_addr = inet_addr (szIPAddr) ;
connect(sock, (SOCKADDR *) &sa, sizeof (sa)) ;
// "connect" will return SOCKET_ERROR because even if it
// succeeds, it will require blocking. The following only
// reports unexpected errors.
if (WSAEWOULDBLOCK != (iError = WSAGetLastError ()))
{
EditPrintf (hwndEdit, TEXT ("Connect error #%i.\r\n"), iError) ;
closesocket (sock) ;
WSACleanup () ;
return TRUE ;
}
EditPrintf (hwndEdit, TEXT ("Connecting to %hs..."), szIPAddr) ;
// The result of the "connect" call will be reported
// through the WM_SOCKET_NOTIFY message.
// Set timer and change the button to "Cancel"
SetTimer (hwnd, ID_TIMER, 1000, NULL) ;
GetWindowText (hwndButton, szOKLabel, sizeof (szOKLabel) /sizeof (TCHAR)) ;
SetWindowText (hwndButton, TEXT ("Cancel")) ;
SetWindowLong (hwndButton, GWL_ID, IDCANCEL) ;
return TRUE ;
case IDCANCEL:
closesocket (sock) ;
sock = 0 ;
WSACleanup () ;
SetWindowText (hwndButton, szOKLabel) ;
SetWindowLong (hwndButton, GWL_ID, IDOK) ;
KillTimer (hwnd, ID_TIMER) ;
EditPrintf (hwndEdit, TEXT ("\r\nSocket closed.\r\n")) ;
return TRUE ;
case IDC_CLOSE:
if (sock)
SendMessage (hwnd, WM_COMMAND, IDCANCEL, 0) ;
DestroyWindow (GetParent (hwnd)) ;
return TRUE ;
}
return FALSE ;
case WM_TIMER:
EditPrintf (hwndEdit, TEXT (".")) ;
return TRUE ;
case WM_SOCKET_NOTIFY:
wEvent = WSAGETSELECTEVENT (lParam) ; // ie, LOWORD
wError = WSAGETSELECTERROR (lParam) ; // ie, HIWORD
// Process two events specified in WSAAsyncSelect
switch (wEvent)
{
// This event occurs as a result of the "connect" call
case FD_CONNECT:
EditPrintf (hwndEdit, TEXT ("\r\n")) ;
if (wError)
{
EditPrintf ( hwndEdit, TEXT ("Connect error #%i."), wError) ;
SendMessage (hwnd, WM_COMMAND, IDCANCEL, 0) ;
return TRUE ;
}
EditPrintf (hwndEdit, TEXT ("Connected to %hs.\r\n"), szIPAddr) ;
// Try to receive data. The call will generate an error
// of WSAEWOULDBLOCK and an event of FD_READ
recv (sock, (char *) &ulTime, 4, MSG_PEEK) ;
EditPrintf (hwndEdit, TEXT ("Waiting to receive...")) ;
return TRUE ;
// This even occurs when the "recv" call can be made
case FD_READ:
KillTimer (hwnd, ID_TIMER) ;
EditPrintf (hwndEdit, TEXT ("\r\n")) ;
if (wError)
{
EditPrintf (hwndEdit, TEXT ("FD_READ error #%i."), wError) ;
SendMessage (hwnd, WM_COMMAND, IDCANCEL, 0) ;
return TRUE ;
}
// Get the time and swap the bytes
iSize = recv (sock, (char *) &ulTime, 4, 0) ;
ulTime = ntohl (ulTime) ;
EditPrintf (hwndEdit,
TEXT ("Received current time of %u seconds ")
TEXT ("since Jan. 1 1900.\r\n"), ulTime) ;
// Change the system time
ChangeSystemTime (hwndEdit, ulTime) ;
SendMessage (hwnd, WM_COMMAND, IDCANCEL, 0) ;
return TRUE ;
}
return FALSE ;
}
return FALSE ;
}
BOOL CALLBACK ServerDlg ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static char * szServer ;
static WORD wServer = IDC_SERVER1 ;
char szLabel [64] ;
switch (message)
{
case WM_INITDIALOG:
szServer = (char *) lParam ;
CheckRadioButton (hwnd, IDC_SERVER1, IDC_SERVER10, wServer) ;
return TRUE ;
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDC_SERVER1:
case IDC_SERVER2:
case IDC_SERVER3:
case IDC_SERVER4:
case IDC_SERVER5:
case IDC_SERVER6:
case IDC_SERVER7:
case IDC_SERVER8:
case IDC_SERVER9:
case IDC_SERVER10:
wServer = LOWORD (wParam) ;
return TRUE ;
case IDOK:
GetDlgItemTextA (hwnd, wServer, szLabel, sizeof (szLabel)) ;
strtok (szLabel, "(") ;
strcpy (szServer, strtok (NULL, ")")) ;
EndDialog (hwnd, TRUE) ;
return TRUE ;
case IDCANCEL:
EndDialog (hwnd, FALSE) ;
return TRUE ;
}
break ;
}
return FALSE ;
}
void ChangeSystemTime (HWND hwndEdit, ULONG ulTime)
{
FILETIME ftNew ;
LARGE_INTEGER li ;
SYSTEMTIME stOld, stNew ;
GetLocalTime (&stOld) ;
stNew.wYear = 1900 ;
stNew.wMonth = 1 ;
stNew.wDay = 1 ;
stNew.wHour = 0 ;
stNew.wMinute = 0 ;
stNew.wSecond = 0 ;
stNew.wMilliseconds = 0 ;
SystemTimeToFileTime (&stNew, &ftNew) ;
li = * (LARGE_INTEGER *) &ftNew ;
li.QuadPart += (LONGLONG) 10000000 * ulTime ;
ftNew = * (FILETIME *) &li ;
FileTimeToSystemTime (&ftNew, &stNew) ;
if (SetSystemTime (&stNew))
{
GetLocalTime (&stNew) ;
FormatUpdatedTime (hwndEdit, &stOld, &stNew) ;
}
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_list pArgList ;
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
----------------------------------------------------------------------------*/
#include <windows.h>
#include <wininet.h>
#include <process.h>
#include "resource.h"
// User-defined messages used in WndProc
#define WM_USER_CHECKFILES (WM_USER + 1)
#define WM_USER_GETFILES (WM_USER + 2)
// Information for FTP download
#define FTPSERVER TEXT ("ftp.cpetzold.com")
#define DIRECTORY TEXT ("cpetzold.com/ProgWin/UpdDemo")
#define TEMPLATE TEXT ("UD??????.TXT")
// Structures used for storing filenames and contents
typedef struct
{
TCHAR * szFilename ;
char * szContents ;
}
FILEINFO ;
typedef struct
{
int iNum ;
FILEINFO info[1] ;
}
FILELIST ;
// Structure used for second thread
typedef struct
{
BOOL bContinue ;
HWND hwnd ;
}
PARAMS ;
// Declarations of all functions in program
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ;
VOID FtpThread (PVOID) ;
VOID ButtonSwitch (HWND, HWND, TCHAR *) ;
FILELIST * GetFileList (VOID) ;
int Compare (const FILEINFO *, const FILEINFO *) ;
// A couple globals
HINSTANCE hInst ;
TCHAR szAppName[] = TEXT ("UpdDemo") ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
hInst = hInstance ;
wndclass.style = 0 ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = NULL ;
wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow ( szAppName, TEXT ("Update Demo with Anonymous FTP"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
// After window is displayed, check if the latest file exists
SendMessage (hwnd, WM_USER_CHECKFILES, 0, 0) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static FILELIST * plist ;
static int cxClient, cyClient, cxChar, cyChar ;
HDC hdc ;
int i ;
PAINTSTRUCT ps ;
SCROLLINFO si ;
SYSTEMTIME st ;
TCHAR szFilename [MAX_PATH] ;
switch (message)
{
case WM_CREATE:
cxChar = LOWORD (GetDialogBaseUnits ()) ;
cyChar = HIWORD (GetDialogBaseUnits ()) ;
return 0 ;
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
si.cbSize = sizeof (SCROLLINFO) ;
si.fMask = SIF_RANGE | SIF_PAGE ;
si.nMin = 0 ;
si.nMax = plist ? plist->iNum - 1 : 0 ;
si.nPage = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
return 0 ;
case WM_VSCROLL:
si.cbSize = sizeof (SCROLLINFO) ;
si.fMask = SIF_POS | SIF_RANGE | SIF_PAGE ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
switch (LOWORD (wParam))
{
case SB_LINEDOWN: si.nPos += 1 ; break ;
case SB_LINEUP: si.nPos -= 1 ; break ;
case SB_PAGEDOWN: si.nPos += si.nPage ; break ;
case SB_PAGEUP: si.nPos -= si.nPage ; break ;
case SB_THUMBPOSITION: si.nPos = HIWORD (wParam) ; break ;
default: return 0 ;
}
si.fMask = SIF_POS ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case WM_USER_CHECKFILES:
// Get the system date & form filename from year and month
GetSystemTime (&st) ;
wsprintf (szFilename, TEXT ("UD%04i%02i.TXT"), st.wYear, st.wMonth) ;
// Check if the file exists; if so, read all the files
if (GetFileAttributes (szFilename) != (DWORD) -1)
{
SendMessage (hwnd, WM_USER_GETFILES, 0, 0) ;
return 0 ;
}
// Otherwise, get files from Internet.
// But first check so we don't try to copy files to a CD-ROM!
if (GetDriveType (NULL) == DRIVE_CDROM)
{
MessageBox (hwnd, TEXT ("Cannot run this program from CD-ROM!"),
szAppName, MB_OK | MB_ICONEXCLAMATION) ;
return 0 ;
}
// Ask user if an Internet connection is desired
if (IDYES == MessageBox (hwnd, TEXT ("Update information from Internet?"),
szAppName, MB_YESNO | MB_ICONQUESTION))
// Invoke dialog box
DialogBox (hInst, szAppName, hwnd, DlgProc) ;
// Update display
SendMessage (hwnd, WM_USER_GETFILES, 0, 0) ;
return 0 ;
case WM_USER_GETFILES:
SetCursor (LoadCursor (NULL, IDC_WAIT)) ;
ShowCursor (TRUE) ;
// Read in all the disk files
plist = GetFileList () ;
ShowCursor (FALSE) ;
SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
// Simulate a WM_SIZE message to alter scroll bar & repaint
SendMessage (hwnd, WM_SIZE, 0, MAKELONG (cxClient, cyClient)) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SetTextAlign (hdc, TA_UPDATECP) ;
si.cbSize = sizeof (SCROLLINFO) ;
si.fMask = SIF_POS ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
if (plist)
{
for (i = 0 ; i < plist->iNum ; i++)
{
MoveToEx (hdc, cxChar, (i - si.nPos) * cyChar, NULL) ;
TextOut (hdc, 0, 0, plist->info[i].szFilename,
lstrlen (plist->info[i].szFilename)) ;
TextOut (hdc, 0, 0, TEXT (": "), 2) ;
TextOutA (hdc, 0, 0, plist->info[i].szContents,
strlen (plist->info[i].szContents)) ;
}
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
BOOL CALLBACK DlgProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static PARAMS params ;
switch (message)
{
case WM_INITDIALOG:
params.bContinue = TRUE ;
params.hwnd = hwnd ;
_beginthread (FtpThread, 0, ¶ms) ;
return TRUE ;
case WM_COMMAND:
switch (LOWORD (wParam))
{
case IDCANCEL: // button for user to abort download
params.bContinue = FALSE ;
return TRUE ;
case IDOK: // button to make dialog box go away
EndDialog (hwnd, 0) ;
return TRUE ;
}
}
return FALSE ;
}
/*---------------------------------------------------------------------------
FtpThread: Reads files from FTP server and copies them to local disk
-----------------------------------------------------------------------------*/
void FtpThread (PVOID parg)
{
BOOL bSuccess ;
HINTERNET hIntSession, hFtpSession, hFind ;
HWND hwndStatus, hwndButton ;
PARAMS * pparams ;
TCHAR szBuffer [64] ;
WIN32_FIND_DATA finddata ;
pparams = parg ;
hwndStatus = GetDlgItem (pparams->hwnd, IDC_STATUS) ;
hwndButton = GetDlgItem (pparams->hwnd, IDCANCEL) ;
// Open an internet session
hIntSession = InternetOpen (szAppName, INTERNET_OPEN_TYPE_PRECONFIG,
NULL, NULL, INTERNET_FLAG_ASYNC) ;
if (hIntSession == NULL)
{
wsprintf (szBuffer, TEXT ("InternetOpen error %i"), GetLastError ()) ;
ButtonSwitch (hwndStatus, hwndButton, szBuffer) ;
_endthread () ;
}
SetWindowText (hwndStatus, TEXT ("Internet session opened...")) ;
// Check if user has pressed Cancel
if (!pparams->bContinue)
{
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, NULL) ;
_endthread () ;
}
// Open an FTP session.
hFtpSession = InternetConnect (hIntSession, FTPSERVER, INTERNET_DEFAULT_FTP_PORT,
NULL, NULL, INTERNET_SERVICE_FTP, 0, 0) ;
if (hFtpSession == NULL)
{
InternetCloseHandle (hIntSession) ;
wsprintf (szBuffer, TEXT ("InternetConnect error %i"),
GetLastError ()) ;
ButtonSwitch (hwndStatus, hwndButton, szBuffer) ;
_endthread () ;
}
SetWindowText (hwndStatus, TEXT ("FTP Session opened...")) ;
// Check if user has pressed Cancel
if (!pparams->bContinue)
{
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, NULL) ;
_endthread () ;
}
// Set the directory
bSuccess = FtpSetCurrentDirectory (hFtpSession, DIRECTORY) ;
if (!bSuccess)
{
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
wsprintf ( szBuffer, TEXT ("Cannot set directory to %s"),
DIRECTORY) ;
ButtonSwitch (hwndStatus, hwndButton, szBuffer) ;
_endthread () ;
}
SetWindowText (hwndStatus, TEXT ("Directory found...")) ;
// Check if user has pressed Cancel
if (!pparams->bContinue)
{
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, NULL) ;
_endthread () ;
}
// Get the first file fitting the template
hFind = FtpFindFirstFile (hFtpSession, TEMPLATE, &finddata, 0, 0) ;
if (hFind == NULL)
{
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, TEXT ("Cannot find files")) ;
_endthread () ;
}
do
{
// Check if user has pressed Cancel
if (!pparams->bContinue)
{
InternetCloseHandle (hFind) ;
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, NULL) ;
_endthread () ;
}
// Copy file from internet to local hard disk, but fail
// if the file already exists locally
wsprintf (szBuffer, TEXT ("Reading file %s..."), finddata.cFileName) ;
SetWindowText (hwndStatus, szBuffer) ;
FtpGetFile ( hFtpSession,
finddata.cFileName, finddata.cFileName, TRUE,
FILE_ATTRIBUTE_NORMAL, FTP_TRANSFER_TYPE_BINARY, 0) ;
}
while (InternetFindNextFile (hFind, &finddata)) ;
InternetCloseHandle (hFind) ;
InternetCloseHandle (hFtpSession) ;
InternetCloseHandle (hIntSession) ;
ButtonSwitch (hwndStatus, hwndButton, TEXT ("Internet Download Complete"));
}
/*----------------------------------------------------------------------------
ButtonSwitch: Displays final status message and changes Cancel to OK
-------------------------------------------------------------------------*/
VOID ButtonSwitch (HWND hwndStatus, HWND hwndButton, TCHAR * szText)
{
if (szText)
SetWindowText (hwndStatus, szText) ;
else
SetWindowText (hwndStatus, TEXT ("Internet Session Cancelled")) ;
SetWindowText (hwndButton, TEXT ("OK")) ;
SetWindowLong (hwndButton, GWL_ID, IDOK) ;
}
/*---------------------------------------------------------------------------
GetFileList: Reads files from disk and saves their names and contents
-----------------------------------------------------------------------------*/
FILELIST * GetFileList (void)
{
DWORD dwRead ;
FILELIST * plist ;
HANDLE hFile, hFind ;
int iSize, iNum ;
WIN32_FIND_DATA finddata ;
hFind = FindFirstFile (TEMPLATE, &finddata) ;
if (hFind == INVALID_HANDLE_VALUE)
return NULL ;
plist = NULL ;
iNum = 0 ;
do
{
// Open the file and get the size
hFile = CreateFile (finddata.cFileName, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL) ;
if (hFile == INVALID_HANDLE_VALUE)
continue ;
iSize = GetFileSize (hFile, NULL) ;
if (iSize == (DWORD) -1)
{
CloseHandle (hFile) ;
continue ;
}
// Realloc the FILELIST structure for a new entry
plist = realloc (plist, sizeof (FILELIST) + iNum * sizeof (FILEINFO));
// Allocate space and save the filename
plist->info[iNum].szFilename = malloc (lstrlen (finddata.cFileName) +sizeof (TCHAR)) ;
lstrcpy (plist->info[iNum].szFilename, finddata.cFileName) ;
// Allocate space and save the contents
plist->info[iNum].szContents = malloc (iSize + 1) ;
ReadFile (hFile, plist->info[iNum].szContents, iSize, &dwRead, NULL);
plist->info[iNum].szContents[iSize] = 0 ;
CloseHandle (hFile) ;
iNum ++ ;
}
while (FindNextFile (hFind, &finddata)) ;
FindClose (hFind) ;
// Sort the files by filename
qsort (plist->info, iNum, sizeof (FILEINFO), Compare) ;
plist->iNum = iNum ;
return plist ;
}
/*---------------------------------------------------------------------------
Compare function for qsort
----------------------------------------------------------------------------*/
int Compare (const FILEINFO * pinfo1, const FILEINFO * pinfo2)
{
return lstrcmp (pinfo2->szFilename, pinfo1->szFilename) ;
}
UPDDEMO.RC (摘录)
//Microsoft Developer Studio generated resource script.
#include "resource.h"
#include "afxres.h"
/////////////////////////////////////////////////////////////////////////////
// Dialog
UPDDEMO DIALOG DISCARDABLE 20, 20, 186, 95
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Internet Download"
FONT 8, "MS Sans Serif"
BEGIN
PUSHBUTTON Cancel",IDCANCEL,69,74,50,14
CTEXT "",IDC_STATUS,7,29,172,21
END
RESOURCE.H (摘录)
// Microsoft Developer Studio generated include file.
// Used by UpdDemo.rc
#define IDC_STATUS 40001
UPDDEMO使用的档案名称是UDyyyymm.TXT,其中yyyy是4位阿拉伯数字的年数(当然适用於2000),mm是2位阿拉伯数字的月数。这里假定程式可以享有每个月都有更新档案的好处。这些档案可能是整个月刊,而由於阅读效率上的考虑,让程式将其下载到本地储存媒体上。
因此,WinMain在呼叫ShowWindow和UpdateWindow来显示UPDDEMO主视窗以後,向WndProc发送程式定义的WM_USER_CHECKFILES讯息。WndProc通过获得目前的年、月并检查该年月UDyyyymm.TXT档案所在的内定目录来处理此讯息。这种档案的存在意义在於UPDDEMO会被完全更新(当然,事实并非如此。一些过时的档案将漏掉。如果要做得更完整,程式得进行更广泛的检测)。在这种情况下,UPDDEMO向自己发送一个WM_USER_GETFILES讯息,它通过呼叫GetFileList函式来处理。这是UPDDEMO.C中稍长的一个函式,但它并不是特别有用,它所做的全部工作就是将所有的UDyyyymm.TXT档案读到动态配置的FILELIST型态结构中,该结构是在程式顶部定义的,然後让程式在其显示区域显示这些档案的内容。
如果UPDDEMO没有最新的档案,那么它必须透过Internet进行更新。程式首先询问使用者这样做是否「OK」。如果是,程式将显示一个简单的对话方块,其中只有一个「Cancel」按钮和一个ID为IDC_STATUS的静态文字区。下载时,此静态文字区向使用者提供状态报告,并且允许使用者取消过於缓慢的更新作业。此对话程序的名称是DlgProc。
DlgProc很短,它建立了一个包括自身视窗代号的PARAMS型态的结构以及一个名称为bContinue的BOOL变数,然後呼叫_ beginthread来执行第二个执行绪。
FtpThread函式透过使用下面的呼叫来完成实际的传输:InternetOpen、InternetConnect、FtpSetCurrentDirectory、FtpFindFirstFile、InternetFindNextFile、FtpGetFile和InternetCloseHandle(三次)。如同大多数程式码,该执行绪函式如果略过错误检查、让使用者了解下一步的操作情况以及允许使用者随意取消整个显示的那些步骤,那么它将变得简洁许多。FtpThread函式透过用hwndStatus代号呼叫SetWindowText来让使用者知道进展情况,这里指的是对话方块中间的静态文字区。
执行绪可以依照下面的三种方式之一来终止:
第一种,FtpThread可能遇到从WinInet函式传回的错误。如果是这样,它将清除并编排错误字串的格式,然後将此字串(连同对话方块文字区代号和「Cancel」按钮的代号一起)传递给ButtonSwitch。ButtonSwitch是一个小函式,它显示了文字字串,并将「Cancel」按钮转换成「OK」按钮-不只是按钮上的文字字串的转换,还包括控制项ID的转换。这样就允许使用者按下「OK」按钮来结束对话方块。
第二种方式,FtpThread能在没有任何错误的情况下完成任务,其处理方法和遇到错误时的方法一样,只不过对话方块中显示的字串为「Internet Download Complete」。
第三种方式,使用者可以在程序中选择取消下载。这时,DlgProc将PARAMS结构的bContinue栏位设定为FALSE。FtpThread频繁地检查该值,如果bContinue等於FALSE,那么函式将做好应该进行的收拾工作,并以NULL文字参数呼叫ButtonSwitch,此参数表示显示了字串「Internet Session Cancelled」。同样,使用者必须按下「OK」按钮来关闭对话方块。
虽然UPDDEMO取得的每个档案只能显示一行,但我(本书的作者)可以用这个程式来告诉您(本书的读者)本书的更新内容以及其他资讯,您也可以在网站上发现更详细的资讯。因此,UPDDEMO成为我向您传送资讯的方法,并且可以让本书的内容延续到最後一页之後。