Docstoc

Advanced FTP_ or Teaching Fido To Phetch

Document Sample
Advanced FTP_ or Teaching Fido To Phetch Powered By Docstoc
					Advanced FTP, or Teaching Fido To Phetch
Robert Coleridge

Microsoft Developer Network Technology Group


July 29, 1996

Abstract

This technical article examines the usage of the File Transfer Protocol (FTP) functions contained in the

Win32? Internet (WinInet) Software Development Kit (SDK). In order to use these API functions you will

need the WinInet dynamic-link libraries (DLLs), which are included with the ActiveX? SDK, or the newest

release of the Win32 SDK. To compile the AdvFTP sample application you will also need Microsoft? Visual

C++? version 4.2 or higher. Along with the WinInet FTP functions, this article also touches briefly on

Microsoft Foundation Class Library (MFC) multithreading and Win32 synchronization.


This article demonstrates an MFC-based, multithreaded Internet file transfer program that will retrieve

files from a remote FTP server in an asynchronous fashion. Making the program asynchronous dictates

that we should be using a multithreading approach. The reason I say "should" is that, although it is quite

possible to write a sample application as a single-threaded program, a multithreaded program is far more

functional for what we want to accomplish.


It is assumed that the reader has some working knowledge of the MFC and the Win32 thread and

synchronization functions. Although some of these concepts are beyond the scope of this article, I will

discuss them briefly when I use them. For further details on any of these functions, see the Win32 SDK

documentation (now called the Platform SDK on the MSDN Library) for a fuller explanation. The AdvFTP

sample accompanying this article was compiled and tested with the Microsoft Visual C++ compiler version

4.2 under Windows? 95.


This article examines the following functions: InternetConnect, FtpFindFirstFile, FtpFindNextFile,

FtpGetFile, FtpSetCurrentDirectory, FtpGetCurrentDirectory, AfxBeginThread, SetEvent,

ResetEvent, CreateEvent, and WaitForSingleObject.

Introduction

With the growth in popularity of the Internet comes a dramatic increase in the availability of information.

This wealth of information is useless to us, however, unless we have the means to access it. Fortunately

for the majority of programmers, Microsoft has provided an easy way to design and develop programs

that can access the information on the Internet. This article examines how to accomplish various Internet
FTP functions. Included with this article is a sample application (AdvFTP) that demonstrates these

functions.


This article shows you, in a step-by-step fashion, how to connect to and disconnect from an FTP server.

Once connected to the FTP server, you will learn how to enumerate or read that server's directory. Given a

list of files in a particular directory, you will be able to retrieve selected files from an FTP server. Although

the sample application does not write any files to the server, this article looks at how to do so. Because

the files we want may be in different directories on the server, you will also learn how to move around the

server's directory tree. Once you have gone through the examples in this article, you will be able to build

your own Internet FTP download program.

Overview of Internet API FTP Functions

Given that FTP stands for "File Transfer Protocol," the FTP-related application programming interface (API)

functions break down into two general file categories: functions to manipulate directories (file containers)

and functions to manipulate the files themselves.


Functions for Manipulating FTP Directories

Following are the four basic directory manipulation functions:



        FtpCreateDirectory—make a directory)


        FtpRemoveDirectory—delete a directory


        FtpSetCurrentDirectory—go to a particular directory


        FtpGetCurrentDirectory—find out where you are in a directory tree


These functions have very simple interfaces. All but the last (FtpGetCurrentDirectory) take two

parameters: a handle to a previously started FTP session and a pointer to a string specifying the directory

to manipulate. The FtpGetCurrentDirectory function uses one more parameter to specify the size of the

buffer that will receive the name of the current directory.


Two functions are used to enumerate or read an FTP directory: FtpFindFirstFile and FtpFindNextFile.

They are identical in functional to the Win32? FindFirstFile and FindNextFile functions.


Functions for Manipulating FTP Files

The Win32? Internet (WinInet) Software Development Kit (SDK) allows you to manipulate remote FTP files

in a fashion that is similar to manipulating files locally. There are functions to read and write files

(FtpReadFile and FtpWriteFile), to open files (FtpOpenFile), rename or delete files (FtpRenameFile,
FtpDeleteFile), and so on. As you can see, most of the work you would do on local files you can also do

on remote FTP files.


The WinInet SDK also supplies us with two functions that encapsulate a very common programming task—

that of retrieving or transferring an entire file. The "standard" methodology for transferring entire files is

to repetitively read segments of the file and write them to their new destination until the entire file has

been read. The WinInet SDK has greatly simplified this task with FtpGetFile and FtpPutFile. These two

functions encapsulate this "repetitive loop" for you. All you need to transfer a file is to supply two file

specifications: the input location and the output location. With these two parameters, along with a few

optional flags, the functions will accomplish the file transfer for you.


Issues Involving the FTP Functions

When manipulating files locally the programmer or user usually has complete control over the process.

This, however, is not the case when manipulating files remotely. There are a number of things that can

affect the success of the remote transfer, such as poor communications lines; communications lines that

are slow due to excessive traffic, and so on.

Synchronous vs. Asynchronous Communications


In communications there are two methods of information exchange: synchronous and asynchronous.

Synchronous communication can be compared to a radio transmission, where one person speaks at a

time, while asynchronous communication is like using a telephone, where the parties can communicate

simultaneously. Each method has its advantages and disadvantages. Synchronous communication, on the

one hand, is very easy to implement but may take longer to transfer information and does not guarantee

success. Asynchronous communication, on the other hand, is not as easy to implement, but can usually

transfer information more quickly and with a higher degree of success. For the purpose of this article I will

focus on the asynchronous methodology.

Single-threaded vs. Multithreaded


In any two-way communication it takes time to process what the other party said and respond. This slows

down the conversation. Most people communicate and process what they are hearing in a linear fashion.

That is, first they talk, then they listen, then they analyze what they heard, then they talk, and so on. This

could be called doing a single task at a time, or a single-threaded process, because only one task or

"thread" is happening at any one time. For people this works quite well, since it can be rude to talk while

another person is talking. With computers, however, this form of communicating is inefficient. There is no

real need for one computer to wait for another computer to respond before acting, because today's

computers are powerful enough to do more than one task at a time. This ability is called multitasking or
multithreading. It is this ability to perform multiple threads of work simultaneously that we will use to do

asynchronous FTP.


With the use of the Win32 SDK and the WinInet SDK this type of scenario is quite easily accomplished,

inasmuch as both are designed for this and allow us to create programs that take advantage of this

capability.

Some FTP Functions from a Synchronous Perspective: An In-depth
Examination

To avoid confusion about some of the complexities of asynchronous processing, this article first examines

the FTP functions from a synchronous perspective. Once you have seen how to use each function, I will

put them together in a pseudo-program sample. I say pseudo because the sample code is not the best of

examples, inasmuch as the FTP functions should really be used asynchronously. The pseudo-sample is

given merely to demonstrate the flow of control necessary to use the functions. The section following this

one examines rewriting them for asynchronous processing.


How to Connect to a Remote FTP Server

InternetOpen


In order to use the WinInet API you must use the InternetOpen function to obtain a handle to a

connection to the Internet. The handle returned to the program is needed to do any further work with the

SDK.


For this function to work, you must have an Internet agent or program that will handle the work for you.

In this case I will be using the Microsoft Internet Explorer. You will also need to specify the type of access

you want and a few optional flags. The sample program uses a proxy server, so you will need to specify

the proxy server itself. The function will then return a handle to an Internet session. When you are

finished with the connection, you must close it by passing the handle to the InternetCloseHandle

function. For example:

HINTERNET hInternetSession;

hInternetSession = InternetOpen(

                           "Microsoft Internet Explorer",                     // agent

                           INTERNET_OPEN_TYPE_PROXY,                          // access

                           "ftp-gw",                                          // proxy server

                           NULL,                                              // defaults

                           0);                                                // synchronous
.

.

.

//Do some processing.

.

.

.

// Close connection.

InternetCloseHandle(hInternetSession);


This example connects, via the Internet agent (in this case Microsoft Internet Explorer), to the Internet

and returns you a handle to the connection, if successful. By specifying the parameter

INTERNET_OPEN_TYPE_PROXY and ftp-gw you have requested that the agent use a proxy server. With

one simple call you now have a connection to the Internet. With the returned handle hInternetSession

used as a parameter to the other functions, you can start accessing Internet information.


InternetConnect

You use the InternetConnect function to make a connection to a specified FTP server. This function can

only be used after a successful call to InternetOpen. The code for this function might look like this:

// Make connection to ftp server.

HINTERNET hFTPSession;

hFTPSession = ::InternetConnect(

         hInternetSession,                                // Handle from a previous

                                                           // call to InternetOpen.

         "ftp://ftp.microsoft.com",                       // Server we wish to connect to.

         INTERNET_INVALID_PORT_NUMBER,                    // Use appropriate port.

         "anonymous",                                     // Username, can be NULL.

         "robcol@homesite.com",                           // Password, can be NULL.

         INTERNET_SERVICE_FTP,                            // Flag to use FTP services.

         0,                                               // Flags (see SDK docs).

         (DWORD)0);                                       // SEE DISCUSSION ON THIS PARAM.
.

.

.

// Do some processing.

.

.

.

// Close connection.

InternetCloseHandle(hFTPSession);


Upon successful completion of this call we now have a handle to the specified FTP server. This is the

handle we will use to access the server's files and directories. Even though the function is straightforward

to use, several parameters require explanation:



        The first parameter is a handle obtained from a call to the InternetOpen function.


        The second parameter is self-explanatory. It is simply the name of the FTP site we want to

    connect to.


        The third parameter, INTERNET_INVALID_PORT_NUMBER, is actually a flag to the function telling

    it to use whatever port is appropriate for the required protocol.


        The fourth parameter is the username used to log on to the FTP server. If this parameter is NULL,

    the function uses an appropriate default. For the FTP protocol, the default is "anonymous".


        The fifth parameter is the password used to log on to the FTP server. If both Password and

    Username are NULL, the function uses the default "anonymous password". In the case of FTP, the

    default anonymous password is the user's e-mail name. If Password is NULL (or an empty string), but

    Username is not NULL, the function uses a blank password. The following table describes the behavior

    for the four possible settings of Username and Password.


Table 1. Settings of Username and Password Parameters


                                                       Username sent to FTP        Password sent to FTP
Username                   Password                    server                      server

NULL or ""                 NULL or ""                  "anonymous"                 User's e-mail name

Non-NULL string            NULL or ""                  Username                    ""

NULL                       Non-NULL string             ERROR                       ERROR
Non-NULL string              Non-NULL string           Username                    Password



        The sixth parameter tells the API to use FTP services.


        The seventh parameter is explained fully in the SDK documentation.


        The eighth parameter is a user-defined value that is passed on to the callback function if

    asynchronous processing was specified during the InternetOpen call. Please note that this value

    CANNOT be zero if asynchronous processing is required. Setting this value to zero effectively turns off

    asynchronous processing.


How to Enumerate or Read an FTP Server's Directory

FtpFindFirstFile and FtpFindNextFile


Now that you have seen how to connect to the Internet (with the InternetOpen function) and connect to

an FTP server (with the InternetConnect function), you need to know the functions required to

enumerate that server's directories and files. These two functions are usually used in sequence. That is, in

order to use FtpFindNextFile you must first have called FtpFindFirstFile. If the file you are trying to

find is not ambiguous, however, you just need to use FtpFindFirstFile. The code to enumerate all .ZIP

files might look like the following;

// Find first .ZIP file.

HINTERNET hFileConnection;

WIN32_FIND_DATA sWFD;

BOOL bResult = TRUE;



hFileConnection = ::FtpFindFirstFile(

                               hFTPSession,

                               "*.ZIP",

                               &sWFD,

                               0,

                               0);

if (hFileConnection != (HINTERNET)NULL)

    {

    while (bResult)
         {

         .

         .

         .

         //Do something with file (sWFD.cFileName).

         .

         .

         .

         //

         bResult = ::InternetFindNextFile(

                           hFileConnection,

                           &sWFD);

         }

    }



// Close connection

InternetCloseHandle(hFileConnection);


Note that in this example I introduce the new data type WIN32_FIND_DATA. This is not part of the

WinInet SDK, but rather of the standard Win32 SDK. The example above does several things: First, it

makes the initial call to FtpFindFirstFile, then the code goes into a loop, processing the returned filespec

and getting the next matching file. The loop exits when there are no more files matching the original

specification passed in via the FtpFindFirstFile call.


How to Retrieve Selected Files from an FTP Server

FtpGetFile


FtpGetFile is one of the easiest functions to use in the WinInet SDK with regard to the sample program.

An example might look like:

BOOL bResult;

bResult = ::FtpGetFile(

                  hFTPSession,                  // Handle from an InternetConnect call
                   "ftp://ftp.mysite.com/reference.doc",

                   "c:\notes\reference.doc",

                   FALSE,

                   FILE_ATTRIBUTE_NORMAL,

                   FTP_TRANSFER_TYPE_BINARY,

                   0);


This sample piece of code retrieves the reference.doc file from the mysite.com FTP server and stores it

in the C:\notes subdirectory on the local machine. The FILE_ATTRIBUTE_NORMAL flag specifies that

the file will have normal attributes upon creation on the local machine. The

FTP_TRANSFER_TYPE_BINARY flag specifies that the file being transferred is to be transferred in an

"as-is" state (that is, no translation of carriage returns to new lines, and so on).


How to Store a File on an FTP Server

FtpPutFile


FtpPutFile is very easy to use. An example might look like this:

BOOL bResult;

bResult = ::FtpPutFile(

                   hFTPSession,                  // Handle from an InternetConnect call

                   "c:\notes\reference.doc",

                   "ftp://ftp.mysite.com/reference.doc",

                   FTP_TRANSFER_TYPE_BINARY,

                   0);


This sample piece of code sends the c:\notes\reference.doc file to the mysite.com FTP server and

stores it under the name of reference.doc. The FTP_TRANSFER_TYPE_BINARY flag specifies that the

file being transferred is to be transferred in an "as-is" state (that is, no translation of carriage returns to

new lines, and so on).


How to Move Around an FTP Server's Directory Structure

The FtpSetCurrentDirectory and FtpGetCurrentDirectory functions are identical to the

SetCurrentDirectory and GetCurrentDirectory Win32 API functions. All these functions do is allow the
program to specify which remote directory the program will work with by default. The

FtpSetCurrentDirectory might look like this:

FtpSetCurrentDirectory(hFTPSession, "/bin/driver");


This would change the current default directory to /bin/driver.


The FtpGetCurrentDirectory function simply returns to the user the name of the current default

directory. For example:

char cBuffer[_MAX_PATH];

DWORD dwSize = _MAX_PATH;

FtpGetCurrentDirectory(hFTPSession, cBuffer, &dwSize);


This code retrieves the current directory and stores it in the cBuffer buffer. The dwSize variable initially

contains the size of the buffer, and upon successful completion of the function call will contain the size of

the returned value in cBuffer.


Creating a Synchronous Example

Suppose that you want to connect to an FTP server called ftp://ftp.infosite.com and copy all of the .ZIP

files from its /BIN subdirectory to your local hard drive and store them in your C:\ZIPFILES

subdirectory. The sample code might look like this:

HINTERNET hInternetSession;                              // handle to internet connection

HINTERNET hFTPSession;                                   // handle to FTP session

HINTERNET hFileConnection;                               // handle to file enumeration

WIN32_FIND_DATA sWFD;                                    // structure to hold FIND data

BOOL bResult = TRUE;                                     // Boolean for return code

CString InputSpec;                                       // variable to hold input spec

Cstring OutputSpec;                                      // variable to hold output spec



hInternetSession = InternetOpen(

                           "Microsoft Internet Explorer",                        // agent

                           INTERNET_OPEN_TYPE_PROXY,                             // access

                           "ftp-gw",                                             // proxy server
                     NULL,                             // defaults

                     0);                               // synchronous



// Make connection to ftp server.

hFTPSession = ::InternetConnect(

            hInternetSession,               // Handle from a previous

                                            // call to InternetOpen.

            "ftp://ftp.infosite.com",       // Server we want to connect
to

            INTERNET_INVALID_PORT_NUMBER,   // Use appropriate port.

            NULL,                           // Use anonymous for
username.

            NULL,                           // Use e-mail name for
password

            INTERNET_SERVICE_FTP,           // Flag to use FTP services

            0,                              // Flags (see SDK docs)

            0);                             // Synchronous mode



// Find first .ZIP file.

hFileConnection = ::FtpFindFirstFile(

                       hFTPSession,

                       "*.ZIP",

                       &sWFD,

                       0,

                       0);

if (hFileConnection != (HINTERNET)NULL)

     {

     ::FtpSetCurrentDirectory(hFTPSession, "/BIN");
   while (bResult)

       {

       // Create file specs.

       InputSpec = "ftp://ftp.infosite.com/BIN/";

       InputSpec = InputSpec + sWFD.cFileName;

       OutputSpec = "c:\zipfiles\";

       OutputSpec = OutputSpec + sWFD.cFileName;



       // Transfer the file.

       bResult = ::FtpGetFile(

           hFTPSession,

           InputSpec,

           OutputSpec,

           FALSE,

           FILE_ATTRIBUTE_NORMAL,

           FTP_TRANSFER_TYPE_BINARY,

           0);



       // Get next file.

       bResult = ::InternetFindNextFile(

                     hFileConnection,

                     &sWFD);

       }

   }



// Close connections.

InternetCloseHandle(hFileConnection);

InternetCloseHandle(hFTPSession);
InternetCloseHandle(hInternetSession);

Some FTP Functions from an Asynchronous Perspective: An In-depth
Examination

In order to process the FTP information from an asynchronous perspective you must add two new

concepts to the existing examples. First, you must have some method of telling your code to wait

efficiently until certain events have occurred; and second, you must use a callback function.


The first concept is commonly called a synchronization object. This can be thought of as a fancy traffic

signal. It is a mechanism whereby certain events are allowed to occur while others are halted. Although

there are several types of synchronization objects available, you will be using the Event object.


The second concept, the callback function, is simply a function that you have informed the API it can use

to "call you back" whenever it needs to notify you of something.


The Event Object

The Event object is a very simple object to create—you simply call the CreateEvent Win32 API function.

The function returns to the program a handle to that object. For your purposes you will be creating the

simplest form of Event object. (For complete details on the other forms of Event objects see the Win32

documentation.) Creating the simplest form might look like this:

HANDLE hEvent;



hEvent = CreateEvent(

                  NULL,                // No security descriptor.

                  TRUE,                // We will reset the event ourself.

                  TRUE,                // Start signaled or "green light" mode.

                  NULL);               // Nameless event.

.

.

.

//Use the event.

.

.
.

CloseHandle(hEvent);


The example above created an Event object. (Note that when you were finished with the event you

released it with the CloseHandle API function. This must always be done or else the program will incur

resource leakage.) There are two states to an Event object—signaled ("green light") and non-signaled

("red light"). The green light or signaled state allows anyone else who is waiting for the object to proceed,

and the red light or non-signaled state prevents anyone who is waiting for the object from proceeding.


The relevant Win32 API functions are:



        CreateEvent (creates the Event object)


        SetEvent(sets the object's state to signaled)


        ResetEvent (sets the object's state to non-signaled)


        WaitForSingleObject (efficiently pauses the code until the object is in a signaled state)


The WaitForSingleObject function's second parameter requires some explanation. It is the amount of

time, in milliseconds, before the object becomes signaled. The constant INFINITE can be used to cause

the function to wait forever. Use this value only when you are positive the object will become signaled

eventually. An example of these functions in use (in a multithreaded application) might look like this:

void SomeFunctionName()

    {

    HANDLE hEvent;

    .

    .

    .

    // Create the Event object.

    hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);

    .

    .

    .

    // Set up Event object so we will have to wait.
    ResetEvent(hEvent);



    // Now do something that takes time but we don't know how long.

    // Pass in handle to Event object so other procedure can alter it

    // when it is finished (this assumes the code we are calling

    // is running on another thread).

    DoSomeLengthyCalculationOnAnotherThread(hEvent);



    // Now wait until other procedure is finished

    WaitForSingleObject(hEvent, INFINITE);

    .

    .

    .

    return;

    }



.

.

.

void DoSomeLengthyCalculationOnAnotherThread(HANDLE hEvent)

    {

    // Do something lengthy.

    .

    .

    .

    // Set Event object's state to a signaled state so other code can

    // continue.

    SetEvent(hEvent);
    // Continue work while other code continues.

    .

    .

    .

    }

Monitoring Progress Through Callback Functions

What you need to do at this point is to inform the system that you want to use a callback routine and

have it notify you when certain events occur. In order to do this you need to make several changes to how

you previously used certain WinInet API functions.


InternetOpen

In order to do asynchronous processing you need to supply a new value for one of the parameters (the

new value appears in bold type):

HINTERNET hInternetSession;

hInternetSession = InternetOpen(

                          "Microsoft Internet Explorer",                  // agent

                          INTERNET_OPEN_TYPE_PROXY,                       // access

                          "ftp-gw",                                       // proxy server

                          NULL,                                           // defaults

                          INTERNET_FLAG_ASYNC);                           // asynchronous

.

.

.

// Do some processing.

.

.

.

// Close connection.
InternetCloseHandle(hInternetSession);


This example does exactly what the synchronous example did, but it now supplies the

INTERNET_FLAG_ASYNC parameter. This flag causes the API to do all of its processing asynchronously.

InternetConnect


You use the InternetConnect function much as discussed above, but this time you MUST supply a non-

zero context value.

// Make connection to ftp server.

HINTERNET hFTPSession;

DWORD dwContext;



// set context value



dwContext = &SharedDataStructure;                  // address of some data structure



                                                   // somewhere in memory




// Make connection to ftp server.

hFTPSession = ::InternetConnect(

             hInternetSession,                              // handle from a previous

                                                            // call to InternetOpen

             "ftp://ftp.microsoft.com",                     // server we wish to connect
to

             INTERNET_INVALID_PORT_NUMBER,                  // use appropriate port

             "anonymous",                                   // username, can be NULL

             "robcol@homesite.com",                         // password, can be NULL

             INTERNET_SERVICE_FTP,                          // flag to use FTP services

             0,                                             // flags (see SDK docs)
             (DWORD)dwContext);                                // SEE DISCUSSION ON THIS
PARAM

.

.

.

// Do some processing.

.

.

.

// Close connection.

InternetCloseHandle(hFTPSession);


The parameters are all the same the eighth one. This parameter is a user-defined value that is passed on

to the callback function if asynchronous processing was specified during the InternetOpen call.

Note       This value CANNOT be zero if asynchronous processing is required. Setting this value to zero
effectively turns off asynchronous processing.

InternetSetStatusCallback


This is the API function that informs the system which function you want it to call when it needs to notify

you of something. This is done in a manner similar to the following:

INTERNET_STATUS_CALLBACK dwISC;



// Set up Internet status callback.

dwISC = ::InternetSetStatusCallback(                       hInternetConnection, _

                                                      InternetCallback);



// If you couldn't set up callback, process error.

if (dwISC == INTERNET_INVALID_STATUS_CALLBACK)

    {

    // . . . Process error.

    }
The two parameters are: a handle to an Internet session and the address of the callback function. The

callback function must be set up with a specific syntax, as we will see in the next section.

Internet API Callback Function


Because this function is critical to the concept of asynchronous processing, I am going to go into quite a

bit of detail on it. The code looks like this:

//*********************************************************************
*

// InternetCallback

//

// Purpose: Internet callback function used during asynchronous calls

//                 to Wininet

//

// Parameters:

//       HINTERNET hInternet - Upon first entry into the callback (during

//       the INTERNET_STATUS_HANDLE_CREATED status this value contains the

//    handle passed in during the original call to the asynchronous
Wininet

//       API. Upon INTERNET_STATUS_HANDLE_CREATED this value contains

//       the return value of the asynchronous Wininet API.

//

//       DWORD dwContext - an application-defined value associated with

//       the callback. For this application, this is a pointer to an

//       instance of a CAdvancedFTPDlg class (this).

//

//       DWORD dwInternetStatus - status value (INTERNET_STATUS_*)

//

//       LPVOID lpvStatusInformation - value returned by callback function

//       specific to the STATUS type

//
//       DWORD dwStatusInformationLength

//********************************************************************

void CALLBACK InternetCallback(HINTERNET hInternet,

                                    DWORD dwContext,\

                                    DWORD dwInternetStatus,

                                    LPVOID lpvStatusInformation,

                                    DWORD dwStatusInformationLength)

     {

     LPINTERNET_ASYNC_RESULT pIar = (LPINTERNET_ASYNC_RESULT)

                                       (lpvStatusInformation);

                LPSTR pStr =   (LPSTR) (lpvStatusInformation);



     // Act on status code.

     switch(dwInternetStatus)

         {

         // This value is selected when we are notified that the API is

         // looking up the IP address of the name contained in

         // lpvStatusInformation.

         case INTERNET_STATUS_RESOLVING_NAME:

             // Use pStr to point to the value.

             break;



         // This value is selected when we are notified that the API has

         // successfully found the IP address of the name contained in

         // lpvStatusInformation.

         case INTERNET_STATUS_NAME_RESOLVED:

             // Use pStr to point to the value.

             break;
// This value is selected when we are notified that the API is

// connecting to the socket address (SOCKADDR) pointed to by

// lpvStatusInformation.

case INTERNET_STATUS_CONNECTING_TO_SERVER:

   // Use pStr to point to the value.

   break;



// This value is selected when we are notified that the API has

// successfully connected to the socket address (SOCKADDR)

// pointed to by lpvStatusInformation.

case INTERNET_STATUS_CONNECTED_TO_SERVER:

   // Use pStr to point to the value.

   break;



// This value is selected when we are notified that the API is

// sending the information request to the server.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_SENDING_REQUEST:

   break;



// This value is selected when we are notified that the API has

// successfully sent the information request to the server.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_REQUEST_SENT:

   break;



// This value is selected when we are notified that the API is
// waiting for the server to respond to a request.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_RECEIVING_RESPONSE:

   break;



// This value is selected when we are notified that the API has

// successfully received a response from the server.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_RESPONSE_RECEIVED:

   break;



// This value is selected when we are notified that the API is

// closing the connection to the server.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_CLOSING_CONNECTION:

   break;



// This value is selected when we are notified that the API has

// successfully closed the connection to the server.

// The lpvStatusInformation parameter is NULL.

case INTERNET_STATUS_CONNECTION_CLOSED:

   break;



// This value is used when a call to InternetConnect has created

// the new handle. This lets the application call

// InternetCloseHandle from another thread

// if the connection is taking too long.

case INTERNET_STATUS_HANDLE_CREATED:
         // Use pIar to point to the structure.

         break;



      // This value is selected when we are notified that the API has

      // closed a handle.

      case INTERNET_STATUS_HANDLE_CLOSING:

         break;



      // An asynchronous operation has been completed.

      // See InternetOpen for details on INTERNET_FLAG_ASYNC.

      case INTERNET_STATUS_REQUEST_COMPLETE:

         // Check the INTERNET_ASYNC_RESULT structure for error
information.

         pIar = (LPINTERNET_ASYNC_RESULT)(lpvStatusInformation);



         // Do we have an error?

         if (!(pIar->dwResult))

            {

            // If so, then process it.

            switch (pIar->dwError)

                  {

                  //Standard error returned from FtpFindFirstFile

                  //and InternetFindNextFile.

                  case ERROR_NO_MORE_FILES:

                      break;

                  case ERROR_INTERNET_EXTENDED_ERROR:

                      //Error triggered when the server can pass back

                      //more information on what went wrong.
                           break;

                       case 1:

                           //No problem - this would be a Boolean return value

                           //representing success.

                           break;

                       default:

                           //This would be a good place to check for all the

                           //INTERNET_ERROR_* messages (for example,

                           //INTERNET_ERROR_NAME_NOT_RESOLVED) to give more

                           //descriptive output to the user.

                           return;

                       }

                  }

             break;

         }

    return;

    }


For this article we are only interested in knowing when a handle has been created and when a particular

request has been fulfilled. So we will focus on the following two valu es:

INTERNET_STATUS_HANDLE_CREATED and INTERNET_STATUS_REQUEST_COMPLETE.


The INTERNET_STATUS_HANDLE_CREATED value is sent to the callback when the InternetConnect

function has successfully created the handle you have requested. The

INTERNET_STATUS_REQUEST_COMPLETE value is sent to the callback by the WinInet API when it

has finished with a particular request, such as FtpGetFile.


Setting Up Monitoring on a Separate Thread

Setting up a function to run on another thread of execution is quite simple with either the Win32 API or

MFC. Because we are writing an MFC application we will use the MFC methodology. The MFC

AfxBeginThread function is used to create and, usually, execute the desired function on another thread.

An example might look like this:
CWinThread * Callback_Thread;

.

.

.

Callback_Thread = AfxBeginThread(

                        CallbackThread_Proc,                // function to run on thread

                        (LPVOID)0,                          // value to pass to function

                        THREAD_PRIORITY_NORMAL,             // thread's priority

                        0,                                  // stack size

                        CREATE_SUSPENDED,                   // create susupended thread

                        NULL);                              // no security attributes

.

.

.


The example above creates a "suspended" thread that can be started or resumed later at the program's

discretion. You could just as easily have created the thread to begin execution immediately. You resume

the thread, at the appropriate time, with the ResumeThread member function of the CWinThread class.

This would look like:

Callback_Thread->ResumeThread();


The actual thread function must be defined in a predetermined syntax. An example of this would look like:

UINT CallbackThread_Proc(LPVOID lParm)

    {

    UINT uResult;



    uResult = some processing



    return(uResult);

    }
Creating an Asynchronous Example

We have examined the callback function, the Event synchronization object, and how to create other

threads. Now let's put it all together and look at the changes you need to make to the previous examples

in order to create an asynchronous example. The changes are indicated by bold type. A couple of

clarifications before you go any further:



        You create two Event objects during initialization. They are hWaitForHandleCreation and

    hWaitForCompletedRequest. The hWaitForHandleCreation object signals you when the WinInet

    API has finished creating a requested handle from a call to the InternetConnect API function. The

    hWaitForCompletedRequest event object signals you when certain Internet requests, for example

    FtpGetFile, have been completed.


        You established the "callback" function on a separate thread via AfxBeginThread.

New "Callback" Function


Now you need to rewrite part of the callback function to signal you when certain things have happened.

The first thing you need to know is when the application has finished creating a handle for you; the second

is when a request has been completed. The callback example code now looks like this:

         .

         .

         .

         // This value is used when a call to InternetConnect has created

         // the new handle. This lets the application call

         // InternetCloseHandle from another thread

         // if the connection is taking too long.

         case INTERNET_STATUS_HANDLE_CREATED:



             // Use pIar to point to the structure.



             // Get new handle from structure.



             pIar = (LPINTERNET_ASYNC_RESULT)(lpvStatusInformation);
          // Store for future use in global memory or shared memory.



          hResultHandle = (HINTERNET)pIar->dwResult;




          // Let other code continue.



          ::SetEvent(hWaitForHandleCreation);



          break;

      .

      .

      .

      // An asynchronous operation has been completed.

      // See InternetOpen for details on INTERNET_FLAG_ASYNC.

      case INTERNET_STATUS_REQUEST_COMPLETE:

         //Check the INTERNET_ASYNC_RESULT structure for error
information.

          pIar = (LPINTERNET_ASYNC_RESULT)(lpvStatusInformation);

      .

      .

      .



          ::SetEvent(hWaitForCompletedRequest);



          break;
New InternetConnect Example


The next piece of code you need to modify is your example of InternetConnect, because you are

processing in an asynchronous mode. You need to wait for the INTERNET_STATUS_HANDLE_CREATED

message to be received in the callback function. You obtain the handle you need from the parameters

passed to you at this time. The code now looks like this:

// Make connection to ftp server.

HINTERNET hFTPSession;



// Set up so you have to wait until the handle is actually created.



::ResetEvent(hWaitForHandleCreation);




// Create connection (actual handle will come back through callback).

hFTPSession = ::InternetConnect(

             hInternetSession,                              // handle from a previous

                                                            // call to InternetOpen

             "ftp://ftp.microsoft.com",                     // server we wish to connect
to

             INTERNET_INVALID_PORT_NUMBER,                  // use appropriate port

             "anonymous",                                   // username, can be NULL

             "robcol@homesite.com",                         // password, can be NULL

             INTERNET_SERVICE_FTP,                          // flag to use FTP services

             0,                                             // flags (see SDK docs)



             (DWORD)dwContext);                       // SEE DISCUSSION ON THIS PARAM.

                                                      // Assume this value is passed

                                                      // to us somehow.
// Wait until handle is created.



::WaitForSingleObject(hWaitForHandleCreation);




// Retrieve actual handle from global memory or shared memory.



hFTPSession = hResultHandle;




.

.

.

// Do some processing.

.

.

.



// Close connections.

InternetCloseHandle(hFTPSession);

New FtpGetFile Example


The last piece of code you need to change is the piece that gets the files with the FtpGetFile function.

This change is necessary because the file being retrieved may be quite large or your physical connection

may be slow. Either reason may cause the transfer to take some time and you cannot use the file until it

is fully transferred. The new code looks like this:

BOOL bResult;



// Set up so you have to wait until the file is completely transferred.
::ResetEvent(hWaitForCompletedRequest);




bResult = ::FtpGetFile(

                  hFTPSession,

                  "ftp://ftp.mysite.com/reference.doc",

                  "c:\notes\reference.doc",

                  FALSE,

                  FILE_ATTRIBUTE_NORMAL,

                  FTP_TRANSFER_TYPE_BINARY,

                  dwContext);



// Wait until file is transferred.



::WaitForSingleObject(hWaitForCompletedRequest);

Putting It All Together

Putting together the examples from the article and creating a "complete" example is not simple. You must

keep in mind that there will be TWO threads or tasks running simultaneously for the final example. I will

explain what is going on as it happens.


The first function you will "build" has a single purpose in life: To set up the Internet API's callback

function. Doing so in a separate function may seem redundant, but all will be explained.

UINT SetupCallbackFunction(LPVOID lParam)

    {

    INTERNET_STATUS_CALLBACK dwISC;



    // Set up event to wait for program completion.

    ::ResetEvent(hWaitForProgramCompletion);
    // Set up Internet status callback.

    dwISC = ::InternetSetStatusCallback(                        hInternetConnection,

                                         InternetCallback);



    // Set up event to wait for program completion.

    ::WaitForSingleObject(hWaitForProgramCompletion, INFINITE);

    }


If you recognize the syntax of this function as being the same as that of a thread procedure, you are

correct. The reason I decided to launch the Internet API callback AND wait for some signal to terminate

the thread was that I want to keep this processing separate from the main application. If you do not do

this, the callback routine is viewed by the system as part of the main application and therefore could

potentially affect the main program. This is done in the sample application so that the callback routine can

cause status display updates in the main program and not have to worry about the two threads conflicting

with each other.


The second function you will build is the one that makes the connection to the FTP server, waits for the

real handle via the callback function, and then returns that value. The code looks like this:

HINTERNET ConnectToFtpServer(HINTERNET hInternetConnection,

                                    LPSTR pFTPServer,

                                    LPSTR pUsername,

                                    LPSTR pPassword,

                                    LPVOID lpContext)

    {

    // Make connection to ftp server.

    HINTERNET hFTPSession;



    // Set up so you have to wait until the handle is actually created.

    ::ResetEvent(hWaitForHandleCreation);
   // Create connection (actual handle will come back through
callback).

     hFTPSession = ::InternetConnect(

              hInternetSession,                                  // Handle from a previous

                                                                  // call to InternetOpen.

              pFTPServer,                                        // Server we want to connect
to

              INTERNET_INVALID_PORT_NUMBER,                      // Use appropriate port

              pUsername,                                         // Username, can be NULL

              pPassword,                                         // Password, can be NULL

              INTERNET_SERVICE_FTP,                              // Flag to use FTP services

              0,                                                 // Flags (see SDK docs)

         (DWORD) lpContext);                                     // Context for this
connection



     // Wait until handle is created.

     ::WaitForSingleObject(hWaitForHandleCreation);



     // Retrieve actual handle from global memory or shared memory.

return(hResultHandle);

     }


The third function you need to write is the actual callback function itself. It looks like this:

void CALLBACK InternetCallback(                      HINTERNET hInternet,

                                     DWORD dwContext,\

                                     DWORD dwInternetStatus,

                                     LPVOID lpvStatusInformation,

                                     DWORD dwStatusInformationLength)

     {

     LPINTERNET_ASYNC_RESULT pIar =                     (LPINTERNET_ASYNC_RESULT)
                       (lpvStatusInformation);

LPSTR pStr =           (LPSTR) (lpvStatusInformation);



// act upon status code

switch(dwInternetStatus)

   {

   //

   // other values same as in example above (removed for brevity)

   //



   // This value is used when a call to InternetConnect has created

   // the new handle. This lets the application call

   // InternetCloseHandle from another thread

   // if the connection is taking too long.

   case INTERNET_STATUS_HANDLE_CREATED:



        // Use pIar to point to the structure.

        // Get new handle from structure.

        pIar = (LPINTERNET_ASYNC_RESULT)(lpvStatusInformation);



        // Store for future use in global memory or shared memory.

        hResultHandle = (HINTERNET)pIar->dwResult;



        // Let other code continue.

        ::SetEvent(hWaitForHandleCreation);

        break;

   // An asynchronous operation has been completed.

   // See InternetOpen for details on INTERNET_FLAG_ASYNC.
         case INTERNET_STATUS_REQUEST_COMPLETE:

         //check the INTERNET_ASYNC_RESULT structure for error
information.

             pIar = (LPINTERNET_ASYNC_RESULT)(lpvStatusInformation);



             //

             // Other code same as in example above (removed for brevity).

             //



             ::SetEvent(hWaitForCompletedRequest);

             break;

    }


Now you code up the "main" routine to put it all together:

#include . . .

#include <WinINet.h>                  // Header file for WinINet SDK



// "Global" or shared memory

HANDLE hWaitForHandleCreation;

HANDLE hWaitForCompletedRequest;

HANDLE hWaitForProgramCompletion;

HANDLE hResultHandle;

DWORD dwContextValue;



int MainRoutine()

    {

    HINTERNET hInternetSession;

    HINTERNET hFTPSession;

    HINTERNET hFileConnection;
WIN32_FIND_DATA sWFD;

BOOL bResult = TRUE;



hInternetSession = ::InternetOpen(

                       "Microsoft Internet Explorer",   // agent

                        INTERNET_OPEN_TYPE_PROXY,       // access

                        "ftp-gw",                        // proxy server

                        NULL,                           // defaults

                        0);                             // synchronous



// Create our Event objects.

hWaitForHandleCreation = ::CreateEvent(NULL, TRUE, TRUE, NULL);

hWaitForCompletedRequest = ::CreateEvent(NULL, TRUE, TRUE, NULL);

hWaitForProgramCompletion = ::CreateEvent(NULL, TRUE, TRUE, NULL);



// Connect to remote FTP server.

// REMEMBER, THIS ROUTINE WILL NOT RETURN UNTIL YOU HAVE THE ACTUAL

// HANDLE FROM THE CALLBACK ROUTINE.

hFTPSession   = ConnectToFtpServer(hInternetSession,

                        "ftp.microsoft.com"

                        NULL,       // Use anonymous username

                        NULL,       // Use e-mail name password

                        (LPVOID)&dwContext);



// find first .ZIP file

hFileConnection = ::FtpFindFirstFile(

                    hFTPSession,

                    "*.ZIP",
                     &sWFD,

                     0,

                     0);

if (hFileConnection != (HINTERNET)NULL)

   {

   ::FtpSetCurrentDirectory(hFTPSession, "/BIN");



   while (bResult)

       {

       // Create file specs.

       InputSpec = "ftp://ftp.infosite.com/BIN/";

       InputSpec = InputSpec + sWFD.cFileName;

       OutputSpec = "c:\zipfiles\";

       OutputSpec = OutputSpec + sWFD.cFileName;



       // Set up so you have to wait until the transfer is complete.

       ::ResetEvent(hWaitForCompletedRequest);



       // Transfer the file.

       bResult = ::FtpGetFile(

           hFTPSession,

           InputSpec,

           OutputSpec,

           FALSE,

           FILE_ATTRIBUTE_NORMAL,

           FTP_TRANSFER_TYPE_BINARY,

           0);
        // Wait until file is transferred.

        // REMEMBER, THIS "WAIT" WILL WAIT UNTIL THE CALLBACK

        // ROUTINE RECEIVES AN "INTERNET_STATUS_REQUEST_COMPLETE"

        // NOTIFICATION.

        ::WaitForSingleObject(hWaitForCompletedRequest);



        // Get next file.

        bResult = ::InternetFindNextFile(

                    hFileConnection,

                    &sWFD);

        }

    }



// Cause callback thread to terminate.

::SetEvent(hWaitForProgramCompletion);



// Close down all connections.

::InternetCloseHandle(hFTPSession);

::InternetCloseHandle(hInternetSession);



// Release all Event objects.

::CloseHandle(hWaitForHandleCreation); NULL);

::CloseHandle(hWaitForCompletedRequest);

::CloseHandle(hWaitForProgramCompletion);



// Return success code.

return(0);

}
Conclusion

That is what it takes to write an asynchronous FTP transfer program. Be warned about the FtpPutFile API

function—if used incorrectly, it can fill up someone else's FTP server. The sample included with this article

uses the discussed concepts and builds on them to provide you with a fully functional, MFC GUI -based,

multithreaded FTP transfer program. Have fun with it. It was certainly fun to write.

				
DOCUMENT INFO
Shared By:
Categories:
Tags:
Stats:
views:29
posted:2/14/2010
language:English
pages:38