23. 領略Internet

Post date: 2012/3/23 上午 05:44:00

23. 領略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)的部分。

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獲得準確時間的程式應該有如下步驟:

  1. 連結到提供此服務的伺服器埠37。
  2. 接收32位元的時間。
  3. 關閉連結。

現在我們已經知道了編寫存取時間服務的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程式必須呼叫的第一個函式是:

NETTIME將第一個參數設定為0x0200(表示2.0版本)。傳回時,WSAData結構包含了Windows Sockets實作的相關資訊,而且NETTIME將顯示szDescription字串,並簡要提供了一些版本資訊。

然後,NETTIME如下呼叫socket函式:

第一個參數是一個位址種類,表示此處是某種Internet位址。第二個參數表示資料以資料流的形式傳回,而不是以資料封包的形式傳回(我們需要的資料只有4個位元組長,而資料封包適用於較大的資料塊)。最後一個參數是一個協定,我們指定使用的Internet協定是TCP。它是RFC-868所定義的兩個協定之一。socket函式的傳回值儲存在SOCKET型態的變數中,以便後面的Socket函式的呼叫。

NETTIME下面呼叫的WSAAsynchSelect是另一個Windows特有的Socket函式。此函式用於避免因Internet回應過慢而造成應用程式當住。在WinSock文件中,有些函式與「阻礙性(blocking)」有關。也就是說,它們不能保證立即把控制項權傳回給程式。WSAAsyncSelect函式強制阻礙性的函式轉為非阻礙性的,即在函式執行完之前把控制項傳回給程式。函式的結果以訊息的形式報告給應用程式。WSAAsyncSelect函式讓應用程式指定訊息和接收訊息的視窗的數值。通常,函式的語法如下:

為此任務,NETTIME使用程式定義的一個訊息,該訊息稱為WM_SOCKET_NOTIFY。它也用WSAAsyncSelect的最後一個參數來指定訊息發送的條件,特別在連結和接收資料時(FD_CONNECT | FD_READ)。

NETTIME呼叫的下一個WinSock函式是connect。此函式需要一個指向Socket位址結構的指標,對於不同的協定來說,此Socket位址結構是不同的。NETTIME使用為TCP/IP設計的結構版本:

iError = WSAStartup (wVersion, &WSAData) ;

sock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP) ;

WSAAsyncSelect (sock, hwnd, message, iConditions) ;

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欄位設定為:

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:接收」)函式來讀取資料:

這意味著,用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將發出一個新時間未設定成功的訊息來指出問題所在。

htons (IPPORT_TIMESERVER)

recv (sock, (char *) &ulTime, 4, MSG_PEEK) ;

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伺服器上):

經由先呼叫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伺服器上下載檔案的方法。

fSuccess = FtpDeleteFile (hFtpSession, szFileName) ; fSuccess = FtpRenameFile (hFtpSession, szOldName, szNewName) ;

程式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成為我向您傳送資訊的方法,並且可以讓本書的內容延續到最後一頁之後。