领略Internet |
|
Internet-全世界计算机透过不同协议交换信息的大型连结体-近几年重新定义了个人计算的几个领域。虽然拨接信息服务和电子邮件系统在Internet流行开来之前就已经存在,但它们通常局限于文字模式,并且根本没有连结而是各自分隔的。例如,每一种信息服务都需要拨不同的电话号码,用不同的使用者ID和密码登录。每一种电子邮件系统仅允许在特定系统的缴款使用者之间发送和接收邮件。
现在,往往只需要拨单一支电话就可以连结整个Internet,而且可以和有电子邮件地址的人进行全球通信。特别是在World Wide Web上,超文字、图形和多媒体(包括声音、音乐和视讯)的使用已经扩展了在线信息的范围和功能。
如果要提供涵盖Windows中所有与Internet相关程序设计问题的彻底介绍,可能还需要再加上几本书才够。所以,本章实际上主要集中在如何让小型的Microsoft Windows应用程序能够有效地从Internet上取得信息的两个领域。这两个领域分别是Windows Sockets (Winsock) API和Windows Internet(WinInet)API支持的文件传输协议(FTP:File Transfer Protocol)的部分。
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获得准确时间的程序应该有如下步骤:
- 连结到提供此服务的服务器埠37。
- 接收32位的时间。
- 关闭连结。
现在我们已经知道了编写存取时间服务的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的方法。
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_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(「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服务器上下载文件的方法。
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, 秏s) ; 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成为我向您传送信息的方法,并且可以让本书的内容延续到最后一页之后。