Advanced FTP, or Teaching Fido To Phetch

Robert Coleridge
Microsoft Developer Network Technology Group

July 29, 1996

Click to open or copy the files in the AdvFTP sample application for this technical article.

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:

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:

Table 1. Settings of Username and Password Parameters


Username

Password
Username sent to FTP server Password sent to FTP 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

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 = ::FtpGetFile(
            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:

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 values: 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:

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.