Chapter 6
File Transfer Protocol
Introduction
Up until 1994, when the World Wide Web took over the Internet, File Transfer Protocol (FTP) was the most widely used Internet client application besides e-mail. It is used as a remote shell for file access on an Internet host. Using an FTP application, you can connect to an FTP server, navigate through the available directories, and transfer files.
An FTP site can be public, private, or both. With a private account, you can be given access to the entire network’s directory structure, or just specific areas. For the longest time I used a private FTP account to manage the files on Carl & Gary’s Visual Basic Home Page.
The Internet is also home to thousands of public access FTP servers that allow anyone to connect and transfer files to and from specific directories regardless of whether they have an account on the host. This is called anonymous FTP. When you connect to an anonymous FTP site, you usually specify "anonymous" as your user name and "guest" or your e-mail address as your password. Anonymous FTP sites are used, for example, to publish a large listing of public domain and/or shareware files. One of the most famous public FTP sites for shareware is ftp.cica.indiana.edu, which has mirror sites all over the world for its famous CICA shareware library.
FTP was designed mainly for use by programs, but the FTP application itself has turned out to be a critical part of any TCP/IP implementation. FTP.EXE is also an application that is installed when you use Microsoft TCP/IP drivers in Windows for Workgroups 3.11, Windows95 or Windows NT.
In fact, FTP is built into Netscape and other World Wide Web browsers so you can browse FTP servers with the same program that you use to browse the Web.
As stated in RFC 959, there were four objectives in the design of the FTP protocol:
1. To promote sharing of files (computer programs and/or data).
2. To encourage indirect or implicit (via programs) use of remote computers.
3. To shield a user from variations in file storage systems among hosts.
4. To transfer data reliably and efficiently.
When Should You Use FTP?
If you are writing an application that does a fair amount of file transfer and are considering using FTP as your primary means of transferring files, you should know a few things. First of all, FTP is a client/server protocol. Using FTP to transfer files from one application to another on the same machine is not practical. You should consider FTP only if you have to transfer files with a known FTP server, or if you are writing a general-use FTP client program.
Sometimes it’s a good idea to use an FTP server as a repository for files shared by all the users of your application. It completely depends on what your project goals are. If you want to give your users access to a bunch of shared files, FTP is a good tool for the job.
FTP does not have file control commands such as VB’s Open, Input #, and Print # commands. If your project requires that you open a file remotely and have file-level access to it then FTP will not work. FTP is used primarily for getting directory listings and transferring files.
This chapter includes code that lets you connect to, navigate, and transfer files to and from any FTP server. I will show you how to use these routines in your VB applications, as well as how they work.
The FTP Program
FTP refers to both the FTP protocol and an FTP application. The FTP protocol defines a series of commands that the client sends the server, and how the client and server transfer data. An FTP application is usually a character-based terminal-type application in which you connect to an FTP server. The purpose for the FTP application is to provide English-like commands and help/error messages, as well as higher-level functionality than just a terminal could provide.
If you are using Microsoft’s built-in TCP/IP drivers then you should have a file in your Windows directory called FTP.EXE. This is an FTP client application. I have noticed that many Windows users are not hip to FTP at all. I feel that knowing how to use FTP ranks right up there with knowing how to use the Windows File Manager or Explorer. Given that, I feel I should educate you on how to use it.
Figure 6.1 shows the FTP window. You may notice that it looks like a DOS window. It is a character-based terminal program. You can view it either in a window or full screen, just like any DOS text-mode application. If you are using Windows NT you can enjoy some nicer features such as scrolling back through your commands with the uparrow.
Figure 6.1 FTP window. FTP.EXE is a simple FTP client that comes with Windows 95/98/NT, and MS TCP/IP drivers for WFW 3.11.
Connecting and Logging In
When you first run FTP you are presented with the following prompt:
ftp> _
You can connect to any FTP site with the OPEN command. Type OPEN followed by the name of the FTP server. For example, to connect to Microsoft’s public FTP site type the following:
ftp> open ftp.microsoft.com <enter>
At this time the program attempts to connect to the server on port 21. Once connected, you will receive a 220 reply followed by a welcome message. Here’s the welcome message at ftp.microsoft.com:
Connected to ftp.microsoft.com.
220 ftp Microsoft FTP Service (Version 1.0).
Next, you get a prompt to enter your username. If you have an account on the server you can enter your username, but for public access (anonymous FTP) just type anonymous:
User (ftp.microsoft.com:(none)): anonymous
331 Anonymous access allowed, send identity (e-mail name) as password.
Next you are asked for a password. Again, if you have an account you can enter your password here, but if you are connecting for public access, just enter your e-mail address. Note that your password is not echoed to the screen.
Password:
The server then grants you access with another welcome message, and you finally get the FTP prompt again. Think of this like a DOS prompt. There are a fixed set of commands that you can use to navigate through the directories on the server and download files.
230-This is ftp.microsoft.com. See the index.txt file
in the root directory for more information
230 Anonymous user logged in as anonymous.
Listing Directories
One of the commands you can use to navigate through directories is DIR. In reality there is no DIR command in the FTP protocol spec. However, the standard user interface for accessing FTP servers has brought this command forward from the operating system because it’s more user friendly (as if typing a bunch of commands into a terminal is at all user friendly).
Figure 6.2 shows what you get when you type DIR at an FTP prompt. Note that there’s a lot more information here than you were probably expecting. In the rightmost column is the file or directory name. To the right of that is the file date and size. All the way to the left is a field of 10 bits. These are attributes. In DOS there are a limited number of attributes for files and directories such as Hidden, Read-Only, and Archive. However, Unix has a much more extensible set of attributes. You can create masks that give access to certain groups of users or prevent other users from gaining access. You can tell which is a file and which is a directory by the "d" attribute, which is displayed in the far-left attribute field. If there is a "d" then it’s a directory.
Figure 6.2 The DIR command displays a directory listing.
ftp>dir
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
d--------- 1 owner group 0 Jul 3 13:52 bussys
d--------- 1 owner group 0 Aug 9 3:00 deskapps
d--------- 1 owner group 0 Oct 27 7:35 developr
---------- 1 owner group 7905 Oct 5 8:53 dirmap.htm
---------- 1 owner group 4510 Oct 5 8:52 dirmap.txt
---------- 1 owner group 712 Aug 25 1994 disclaimer.txt
---------- 1 owner group 860 Oct 5 1994 index.txt
d--------- 1 owner group 0 Aug 31 12:17 KBHelp
---------- 1 owner group 7393252 Nov 28 4:04 ls-lR.txt
---------- 1 owner group 914179 Nov 28 4:05 ls-lR.Z
---------- 1 owner group 766409 Nov 28 4:04 LS-LR.ZIP
d--------- 1 owner group 0 Oct 20 9:27 MSCorp
---------- 1 owner group 28160 Nov 28 1994 MSNBRO.DOC
---------- 1 owner group 22641 Feb 8 1994 MSNBRO.TXT
d--------- 1 owner group 0 Oct 11 3:00 peropsys
d--------- 1 owner group 0 Aug 23 21:55 Products
d--------- 1 owner group 0 Oct 5 8:46 Services
d--------- 1 owner group 0 Nov 22 14:38 Softlib
---------- 1 owner group 5095 Oct 20 1993 support-phones.txt
---------- 1 owner group 802 Aug 25 1994 WhatHappened.txt
226 Transfer complete.
1407 bytes received in 0.99 seconds (1.42 Kbytes/sec)
Changing Directories
You can change directories with the CD command. CD works exactly like it does in DOS but using forward slashes instead of backslashes. Here is the command to change to the /developr/vb directory:
ftp> cd developr/vb
250 CWD command successful.
Once again, typing DIR gives you a list of files and directories. Figure 6.3 shows the result of typing DIR in the /developr/vb directory on ftp.microsoft.com.
Figure 6.3 Listing of the /developr/vb directory of Microsoft.
ftp> dir
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
d--------- 1 owner group 0 Oct 25 6:39 kb
d--------- 1 owner group 0 Feb 24 11:35 public
---------- 1 owner group 1571 Aug 24 1994 README.TXT
d--------- 1 owner group 0 Aug 24 1994 unsup-ed
226 Transfer complete.
270 bytes received in 0.22 seconds (1.23 Kbytes/sec)
Downloading
Downloading a file is simple and straight ahead. Before you download, however, you must make sure you are in binary mode. There are two modes, ASCII and binary. To change to binary mode just type BIN:
ftp> bin
200 Type set to I.
To change back to ASCII mode type ASC. You do not have to change back and forth. In fact, I usually leave FTP in binary mode all the time so I don’t forget and download a file in ASCII mode. (Don’t run that .EXE! I used ASCII mode! Yikes!)
The GET command is used to retrieve a file. If you want to download with its original filename in the default directory, just type GET <filename> <enter>. Figure 6.4 shows this interaction.
Figure 6.4 The Get command retrieves a file from the server.
ftp> get readme.txt
200 PORT command successful.
150 Opening BINARY mode data connection for readme.txt(1571 bytes).
226 Transfer complete.
1571 bytes received in 3.46 seconds (0.45 Kbytes/sec)
You can also just type GET, after which you are prompted for the file you want to download, and then again for the name of the file (and path) on your system. This makes it easier to move files from a Unix or Win32 system (where filenames are long) to a Windows for Workgroups machine, or just to download to a directory other than where your FTP.EXE application is.
Uploading
You can upload a file in much the same way with the SEND command. You must be in a public area that allows uploads, of course. Figure 6.5 shows an example of uploading a file to Carl & Gary’s VB file upload area (ftp.cgvb.com/uploads).
Figure 6.5 The SEND command sends a file to the server.
ftp> send
(local-file) myfile.zip
(remote-file) myfile.zip
200 PORT command successful.
150 Opening BINARY mode data connection for myfile.zip.
226 Transfer complete.
3018 bytes sent in 0.06 seconds (50.30 Kbytes/sec)
Supported Commands
If you want to view a list of supported commands, just type HELP. Figure 6.6 shows what you get on ftp.microsoft.com when you type HELP.
Figure 6.6 The Help command returns a list of supported commands.
ftp>help
Commands may be abbreviated. Commands are:
! delete literal prompt send
? debug ls put status
append dir mdelete pwd trace
ascii disconnect mdir quit type
bell get mget quote user
binary glob mkdir recv verbose
bye hash mls remotehelp
cd help mput rename
close lcd open rmdir
ftp>
Ending the Session
You can end your FTP session at any time by typing BYE at any FTP prompt.
ftp> bye
<the server disconnects the client>
Using a Web Browser to Download Files
There is a much easier way to download files from an anonymous FTP site. Use a web browser— you can download any file by either making a link to it in an HTML document and clicking the link, or simply entering an FTP URL in the location edit window (which most web browsers display at the top of the window directly under the menu section). Using a web browser for FTP access is good and fast, but it is limited in what you can access. For example, you cannot send files (at least, there is no standard way). But for downloading files from public sites, you can’t beat it.
Here is an example URL that downloads a Winsock API Trace utility from Carl & Gary’s VB Home Page FTP server:
ftp://ftp.cgvb.com/pub/misc/tpwins32.zip
If you enter the URL directly from the browser make sure you turn on the "save to disk" option. If you create an HTML file with a link, the file should look like this:
<a href="ftp://ftp.cgvb.com/pub/misc/tpwins32.zip">tpwins32.zip</a>
Using your web browser you can click on the link while holding down the Shift key to save to disk.
The FTP Protocol
The commands you give to an FTP application are a bit different from the commands that an FTP application gives to an FTP server. For example, to get a directory listing with an FTP application you would use the DIR command. However, the FTP application uses the LIST command. When I refer to FTP from now on, I am referring to the protocol, unless otherwise noted.
If there is any one big difference between FTP and the protocols that we’ve covered up to this point, it is that FTP uses more than one port. Port 21, the control connection, is used for transferring commands, and another port, the data connection, is used for transferring data. The default port for the data connection is 20, but any other port can be used also. This makes FTP a wee bit more difficult to code than, say, SMTP or POP3.
FTP Errata
Yes, there is plenty of weirdness concerning FTP, and why not? FTP has long been the staple file transfer mechanism of the Internet community. Because it is so popular, everyone has a way to improve on it. Therefore, there is tons and tons of addenda, comments, and additions to the FTP protocol. This has yielded some inconsistencies in FTP commands between servers. For that reason you must exercise a bit of caution when extending the power of FTP to your users.
For example, if your client only connects to your server and you have tested the connection then you needn’t worry. On the other hand, if you incorporate FTP as a means to grab any file from any FTP server, and extend that functionality to the user, you may be in for a headache. Most third-party FTP tools maintain a list of server types and the different commands that the server may or may not understand. Using the FTP protocol, if you send the SYST command, the FTP server will send you back the FTP server name and version. It is beyond the scope of this book to list all of the different inconsistencies among FTP servers.
I am not trying to make it sound like FTP is not a standard. For the most part, FTP server implementations stick to the standard commands. The commands in this chapter were taken from RFC 959, which is the standard FTP protocol definition. Any deviation from this standard is a risk, and software shops should know this.
Connections
The FTP server accepts initial connections on port 21. Unlike HTTP, which reconnects on every command, FTP keeps the connection open. This connection is for the processing of FTP commands only. A separate connection is used for data transfer. These two connections are called the control connection and the data connection, respectively.
For example, when retrieving a file the client usually sends the PORT command, accepts a connection on port 20 and then tells the server to send the file using the RETR command. The server then sends the data, and closes the connection. The reason this method is used and not the familiar send data ending with a period on a line by itself method, is because FTP sends binary data. There is no practical way to interpret an end-of-line character when every possible character could be interpreted as data.
Another option is for the client to tell the server to listen to a particular port with the PASV command (indicating passive mode), and then connect to that port for the data connection. I like to use passive mode because you don’t have to accept a connection, which isn’t always possible.
If you are going to use the PORT method, it is best to open either port 20 or the next available port over 1024 and then send a PORT command to the server. That way, if the user is already transferring a file with a standalone FTP application, there is no chance of interfering with it. (I say to use ports over 1024 because ports 1–1024 are reserved for TCP/IP and standard protocols.)
FTP Commands
FTP uses a series of simple commands such as LIST and RETR that perform the various tasks of navigating directories and transferring files with an FTP server. These commands are listed in the FTP Reference in Appendix D at the back of this book. FTP commands are always followed with a CR/LF pair.
Server Responses
After sending an FTP command, you will receive a reply that consists of a three-digit number followed by a space and a text message. Figure 6.7 shows a list of reply codes in numeric order. The full descriptions of these codes can be found in RFC 959.
Figure 6.7 Reply codes.
· 110 Restart marker reply.
In this case, the text is exact and not left to the particular implementation; it must read:
MARK yyyy = mmmm
where yyyy is User-process data stream marker, and mmmm is server's equivalent marker (note the spaces between markers and "=").
· 120 Service ready in nnn minutes.
· 125 Data connection already open; transfer starting.
· 150 File status okay; about to open data connection.
· 200 Command okay.
· 202 Command not implemented, superfluous at this site.
· 211 System status, or system help reply.
· 212 Directory status.
· 213 File status.
· 214 Help message.
On how to use the server or the meaning of a particular
nonstandard command. This reply is useful only to the
human user.
· 215 NAME system type.
NAME is an official system name from the list in the
Assigned Numbers document.
· 220 Service ready for new user.
· 221 Service closing control connection.
Logged out if appropriate.
· 225 Data connection open; no transfer in progress.
· 226 Closing data connection.
Requested file action successful (for example, file
transfer or file abort).
· 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
· 230 User logged in, proceed.
· 250 Requested file action okay, completed.
· 257 "PATHNAME" created.
· 331 User name okay, need password.
· 332 Need account for login.
· 350 Requested file action pending further information.
· 421 Service not available, closing control connection.
This may be a reply to any command if the service knows it
must shut down.
· 425 Can't open data connection.
· 426 Connection closed; transfer aborted.
· 450 Requested file action not taken.
File unavailable (e.g., file busy).
· 451 Requested action aborted: local error in processing.
· 452 Requested action not taken.
Insufficient storage space in system.
· 500 Syntax error, command unrecognized.
This may include errors such as command line too long.
· 501 Syntax error in parameters or arguments.
· 502 Command not implemented.
· 503 Bad sequence of commands.
· 504 Command not implemented for that parameter.
· 530 Not logged in.
· 532 Need account for storing files.
· 550 Requested action not taken.
File unavailable (e.g., file not found, no access).
· 551 Requested action aborted: page type unknown.
· 552 Requested file action aborted.
Exceeded storage allocation (for current directory or
dataset).
· 553 Requested action not taken.
Filename not allowed.
Reply Code Categories
Each digit of the reply code has a specific meaning. There are five values for the first digit of the reply code: 1 indicates a positive preliminary reply (the command was accepted, and this is the first of more than one positive reply from the server); 2 indicates a permanent positive reply; 3 indicates a positive intermediate reply, in which case the server is waiting for more information; 4 indicates that the command was not accepted and the requested action did not occur, yet the condition may be temporary; 5 indicates absolute failure.
The second digit indicates the category of the reply: 0 indicates a syntax error; 1 indicates informational content; 2 indicates a message concerning the transmission channel; 3 refers to authentication or accounting messages; 4 is not used; and 5 indicates a message regarding the file system status. The third digit merely specifies the level of granularity of messages in a particular category.
Figure 6.8 shows a quick summary of how to interpret FTP reply codes. You should consult RFC 959 for a complete discussion.
Figure 6.8 Interpreting FTP reply codes.
1xx Positive Preliminary Reply
2xx Positive Reply
3xx Positive Intermediate Reply
4xx Transient Negative Completion Reply
5xx Permanent Negative Completion Reply
x0x Syntax error
x1x Information
x2x Connections
x3x Authentication and accounting
x4x Unspecified as yet
x5x File system
Visual Basic Code
The project FTPDEMO.VBP is an FTP client application. With it, you can connect to any FTP server, navigate and list directories, and send and receive files. Figure 6.9 shows the FTPDemo program. FTPDemo does not have a command parser, such as does the FTP.EXE application. Instead, it has a series of command buttons for FTP actions.
Though this code is a terrific teaching tool, you should use the cfFTP object outlined in Chapter 9 if you want to use FTP code in your own VB applications. Since cfFTP is an object, it can be accessed easily from any VB application.
Figure 6.9 FTPDemo program.
The form, frmFTP, has three DSSock controls on it; one for the control connection (dsSocket1), one to accept a data connection, and the third for the data connection itself. The module FTP.BAS then has FTP routines such as FTPLogon that log you into the FTP server. FTP.BAS requires FTP.FRM and vice versa. First, I’ll show you how to use the FTP routines, and then we’ll get into how they work.
Using a Display Terminal
The FTP code has a provision for using a list box or a text box to display messages and directory listings from the FTP server. If you want to use this feature, you need to modify one line of code in FTP.BAS. Specifically, the DisplayMessage routine has a line that sets a local object called Display to a list box or a text box. Simply change this line to point to your list box or text box on any form:
'-- Set your control name here (Text Box or List Box)
Set Display = frmMain.List1
You can, of course, modify this routine to display text in any type of display control. The Demo project (FTPDemo) uses a list box as a display window.
FTPLogon
The first thing you need to do is connect to an FTP server. Use the FTPLogon function for this. Here is the syntax:
Success% = FTPLogon (ServerName$, UserName$, Password$, Timeout%)
The return value is True if successfully connected and authorized, and False if there was any problem either connecting or logging in. ServerName$, UserName$, and Password$ are self-explanatory. Timeout% is the number of seconds to wait for a successful connection.
Here is an example of calling FTPLogon that connects to Carl & Gary’s Visual Basic Home Page FTP Server anonymously as [email protected], and waits up to 30 seconds for a connection:
If FTPLogon ("ftp.cgvb.com", "anonymous", "guest", 30) Then
MsgBox "Connected!"
Else
MsgBox "There was a problem connecting or logging in"
End If
SendFTPCommand
SendFTPCommand has provisions for any command that requires a data connection, such as RETR and STOR. If a data connection is required, it first sends the command to set up the data connection, and then it sends the command. Here is the syntax:
SendFTPCommand FTPCommand$, BinaryMode%, FileName$
FTPCommand$ is any valid FTP command.
FileName$ is an optional argument that specifies a FileName when sending a file with STOR or STOU, or retrieving a file with RETR.
When retrieving a file using the RETR command, FileName$ is the local name that the file will be saved to. When sending a file using STOR or STOU, FileName$ specifies the name of file on the server that will be created.
BinaryMode indicates whether or not the output from the server is sent in binary mode or ASCII mode. Setting BinaryMode to True indicates binary mode. In general you should send and receive all files using BinaryMode True. When retrieving directory listings with LIST, you should specify BinaryMode False.
Here is an example in which MYFILE.ZIP is downloaded:
SendFTPCommand "RETR", True, "MYFILE.ZIP"
Retrieving a Directory Listing
To retrieve a directory listing of the current directory on the FTP server, use the RETR command with the following syntax:
SendFTPCommand "LIST", False
Note that you do not have to specify a filename if it is not required. SendFTPCommand uses the Visual Basic 4.0 Optional keyword for the FileName argument, so it is not required.
You can also return a listing of files that match a particular spec. For example, the following command returns all files with an extension .zip:
SendFTPCommand "LIST *.zip", False
If you are using a display terminal then the directory listing will be displayed. If not, then you will have to intercept the data in the frmFTP.DSSocket2_Receive event, which is where all data received via the data connection enters your application.
Changing Directories
To change to a new directory, send the CWD (Change Working Directory) command specifying the directory name. For example, to change to /uploads:
SendFTPCommand "CWD /uploads", False
Downloading a File
To download a file, send the RETR command with the name of the file. The FileName$ argument is the name to which the downloaded file will be saved. When sending RETR, make sure you specify nBinaryMode as True. Here is an example that downloads a file in the current FTP server directory called MYFILE.ZIP and saves it locally to C:\FILES\MYFILE.ZIP:
SendFTPCommand "RETR MYFILE.ZIP", True, "C:\FILES\MYFILE.ZIP"
GetFileFromURL
You can also use the GetFileFromURL to connect to an anonymous FTP server, download a file, and disconnect all in one shot. This is similar to using a web browser to download a file from an anonymous FTP site. Here is the syntax:
Success% = GetFileFromURL%(URL$, DestPath$, Email$, Timeout%)
The return value is True if the file was downloaded and False if there was a problem with either connecting or downloading. URL$ is a URL pointing to an anonymous FTP file. It can either start with file:// or ftp://. DestPath$ is a local directory name where the file will be saved. Email$ is your e-mail address (used to connect anonymously). If you leave this blank, then "guest" is used as an e-mail address (some sites don’t allow this). Timeout% is the number of seconds to wait for a connection before the function returns False.
Here is an example that downloads the file sleigh.zip from ftp.northpole.com in the directory /pub/games and saves it to the local C:\ directory. The user’s e-mail address is specified as [email protected], and the routine will wait up to 30 seconds for a connection.
If GetFileFromURL("file://ftp.northpole.com/pub/games/sleigh.zip", _
"c:\", "[email protected]", 30) Then
MsgBox "Success!"
Else
MsgBox "Failure"
End If
The GetFileFromURL function does not return to your code until it has either successfully downloaded the file or determined that there was an error.
Uploading a File
To send a file to an FTP Server you can either use SendFTPCommand or the SendFile routine. SendFile is a wrapper for SendFTPCommand, and adds the benefit of automatically setting the transfer mode to binary, and waiting until your file is sent before it returns. Here is the syntax for SendFile:
ErrCode% = SendFile%(SourceFileName$, DestFileName$, ErrorMessage$)
SendFile% returns zero if all goes well; otherwise it returns the FTP reply that represents an error that occurred. To use SendFile you must already be connected to the FTP server (see FTPLogon).
SourceFileName$ is the name of a local file to be sent. DestFileName$ is the path and filename of the file that will be written on the server. The path uses forward slashes (/) not backslashes (/) and must not contain wildcard characters. ErrorMessage$ returns with an error message if an error occurs.
Here is an example that sends the file c:\myfile.zip to /pub/uploads on the server:
Dim szErrMsg As String
If SendFile("c:\myfile.zip", "/pub/uploads/myfile.zip", szErrMsg) Then
MsgBox szErrMsg, vbInformation
End If
Debugging
My FTP code (in fact, all the code) comes with an excellent debugging tool for those times when you just want to know what’s going on in the code. Stepping through the code is impossible during an FTP session, so the only way to see the flow of data is to write a log file.
The WriteLogFile function is a routine that writes a string to a file in the current directory called ERRORLOG.TXT. It writes the date and time, and the string to the file. If the file is not open it is opened. The string is automatically appended to the file. WriteLogFile writes to the log file only if you use /D on the command line of your application.
Even if you are not having trouble with the code, chances are that sooner or later someone is going to have trouble. You can tell them to start your application with /D and then send you the LOG file. That way you can trace what happens; the last line written to the log indicates the last successful action that the program took before the error occurred.
Figure 6.10 shows the WriteLogFile routine, contained in DSSOCK.BAS. The code checks the command line for /D. The string is written to the file only if it has been specified. Notice that the date and time is also written to the log file.
Figure 6.10 WriteLogFile writes error messages to a file.
Sub WriteLogFile(szData As String)
'-- File handle for the log file (if used)
Dim nLogFileNum As Integer
On Error Resume Next
If InStr(UCase$(Command$), "/D") Then
'-- Open The File
nLogFileNum = FreeFile
Open App.Path & "\" & szLogFileName For Append As nLogFileNum
'-- Write the string
Write #nLogFileNum, Str$(Now) & Chr$(9) & szData
'-- Close the file
Close nLogFileNum
End If
End Sub
You can use WriteLogFile in any of the applications (or any of your own for that matter). I find it an invaluable little debugging tool for real-time communications.
WriteLogFile is called at every critical point in the FTP code, as listed here.
Routine Description
SendData Sending data to a server (writes the data).
dsSocket1_Close When the control connection is closed by the server.
dsSocket1_Connect When the control connection is made between you and the server.
dsSocket1_Exception When a WinSock error occurs in the control connection (writes the error).
dsSocket1_Receive When data is received on the control connection (writes the data).
dsSocket1_SendReady When the control connection becomes ready to send data.
dsSocket2_Accept When a data connection has been established with the server by means of the server connecting to you (PORT).
dsSocket2_Close When the data connection is closed by the server.
dsSocket2_Connect When a data connection has been established with the server by means of you connecting to the server (PASV).
dsSocket2_Exception When a WinSock error occurs in the data connection (writes the error).
dsSocket2_Receive When data is received on the data connection (writes the data).
dsSocket2_SendReady When the data connection becomes ready to send data.
CloseDataConnection When the code closes the data connection (even if it’s not open).
As you can see, using /D will create quite a nice little error log for you to study. I used it several times in debugging the FTP code.
Inside the FTP Code
The FTP Code is a bit complex because of the flags and globals that help bridge the gap between the routines and the FTP form, where all the data transfer occurs. Instead of going over the code line by line, I will instead walk through the data flow using the typical example uses of FTP, connecting to a server, getting a directory, and transferring files. If you are the curious type, you can read the well-commented code.
Take a look at the FTP Form. There are two DSSocket controls: the first (DSSocket1) is used for the control connection and the second (DSSocket2) is used for the data connection. Because there can be more than one data connection, this control is an array, having an index of zero.
Connecting to the Server
This is done with FTPLogon. FTPLogon does more than just connect you to the server, it logs you into the server and waits for the server to say you are logged in. Figure 6.11 shows the FTPLogin function.
Figure 6.11 The FTPLogon routine connects and logs into an FTP server.
Function FTPLogon(szHostAddress As String, szUserName As String, _
szPassword As String, nTimeout As Integer)
Dim EndTime
Screen.MousePointer = vbHourglass
'-- Set Line Mode and EolChar (Linefeed)
frmFTP.dsSocket1.LineMode = True
frmFTP.dsSocket1.EOLChar = 10
'-- Set the username and password
gszUserName = szUserName
gszPassword = szPassword
gnFTPReady = False
'-- Connect (with a timer)
If SocketConnect(frmFTP.dsSocket1, 21, szHostAddress, nTimeout) = 0 Then
'-- Use the same timer value to wait for gnFTPReady
' which is set in DSSocket1_Receive
EndTime = DateAdd("s", nTimeout, Now)
Do
DoEvents
If Now >= EndTime Then
Exit Do
End If
Loop Until gnFTPReady
'-- Success?
If gnFTPReady Then
'-- Yes! We be connected
FTPLogon = True
Else
'-- Error. disconnect
SocketDisconnect frmFTP.dsSocket1
End If
End If
Screen.MousePointer = vbNormal
End Function
The first thing this routine does is make sure that DSSocket1 (the control connection) is in Line mode (not binary) and that the end-of-line character is a linefeed.
Next it sets two globals (gszUserName and gszPassword) to the username and password passed to FTPLogin. These will be used in the DSSocket1_Receive event to answer the server’s request for the username and password.
Also, the flag gnFTPReady is initialized to False. This is set to True when we have received login confirmation from the server.
Next, SocketConnect is called to connect to the FTP server. If you remember back in Chapter 2 on Winsock programming, SocketConnect is the generic connect routine for any type of Internet host.
Check out the next block of code:
EndTime = DateAdd("s", nTimeout, Now)
Do
DoEvents
If Now >= EndTime Then
Exit Do
End If
Loop Until gnFTPReady
This code waits for gnFTPReady to be set to True, indicating a successful FTP login, but exits after nTimeout number of seconds has expired.
The timing code uses the Now function, which returns a date/time value of the exact moment when it is called. The DateAdd function adds nTimeout number of seconds to Now, and returns a value that represents nTimeout number of seconds in the future. Inside the loop, Now is tested against EndTime and exits if the number of seconds has elapsed.
If indeed the loop exits because of a timeout then gnFTPReady will be false. If this is so, then we did not connect; otherwise FTPLogin returns True to indicate success.
Let’s look at the data flow that actually happens when you connect. Immediately after connection, the FTP Server sends a 220 reply with a welcome string. For example:
220 ftp.cgvb.com FTP server (Version wu-2.4.2-academ[BETA-16](1) Sun Apr 26 07:0
9:33 EDT 1998) ready.
In the DSSocket1_Receive event, a Select Case statement is set up on the value of the reply code (the value of the leftmost three digits of ReceiveData). If the code is 220, then we have just logged into the server and should send the USER command with the user name. Here is the code to handle this precise moment:
Case 220 '-- Service ready for new user.
SendData dsSocket1, "USER " & gszUserName & vbCrLF
Now the USER command (specifying the username) has been sent and we should get back a 331 reply indicating that the username requires a password. Here is the code to handle this (also in dsSocket1_Receive):
Case 331 '-- User name okay, need password.
SendData dsSocket1, "PASS " & gszPassword & vbCrLf
Now, if the password is accepted, then we will get back a 230 command. If this occurs, then gnFTPReady is set to True in the following clause:
Case 230 '-- User logged in, proceed.
'-- This flag tells FTPLogin that weÃre actually logged in.
gnFTPReady = True
Once the gnFTPReady flag is set to True, then the loop in FTPLogin exits, FTPLogin is set to True, and we’re in!
Reality Break
About this time is where most people want to put the book down and use a custom control to do all this. Believe me, it’s not as complex as you think, and you should avoid using controls whenever possible. Remember, I am showing you the innards of the code that you can access at a high level. The dirty work is done. You have the added benefit of being able to tinker with the code, which you don’t get with an FTP control. However, I must stress that if you want to use this code in your own applications you should probably use the cfFTP class outlined in Chapter 9.
So take a few deep breaths, count to 11,239, pour a cup of coffee, put on your fuzzy slippers, and forge ahead. Just think of all the boneheads you’ll impress with just the buzzwords alone, let alone the fact that you’ll be able to communicate with FTP servers in Visual Basic, something that my cats can’t even do (and they’re not stupid!).
Inside SendFTPCommand
Figure 6.12 shows the SendFTPCommand routine. I want to bring to your attention right off the bat the block of code that begins with:
If gszLastCmdSent <> "TYPE" Then
This code determines if the user has specified a change of transfer mode (binary to ASCII or vice versa). If so, the command string passed in to SendFTPCommand is saved in the global gszDataCommand variable, the TYPE command is sent to change the mode, and the routine is exited. When the OK reply (200) is received after sending the TYPE command, the original command (gszDataCommand) is then issued.
Figure 6.12 SendFTPCommand command lets you send any FTP command to the server.
Sub SendFTPCommand(szCommand As String, szFileName As String, _
nBinaryMode As Integer)
'-- This function sends any command that requires a data connection.
' These commands are "RETR", "APPE", "LIST", "NLST", "STOR", and "STOU".
' Add more as they become necessary.
Dim nPos1 As Integer
Dim nPos2 As Integer
Dim nPos3 As Integer
Dim nSpace As Integer
Dim szAddr As String
Dim szPort As String
'-- Set up an error handler
On Error GoTo ERR_SendDataCommand
WriteLogFile "SendFTPCommand: " & szCommand & ", " _
& szFileName & "," & Str$(nBinaryMode)
'-- Save the command internally
gszLastCmdSent = UCase$(Left$(szCommand, 4))
gnFileOK = False
If gszLastCmdSent <> "TYPE" Then
'-- Handle the data mode (binary or ASCII)
If gnLastMode <> nBinaryMode Then
gnLastMode = nBinaryMode
gszDataCommand = szCommand
gszLastCmdSent = "TYPE"
gszFileName = szFileName
If nBinaryMode Then
SendData frmFTP.dsSocket1, "TYPE I" & gszCRLF
Else
SendData frmFTP.dsSocket1, "TYPE A" & gszCRLF
End If
Exit Sub
End If
End If
gszFileName = szFileName
Select Case gszLastCmdSent
Case "RETR", "APPE", "LIST", "NLST", "STOR", "STOU"
'-- These commands require setting up a data connection.
gnBinaryMode = nBinaryMode
'-- Make sure the data connections are closed
CloseDataConnection
'-- Close the data file if open.
If gnFileNum Then
Close gnFileNum
End If
gnFileNum = 0
'-- Save this command
gszDataCommand = szCommand
'-- If gnPassiveMode is True, then we connect to the
' FTP server, otherwise it connects to us (for a
' data connection).
If gnPassiveMode Then
gszLastCmdSent = "PASV"
'-- Send the PASV command
SendData frmFTP.dsSocket1, "PASV" & gszCRLF
Else
On Error Resume Next
frmFTP.dsSocket2(0).Action = SOCK_ACTION_CLOSE
On Error GoTo ERR_SendDataCommand
'-- Tell the data connection socket to listen to
' the next available port.
frmFTP.dsSocket2(0).LocalPort = 0
frmFTP.dsSocket2(0).LocalDotAddr = ""
frmFTP.dsSocket2(0).ServiceName = ""
frmFTP.dsSocket2(0).Action = SOCK_ACTION_LISTEN
'-- Devise a PORT command to tell the FTP server where
' to connect.
szAddr = frmFTP.dsSocket2(0).LocalDotAddr
nPos1 = InStr(szAddr, ".")
nPos2 = InStr(nPos1 + 1, szAddr, ".")
nPos3 = InStr(nPos2 + 1, szAddr, ".")
szPort = "PORT " & Left(szAddr, nPos1 - 1) & ","
szPort = szPort & Mid(szAddr, nPos1 + 1, nPos2 - nPos1 - 1) & ","
szPort = szPort & Mid(szAddr, nPos2 + 1, nPos3 - nPos2 - 1) & ","
szPort = szPort & Mid(szAddr, nPos3 + 1, _
Len(szAddr) - nPos3) & ","
szPort = szPort & frmFTP.dsSocket2(0).LocalPort \ 256 & ","
szPort = szPort & frmFTP.dsSocket2(0).LocalPort Mod 256
gszLastCmdSent = "PORT"
'-- Send the port command
SendData frmFTP.dsSocket1, szPort & gszCRLF
End If
Case Else
'-- Send the specified command
SendData frmFTP.dsSocket1, szCommand & gszCRLF
End Select
Exit Sub
ERR_SendDataCommand:
Debug.Print "Error" & Str$(Err) & ": " & Error
On Error Resume Next
Exit Sub
End Sub
This intermediate step takes the burden off you as the programmer of constantly sending a TYPE command before every single command.
At the top of the routine the leftmost four characters of the command are saved as the global gszLastCmdSent. This variable always contains the last command sent via SendFTPCommand, and is necessary for determining action to be taken based on the codes received.
Changing Directories
The CWD command is extremely simple. Use the following example as a model for syntax:
SendFTPCommand "CWD /pub/cdrom/win3", False
When you send this command, the server will change directories and send back a 250 reply to notify success. There is no special handling for the CWD command in the code, but you can add it by simply looking at gszLastCmdSend. If it’s CWD, then you have yourself a handler.
Creating a Data Connection
The next thing the code does is determine if the command requires a data connection. There are two basic types of FTP commands: those that require a data connection and those that don’t. For example, the CWD command does not require a data connection. The commands RETR, APPE, LIST, NLST, STOR, and STOU all require a data connection.
Select Case gszLastCmdSent
Case "RETR", "APPE", "LIST", "NLST", "STOR", "STOU"
A data connection can be created one of two ways. You can either connect to the FTP server on a port that the server gives you, or you can tell the server to connect to you on a port you give it. The PORT command tells the server to connect to you, and the PASV (passive) command asks the server for a port to connect to for the data connection.
Usually the PORT command is used, but there are situations when you want to connect to the server for a data connection, such as when you are behind a firewall that does not allow incoming connections. It is for this reason that I use PASV in the demo code as the default method of creating a data connection.
The gnPassiveMode variable determines who connects to whom for the data connection. Set gnPassiveMode to False if you want to accept the data connection, otherwise leave it set to True (the default).
If gnPassiveMode is True, then the PASV command is sent; otherwise a connection is created with DSSocket2(1). In the case that gnPassiveMode is False, the code that begins with the following comment devises a string to be sent to the server that defines the IP address and port that DSSocket2(0) is listening on:
'-- Devise a PORT command to tell the FTP server where
' to connect.
Once the new control is listening, the PORT command is sent with the aforementioned string that defines the IP address and port for the data connection, and the routine exits.
At this point we’ve sent the PORT command and are waiting for a reply. When DSSocket2(0) answers the connection, it passes the socket to DSSocket2(1) and the data connection is established, and the FTP server sends an OK reply (200). If, when this is received, the last command sent was PORT, then the original command (gszDataCommand) is sent. Figure 6.13 shows the code in DSSocket1_Receive that handles this precise moment.
Figure 6.13 After receiving a PORT command, the server sends a 200 reply, at which point you send the original commands such as RETR or STOR.
Case 200 '-- Command okay.
'-- What was the last command sent?
Select Case gszLastCmdSent
Case "TYPE" '-- Type toggles between Binary and Ascii
' modes too.
'-- Set the binary mode flag accordingly
If InStr(UCase$(ReceiveData), "SET TO A") > 0 Then
gnBinaryMode = False
ElseIf InStr(UCase$(ReceiveData), "SET TO I") > 0 Then
gnBinaryMode = True
End If
gnLastMode = gnBinaryMode
If Len(gszDataCommand) Then
SendFTPCommand gszDataCommand, gszFileName, gnBinaryMode
End If
Case "PORT"
'-- Send the actual command that the PORT command
' was sent to prepare for.
DisplayMessage "Sending command: " & gszDataCommand
SendData dsSocket1, gszDataCommand & gszCRLF
End Select
If the last command was PORT, that means that the data connection is made (or is in the process of being made) and the server is ready to accept the original command that required a data connection. At this time, the original command is sent, and the results are returned via the data connection. When the sending side of the connection has finished sending, it closes the connection.
Retrieving a Directory Listing
A directory listing involves the use of a display terminal (see Using a Display Terminal). All you need to do to get a directory is send the LIST command.
LIST takes a filespec parameter just like DOS’s DIR command. Here is an example that asks for a directory listing of all the ZIP files in /pub/files/new:
SendFTPCommand "LIST /pub/files/new/*.zip", False
Unix Wildcards
FTP uses Unix wildcards in filespecs. Here are a few of the most commonly used Unix wildcards:
? matches a single character. Unlike DOS, "hell?" will match "hell"
but not "hello".
* matches any sequence (including a period).
[xxx] where xxx is a collection of letters or a range of letters (like A–Z).
"hello.[A-Za-z]" matches "hello.z" but not "hello.9".
The LIST command requires the use of the data connection. The data connection must be created on the fly before sending the actual LIST command or any other command that requires it (see Creating a Data Connection).
When you call SendFTPCommand with LIST as the command, first a data connection is made using either PORT or PASV, depending on gnPassiveMode. Once the data connection is made, then the LIST command gets sent. The server immediately sends the directory information and the DSSocket2_Receive event fires upon receiving the data. Figure 6.14 shows the code in this event.
Figure 6.14 Receiving data via the data connection.
Sub dsSocket2_Receive(Index As Integer, ReceiveData As String)
WriteLogFile "RD: " & Str$(Len(ReceiveData)) _
& " bytes. First 10 = " & Left$(ReceiveData, 10)
'-- What was the last command sent?
Select Case UCase$(Left$(gszDataCommand, 4))
Case "RETR" '-- The retrieve command is the same as a
' Download command, the purpose is to
' retrieve a file. If we are here then
' we are receiving file data.
'-- Is the file not open?
If gnFileNum = 0 Then
'-- Was a file specified?
If Len(gszFileName) Then
'-- Yes. Open the file in binary mode (always)
gnFileNum = FreeFile
Open gszFileName For Binary As gnFileNum
End If
End If
'-- If the file is open, write the data.
If gnFileNum Then
Put gnFileNum, , ReceiveData
End If
Case "LIST"
'-- This is a line of a directory listing. If you wish to parse
' it to determine the properties of the files, you should do
' so here, but be warned... The format of this listing may
' change from server to server.
DisplayMessage ReceiveData
Case Else
'-- We are not retrieving a file, so display the
' received data. Of course, you can modify this
' logic to also display retrieved file data (if
' in text mode, or something like that) or to save
' all received data to a file.
DisplayMessage ReceiveData
End Select
'-- Update the number of bytes received.
glBytesReceived = glBytesReceived + Len(ReceiveData)
End Sub
Look at the statement "Case LIST." This is where the code arrives after sending the LIST command. By default the code simply calls DisplayMessage, which displays the text in either a list box or a text box, depending on which you want to use. The demo program uses a list box.
Uploading a File
When you call the SendFile Routine (shown in Figure 6.15) the first thing that happens is the STOR command is sent with SendFTPCommand. The data connection is created using either the PASV command or the PORT command, and the STOR (or STOU) command string is temporarily stored in the gszDataCommand variable. Now, the server sends either a 125 or a 150 code indicating that the transfer is starting. Figure 6.16 shows the code in the DSSocket1_Receive event that handles this precise moment.
Figure 6.15 The SendFile routine sends a file to the server.
Function SendFile(szSourceFile As String, szDestFile As String, _
szErrorMessage As String) As Integer
Dim nRetVal As Integer
SendFTPCommand "STOR " & szDestFile, szSourceFile, True
nRetVal = WaitForFileResponse()
If nRetVal = 226 Then
nRetVal = False
Else
szErrorMessage = gszErrMsg
End If
SendFile = nRetVal
End Function
Figure 6.16 The dsSocket2_Close event occurs when the server closes the data connection.
Sub dsSocket2_Close(Index As Integer, ErrorCode As Integer, ErrorDesc As String)
WriteLogFile "dsSocket2(" & Trim$(Str$(Index)) & ")_Close"
DisplayMessage "Connection Closed. Total bytes received = " _
& glBytesReceived & gszCRLF
'-- Close the file if its open
If gnFileNum Then
Close gnFileNum
gnFileNum = 0
End If
'-- You cannot unload dsSocket2(0)
If Index = 1 Then
Unload dsSocket2(Index)
End If
gszDataCommand = ""
glBytesReceived = -1
End Sub
Simply put, the file is opened in Binary mode and sent in chunks. The chunk size is set with the variable gnSendBlockSize, which you can set in the Form_Load event of frmFTP. The default is 32000. The basic rule is that there is more overhead in sending two blocks of n bytes than sending one block of 2n bytes. In other words, the bigger the buffer, the better. So, try to avoid bagging bugs by barking with big buffers. Once the file has been sent, the data connection is closed.
SendFile makes use of another handy routine, called WaitForFileResponse. This routine waits in a loop for the gnFileOK global integer variable to be set, and returns the value of gnFileOK. gnFileOK is set to any 200 completion code or error code. It is not set by interim codes such as 200, but is set only after a command has been completely carried out. Using WaitForFileResponse is an easy way to return control to your program after a command has been carried out. I use it in the FTPdemo project to disable the form temporarily until a command is complete. This prevents the user from stacking up commands on top of each other, which could yield unpredictable results.
Downloading a File
The RETR (Retrieve) command downloads (receives) a file from the FTP server. The file can be anywhere on the server (that’s available to you, of course) and you can specify a full server-side path with the filename.
Here is the syntax to download a file:
SendFTPCommand "RETR MYFILE.ZIP", True, "C:\FILES\MYFILE.ZIP"
When you send this command, just like with LIST, SendFTPCommand saves the RETR command to gszDataCommand and sends the PORT command (or the PASV command) to initiate a data connection. Once the data connection is made, the original command (RETR) is sent. The server responds positively with a 150 reply to indicate that it’s OK to start the transfer, or a 125 if the data connection is already open.
At this time you are ready to start receiving the file. Look at Figure 6.14 again, which shows the DSSocket2_Receive event. If the last command sent was RETR then you are receiving binary file data. The code opens the file if it is not open and writes the received data to the file.
When the server has finished sending the data, it closes the data connection. Figure 6.17 shows the DSSocket2_Close event that occurs when the server closes the data connection. gnFileNum, the VB file handle of the received file, is closed when the data connection is closed. Also, the data connection control DSSocket2(Index) is unloaded from memory. To clean up, gszDataCommand is zeroed and the number of bytes received is set to –1 to indicate an end of file to the code in DSSocket1_Receive.
Figure 6.17 The DSSocket2_Close event.
Sub dsSocket2_Close(Index As Integer, ErrorCode As Integer, ErrorDesc As String)
WriteLogFile "dsSocket2(" & Trim$(Str$(Index)) & ")_Close"
DisplayMessage "Connection Closed. Total bytes received = " & glBytesReceived & vbCrLf
'-- Close the file if its open
If gnFileNum Then
Close gnFileNum
gnFileNum = 0
End If
gszDataCommand = ""
glBytesReceived = -1
End Sub
Using the cfFTP Object
Now that you know how FTP works you can use the cfFTP object in your own projects without having to cut and paste my sample code into your project. Chapter 9 shows you how to drop this object into your own projects with just a few lines of code.
Epilogue
The FTP protocol isn’t difficult on paper, but it requires a sort of state-machine mentality to write in Visual Basic, hence all the gszLastCommand variables and what-not. In all honesty, because of this, the FTP code took me the longest to write. Fortunately for you, you can just drop this code in your project and be sending and receiving files in no time. Please be sure and stop by the Visual Basic Internet 4.0 Programming site (http://carl.franklins.net/vbip) for code updates and utilities.