编写一个简单的FTP客户机程序,要求能够向FTP服务器发送命令,并接收FTP服务器返回的响应与数据。程序设计的具体要求如下:
1)要求实现的程序为图形化界面(如图下所示),可以输入FTP服务的相关信息(包括IP地址、用户名与密码),输出交互过程中的FTP命令与响应信息,以及从FTP服务器的根目录获得的文件(或目录)列表。
2)要求遵循RFC959的相关FTP协议规定。只要求实现USER、PASS、PASV、LIST、RETR和QUIT命令。点击Connect按钮,实现USER与PASS命令;点击List按钮,实现PASV 与LIST命令;点击Download按钮,实现PASV 与RETR命令;点击Quit按钮,实现QUIT命令。
3)支持IP地址、域名输入,及合法性检测,显示FTP登录过程,下载速率等。
4)支持多线程下载,实现线程管理,显示各线程的状态。报告各种异常,提示产生异常的原因。
5)要求有良好的编程规范与注释信息。
6)要求有详细的说明文档,包括程序的设计思想、工作流程、关键问题等。
7)要求在Windows操作系统环境中,建议使用Visual C++编程工具实现。
一.详细设计
我之所以选择使用wininet的接口,是因为用它来开发的话会比较简单和快速。
(1)界面布局
(2)为Connect,List,Download,Upload,Quit,<-等button添加BN_CLICKED事件以及编写事件函数
(3)代码的编写集中CFtpClientDlg类里,下面就详细介绍一下这个类的代码:
1.几个常量:
//存储所有文件类型
const char fielType[8][9] = {"归档文件","文件夹","隐藏文件","普通文件","只读文件","系统文件","临时文件","其他文件"};
const int BUF_SIZE = 4096 ;
const int MSG_SIZE = 1024 ;
2.CFTPClientDlg的变量
CFont edit_font;
CInternetSession m_InternetSession; //定义一个会话对象
CFtpConnection *m_pFtpConnection; //连接对象指针
HANDLE m_hEventKill; //事件句柄
CString m_sFTPServerCurFolder; //FTP服务器当前目录
CString m_sFTPServerParentFolder; //FTP服务器当前目录的父目录
DWORD dwLastErrorMsg; //应答码
DWORD dwErrorMsgSize; //应答信息长度
char buf[MSG_SIZE]; //应答信息
CString strServer; //服务器地址
CString strRemoteFile; //选中的远程文件地址
CString strLocalFile; //存储在本地的文件地址
int nItem; //选中项的索引
int selectedFileType; //选中文件的类型
long fileLen; //选中文件的长度
long downloadSize; //当前下载量
bool isSelected; //是否选中
3.CFTPClientDlg的构造函数
CFTPClientDlg::CFTPClientDlg(CWnd* pParent /*=NULL*/)
: CDialog(CFTPClientDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CFTPClientDlg)
m_Address = _T("");
m_Password = _T("");
m_User = _T("");
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
edit_font.CreateFont(
18, //字体大小
0,0,0,FW_NORMAL,
FALSE,FALSE,
0,
ANSI_CHARSET, // nCharSet
OUT_DEFAULT_PRECIS, // nOutPrecision
CLIP_DEFAULT_PRECIS, // nClipPrecision
DEFAULT_QUALITY, // nQuality
DEFAULT_PITCH | FF_SWISS, "Arial");
m_pFtpConnection = NULL;
m_hEventKill = CreateEvent (NULL, TRUE, FALSE, NULL);
m_sFTPServerCurFolder = "/";
m_sFTPServerParentFolder = "/";
dwLastErrorMsg = 0;
dwErrorMsgSize = MSG_SIZE;
strServer = "";
strRemoteFile = "";
strLocalFile = "";
nItem = 0;
selectedFileType = 0;
fileLen = 0;
downloadSize = 0;
isSelected = FALSE;
// Set the timeout value to 10 seconds
m_InternetSession.SetOption (INTERNET_OPTION_CONNECT_TIMEOUT, 10000);
m_InternetSession.SetOption (INTERNET_OPTION_RECEIVE_TIMEOUT, 10000);
m_InternetSession.SetOption (INTERNET_OPTION_SEND_TIMEOUT, 10000);
}
4.CFTPClientDlg的地址解析函数
bool CFTPClientDlg::parsrAddress()
{
if(m_Address.IsEmpty())
{
AfxMessageBox("没有给出主机地址,请输入一个主机地址!");
return FALSE;
}
else
{
m_Interact_Info.AddString("状态: 正在解析"+m_Address+"的地址...");
DWORD dwServiceType;
CString strObject;
INTERNET_PORT nPort;
AfxParseURL(m_Address,dwServiceType,strServer,strObject,nPort);
if (strServer.IsEmpty())
strServer = m_Address;
hostent* phe = gethostbyname(strServer);
struct in_addr addr;
if(strServer.IsEmpty() && NULL == phe)
m_Interact_Info.AddString("错误: 非法地址");
else
{
addr.s_addr = *(u_long *) phe->h_addr_list[0];
char* ip = inet_ntoa(addr);
CString temp = "状态: 正在连接 ";
temp += ip;
temp += "...";
m_Interact_Info.AddString(temp);
}
phe = NULL;
return TRUE;
}
}
5.判断是否连接到FTP服务器
bool CFTPClientDlg::IsInitialized()
{
if (m_pFtpConnection != NULL)
return true;
else
return false;
}
6.获取服务器的响应信息,不过这个函数并不是经常奏效,我也没搞明白为什么有时候获取不到响信息
void CFTPClientDlg::GetResponseInfo()
{
::InternetGetLastResponseInfo(&dwLastErrorMsg,buf,&dwErrorMsgSize);
char info[100];
int i=0,j=0;
for( i =0;i<MSG_SIZE;i++)
{
if('\0' == buf[i] )
break;
if ('\n' != buf[i])
{
info[j] = buf[i];
j++;
}
else
{
info[j] = '\0';
m_Interact_Info.AddString(CString("响应: ") +info);
j=0;
}
}
}
7.登陆函数
bool CFTPClientDlg::Login (const CString &sHost, const CString &sUsername, const CString &sPassword, BOOL bPASV, int nPort, int nRetries, int nRetryDelay)
{
bool rr = false ;
int nRetryCount = 0 ;
if (m_pFtpConnection == NULL)
{
while (nRetryCount < nRetries)
{
//连接指定的FTP服务器
m_pFtpConnection = m_InternetSession.GetFtpConnection (sHost, sUsername, sPassword, nPort, bPASV);
if (m_pFtpConnection != NULL) //获取当前目录
{
m_Interact_Info.AddString("状态: 连接建立,等待欢迎消息...");
GetResponseInfo();
m_pFtpConnection->GetCurrentDirectory (m_sFTPServerCurFolder);
GetResponseInfo();
break;
}
m_Interact_Info.AddString("错误: 时间超时!");
nRetryCount ++;
}
}
rr = (nRetryCount < nRetries);
return rr ;
}
8.退出服务器函数
void CFTPClientDlg::Logoff()
{
if (m_pFtpConnection != NULL)
{
m_pFtpConnection->Close ();
GetResponseInfo();
delete m_pFtpConnection;
m_pFtpConnection = NULL;
m_sFTPServerCurFolder.Empty ();
m_Interact_Info.AddString("状态: 退出服务器!");
m_Interact_Info.SetTopIndex(m_Interact_Info.GetCount()-5);//将滚动条下拉到显示ListBox的最新内容
m_Dir_Info.DeleteAllItems();
}
}
9.设置FTP服务器的当前目录
bool CFTPClientDlg::ChangeCurrentDir(const CString &sRemoteDir)
{
bool rr = false ;
if (m_pFtpConnection != NULL)
{
m_pFtpConnection->SetCurrentDirectory(sRemoteDir);
GetResponseInfo();
rr = true ;
}
return rr ;
}
10.下载函数
bool CFTPClientDlg::Download (const CString &sRemoteFile, const CString &sLocalFile, bool pbAbort,DWORD dwTransferType)
{
bool rr = false ;
downloadSize = 0;
if (m_pFtpConnection != NULL)
{
CString sSource (sRemoteFile);
CString sDestPath (sLocalFile);
CFile file;
// 打开本地文件
if (file.Open(sDestPath, CFile::modeCreate | CFile::modeWrite, NULL))
{
//打开远程资源文件
CInternetFile* pInternetFile = m_pFtpConnection->OpenFile (sSource, GENERIC_READ, dwTransferType);
GetResponseInfo();
if (pInternetFile)
{
char buffer[BUF_SIZE];
unsigned int nRead = BUF_SIZE;
char temp[20];
//读取文件
while ( (nRead == BUF_SIZE) && (WaitForSingleObject(m_hEventKill, 0) == WAIT_TIMEOUT) && (!pbAbort) )
{
// read remote data into buffer
nRead = pInternetFile->Read (buffer, BUF_SIZE);
downloadSize += nRead ;//更新下载量
m_Dir_Info.SetItemText(nItem,3,ltoa(downloadSize,temp,10));//在列表控件里更新显示
this->UpdateWindow();
// write buffer to data file
file.Write (buffer, nRead);
}
GetResponseInfo();
//关闭远程资源文件
pInternetFile->Close ();
delete pInternetFile;
rr = true ;
}
// 关闭本地文件
file.Close ();
}
}
return rr ;
}
11.上传函数
bool CFTPClientDlg::Upload (const CString &sLocalFile, bool pbAbort, DWORD dwTransferType)
{
bool rr = false ;
if (m_pFtpConnection != NULL)
{
CFile file;
//打开本地文件
if (file.Open (sLocalFile, CFile::modeRead, NULL))
{
CInternetFile* pInternetFile = m_pFtpConnection->OpenFile (file.GetFileName (), GENERIC_WRITE);
GetResponseInfo();
if (pInternetFile)
{
//定义读写缓存
char buffer [BUF_SIZE];
unsigned int nRead = BUF_SIZE;
//读写文件
while ( nRead == BUF_SIZE && (WaitForSingleObject(m_hEventKill, 0) == WAIT_TIMEOUT) &&(!pbAbort) )
{
// read data into buffer
nRead = file.Read (buffer, BUF_SIZE);
// write buffer to remote data file
pInternetFile->Write (buffer, nRead);
}
GetResponseInfo();
//关闭远程资源文件
pInternetFile->Close();
delete pInternetFile;
rr = true ;
}
// 关闭本地文件
file.Close();
}
}
return rr ;
}
12.查找FTP服务器当前目录下的所有文件
bool CFTPClientDlg::GetFileList (const CString &sCurrentDir)
{
bool rr = false ;
CString sFileName;
char buf[20];
CString len;
int type;
if (m_pFtpConnection != NULL)
{
if (m_Dir_Info.GetItemCount()>0)
m_Dir_Info.DeleteAllItems();
CFtpFileFind ftpFind (m_pFtpConnection); //创建CFtpFileFind对象
BOOL bContinue = ftpFind.FindFile (sCurrentDir); //查找FTP当前目录下的文件
GetResponseInfo();
if (!bContinue)
{
ftpFind.Close ();
m_Interact_Info.AddString("错误: 未能在服务器找到文件!");
return rr;
}
int i = 0;
// 将所检索到的文件追加到列表控件中
while (bContinue)
{
bContinue = ftpFind.FindNextFile ();
GetResponseInfo();
sFileName = ftpFind.GetFileName();
m_Dir_Info.InsertItem(i,sFileName);//文件名
GetTypeOfFile(type,ftpFind);
if(1 != type )
{
len = ltoa(ftpFind.GetLength(),buf,10);
len += " B";
}
else
len = "";
m_Dir_Info.SetItemText(i,1,len);//文件大小
m_Dir_Info.SetItemText(i,2,fielType[type]);//文件类型
m_Dir_Info.SetItemData(i,type);
i++;
rr = true ;
}
if (0 == i)
m_Dir_Info.SetItemText(0,0,"空文件夹");
ftpFind.Close ();
}
return rr ;
}
13.获取文件类型函数
void CFTPClientDlg::GetTypeOfFile(int &type,const CFtpFileFind &fileFind)
{
if (fileFind.IsArchived())
type = 0;
else if (fileFind.IsDirectory()||fileFind.IsDots())
type = 1;
else if (fileFind.IsHidden())
type = 2;
else if(fileFind.IsNormal())
type = 3;
else if(fileFind.IsReadOnly())
type = 4;
else if(fileFind.IsSystem())
type = 5;
else if(fileFind.IsTemporary())
type = 6;
else
type = 7;
}
14.初始化列表控件
void CFTPClientDlg::initListCtrl()
{
m_Dir_Info.InsertColumn(0,"文件名",LVCFMT_LEFT,80);
m_Dir_Info.InsertColumn(1,"文件大小",LVCFMT_LEFT,100);
m_Dir_Info.InsertColumn(2,"文件类型",LVCFMT_LEFT,80);
m_Dir_Info.InsertColumn(3,"下载进度",LVCFMT_LEFT,80);
m_Dir_Info.SetExtendedStyle(LVS_EX_FULLROWSELECT);
}
15.点击List按钮的响应函数
void CFTPClientDlg::OnButtonList()
{
// TODO: Add your control notification handler code here
if(isSelected)
{
UpdateCurrentDir();
m_Interact_Info.AddString("状态: 正在读取目录列表...");
if(GetFileList(m_sFTPServerCurFolder))
m_Interact_Info.AddString("状态: 读取目录列表成功");
else
m_Interact_Info.AddString("错误: 读取目录列表失败!");
isSelected = FALSE;
m_Interact_Info.SetTopIndex(m_Interact_Info.GetCount()-5);//将滚动条下拉到显示ListBox的最新内容
}
else
AfxMessageBox("请选择文件!");
}
16.更新服务器的当前目录
void CFTPClientDlg::UpdateCurrentDir()
{
if(1 == selectedFileType)//1代表文件夹
{
if ("/" == m_sFTPServerCurFolder.Right(1) )//如果当前目录以'/'结尾
m_sFTPServerCurFolder += strRemoteFile;
else
m_sFTPServerCurFolder += ("/" + strRemoteFile);
}
// AfxMessageBox(m_sFTPServerCurFolder);
ChangeCurrentDir(m_sFTPServerCurFolder);
}
17.点击Download按钮的响应函数
void CFTPClientDlg::OnButtonDownload()
{
// TODO: Add your control notification handler code here
if(isSelected)
{
CFileDialog saveDlg(FALSE,NULL,strRemoteFile,OFN_HIDEREADONLY |OFN_OVERWRITEPROMPT,NULL,this);
if( IDOK == saveDlg.DoModal() )
{
m_Interact_Info.AddString("状态: 开始下载 " + strRemoteFile + " ...");
if(1 != selectedFileType )
{
strLocalFile = saveDlg.GetPathName();
Download(strRemoteFile,strLocalFile);
m_Interact_Info.AddString("状态: 下载 " + strRemoteFile + "成功!");
}
else
{
AfxMessageBox("不支持下载文件夹!");
m_Interact_Info.AddString("错误: 下载 " + strRemoteFile + "失败!");
}
m_Interact_Info.SetTopIndex(m_Interact_Info.GetCount()-5);//将滚动条下拉到显示ListBox的最新内容
}
isSelected = FALSE;
}
else
AfxMessageBox("请选中文件");
}
18.点击目录信息ListCtrl的响应函数
void CFTPClientDlg::OnClickListDirInfo(NMHDR* pNMHDR, LRESULT* pResult)
{
// TODO: Add your control notification handler code here
isSelected = TRUE;//设置选中
//Retrieves the position of the first selected list view item in a list view control
POSITION pos = m_Dir_Info.GetFirstSelectedItemPosition();
//Retrieves the index of a list view item position, and the position of the next selected list view item for iterating.
nItem = m_Dir_Info.GetNextSelectedItem(pos);
//获取远程文件名字
strRemoteFile = m_Dir_Info.GetItemText(nItem,0);
//选中文件的长度
fileLen = atol(m_Dir_Info.GetItemText(nItem,1));
//获取选中文件的类型
selectedFileType = m_Dir_Info.GetItemData(nItem);
*pResult = 0;
}
19.点击Quit按钮的响应函数
void CFTPClientDlg::OnButtonQuit()
{
// TODO: Add your control notification handler code here
if(IsInitialized())
Logoff();
else
AfxMessageBox("请先登录服务器!");
}
20.点击Upload按钮的响应函数
void CFTPClientDlg::OnButtonUpload()
{
// TODO: Add your control notification handler code here
CFileDialog uploadDlg(TRUE,NULL,NULL,OFN_HIDEREADONLY |OFN_OVERWRITEPROMPT,NULL,this);
if(IDOK == uploadDlg.DoModal())
{
strLocalFile = uploadDlg.GetPathName();
Upload(strLocalFile);
m_Interact_Info.SetTopIndex(m_Interact_Info.GetCount()-5);//将滚动条下拉到显示ListBox的最新内容
}
}
21.点击<-按钮的响应函数
void CFTPClientDlg::OnButtonParent()
{
// TODO: Add your control notification handler code here
if("/" != m_sFTPServerCurFolder)
{
int index = m_sFTPServerCurFolder.ReverseFind('/');
if(-1 != index)
{
//截取字符串获得父目录
if(0 == index) index = 1;
m_sFTPServerParentFolder = m_sFTPServerCurFolder.Left(index);
m_sFTPServerCurFolder = m_sFTPServerParentFolder;
GetFileList(m_sFTPServerCurFolder);
}
}
}
二.运行结果截图
1.初始界面
2.在Address编辑框里输入“ftp.microsoft.com”,然后点击Connect按钮
3.点击List按钮
4.点击Download按钮
5.点击保存按钮,下载完成后
6.点击Quit按钮
三.小结
写这些代码花了两天半,由于个人水平有限,所以代码比较粗糙,希望读者见谅。在写代码过程中,关于FTP client的主要过程,我主要是参考msdn的这篇文章http://msdn.microsoft.com/en-us/library/hf9x9wb4(v=vs.71).aspx,也在msdn查了不少类的api,当然少不了在google查资料咯。这个小程序主要实现了连接ft服务器,列出目录列表,下载和上传文件,退出服务器这些比较基础的功能,但还有相当多的不足,例如没有使用 CInternetException类来处理异常,不能多线程下载和上传,界面不美观,不能有效地获取服务器响应信息,还有重命名,删除服务器的文件等功能没有实现。