Introduction
This article describes the usage of the CFTPConnection
class wrapped in a managed C++ assembly. The following functions are used and discussed:
- Sending / receiving files from FTP server.
- Enumerating directory contents.
- Sending server commands, i.e., NLST to the FTP server using
CFTPConnection::Command
method and parsing the server's response synchronously. The FTP data (20) and control (21) ports.
- Using these functions from a simple UI.
Body
Recently, I had a need to develop an FTP client as a sub-function of a large C# Windows Form application. I quickly discovered that FTP support was not part of the FCL. I decided that the simplest way to address my needs was to develop a wrapper around the CFTPConnection
class MFC WinInet wrapper (yes a wrapper of a wrapper -- :) ).
I started out by just creating a purely unmanaged C++ .dll and just exporting a couple of functions, and then using PInvoke to call the functions from C#. I found this to be both inelegant and largely infeasible when it came to enumerating directory contents and returning these contents to a C# application. The solution I finally settled on was to create a managed C++ assembly that would interact with CFTPConnection
and would package things up into nice managed classes for the C# clients.
Sending / Receiving files
Sending and receiving files is supported right out of the box by CFTPConnection
. My wrapper around these functions is thin at best. Here's a simple example of sending a file with the wrapper:
bSent = FTP_Wrapper.SendFTPFile(Mode, Convert.ToInt32(szFTPServerPort),
szFTPServerName, szFTPUserName,
szFTPUserPassword, szFTPFile,
szFTPDestFile,
ref szErrorMsg);
if (!bSent)
Console.WriteLine(szErrorMsg);
Enumerating Directory Contents
Getting the contents of an FTP directory is pretty straightforward using CFtpFileFind
. I wanted to use this class and return a collection of files and subfolders to my managed clients. One thing to watch out for when enumerating directory contents of a server is that you don't try to pull back an entire server's contents before showing them in your GUI. For a larger server, this reading of the entire directory structure could take quite a while. A better approach (and one that is shown in the sample application) is to just enumerate the root directory, show the results, and then when a user wants to go down into a subdirectory, enumerate that subdirectory.
Enter the DirectoryContents
class:
namespace FTP_Wrap
{
public __gc class DirectoryContents;
public __gc class DirectoryItem
{
public:
System::String* m_Name;
System::String* m_PathName;
int m_Type;
long m_Size;
bool Equals(System::Object* pObj);
};
public __gc class DirectoryContents
{
public:
DirectoryContents();
virtual ~DirectoryContents() {};
bool LoadList(int SourceType, System::String* pServerNm,
int ServerPort, System::String* pUserNm,
System::String* pPwd, System::String* pRemoteDir,
System::String* pMask, System::String*& pErrorMsg);
System::Collections::ArrayList* m_DirectoryList;
};
}
Using the DirectoryContents
class from C#:
static void Main(string[] args)
{
ShowDirContents("/");
}
static void ShowDirContents(String Dir)
{
FTP_Wrap.DirectoryContents dc = new DirectoryContents();
String ErrorMsg = "";
bool Loaded = dc.LoadList("localhost", 21, "USERID", "PASSWORD",
Dir, "*", ref ErrorMsg);
if (!Loaded)
{
Console.WriteLine(ErrorMsg);
return;
}
foreach (DirectoryItem item in dc.m_DirectoryList)
{
if (item.m_Type == 0)
{
Console.WriteLine("File: " + item.m_PathName + " Size: " + item.m_Size);
}
else
ShowDirContents(item.m_PathName);
}
}
CFTPConnection::Command
One additional challenge I faced in implementing my client functionality was that one of the FTP machine types that would typically be interfaced with did not support truly standard FTP. As a result, using CFTPFileFind
did not work with the server to enumerate directories. Trying to use CFTPFileFind
would return a bunch of garbage that was barely if at all interpretable as a list of files. I needed a better way to get the file lists from the server.
After digging around a little more, I noticed that there is a set of 'server' commands that can be sent to an FTP server using the Command
method of CFTPConnection
. The commands are sent on port 21 (the FTP control port) but the responses are sent back on port 20 (the data port). Luckily (or I'd have more code to deal with), CFTPConnection
handles these communications.
In order to get the directory contents using a server command, I issued a NLST command to the server and parsed the response.
Here's what happens at the TCP/IP level:
Send NLST through port 21 to the server:
Get response to NLST from server to my local port 20:
Code from directorycontents.cpp:
CString dir(pRemoteDir);
InternetFile* pFile = NULL;
System::String* pRes = NULL;
BOOL bChg = pFtpConn->SetCurrentDirectory(dir);
CString strDir;
pFtpConn->GetCurrentDirectory(strDir);
File = pFtpConn->Command(_T("NLST"), CFtpConnection::CmdRespRead,
FTP_TRANSFER_TYPE_BINARY);
ULONGLONG a = pFile->GetLength();
char readBuf[256];
unsigned int rd = 0;
System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
do
{
rd = pFile->Read(readBuf, 256);
if (rd > 0)
{
System::String* pStr = new System::String(readBuf);
pSb->Append(pStr, 0, rd);
}
} while (rd > 0);
pRes = pSb->ToString();
System::String* pDelim = "\r\n";
System::Text::RegularExpressions::Regex* pRegex =
new System::Text::RegularExpressions::Regex(pDelim);
System::String* parts[] = pRegex->Split(pRes);
for (int n = 0; n < parts->Length; n++)
{
String* pItemNm = parts[n];
if (pItemNm->Length == 0)
continue;
DirectoryItem* pItem = new DirectoryItem;
if (pItemNm->EndsWith("/"))
{
pItem->m_Type = 1;
String* pTemp = pItemNm->Substring(0, pItemNm->Length - 1);
int nNameBegin = pTemp->LastIndexOf("/");
pItem->m_Name = pTemp->Substring(nNameBegin + 1);
System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
pSb->Append(pRemoteDir);
pSb->Append(pItem->m_Name);
pSb->Append("/");
pItem->m_PathName = pSb->ToString();
}
else
{
pItem->m_Type = 0;
int nNameBegin = pItemNm->LastIndexOf("/");
pItem->m_Name = pItemNm->Substring(nNameBegin + 1);
System::Text::StringBuilder* pSb = new System::Text::StringBuilder();
pSb->Append(pRemoteDir);
pSb->Append(pItem->m_Name);
pItem->m_PathName = pSb->ToString();
}
pItem->m_Size = 0;
m_DirectoryList->Add(pItem);
}
The big thing to note is that the Command method returns a CInternetFile
. To get the actual response data, you just read that file till EOF.
The sample application
I've included a very simple stripped down sample application to show the functionality of the classes. In order to play with the DirectoryContents
enumerations, select the '...' button beside 'file to get' in the get file form, or the '...' button beside the 'FTP Destination File' in the send file form. Go ahead and step through these to get a better idea if I've left something out.
The sample app will save your FTP settings (port, server name, most recent file names, etc.) to the current user registry under the software key.
Conclusion
These classes solved what I was trying to do. I hope they can help someone here.
References
I used numerous resources for figuring out how to use the CFTPConnection
class. Plenty of good information on it is in MSDN. Also, I used the ethereal packet capture utility not only for this but for a lot of network troubleshooting. It can be downloaded from here.