.NET Framework 早期未內建完整 FTP 用戶端類別 (直到 .NET 2.0 的 FtpWebRequest,且功能有限)。許多專案需自行封裝或使用第三方元件。本文介紹一個經典的 FTP 元件架構 (原作者 Jerome Lacaille),並示範如何建構支援事件驅動、非同步操作、拖放下載的 WinForms FTP 用戶端。
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
public class FTPClient
{
// 事件定義
public event EventHandler Connected;
public event EventHandler LoggedIn;
public event EventHandler DirCompleted;
public event EventHandler FileDownloadCompleted;
public event EventHandler Error;
// 連線參數
public string Hostname { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public int Port { get; set; } = 21;
public bool PassiveMode { get; set; } = true;
// 內部狀態
private TcpClient _controlClient;
private StreamReader _controlReader;
private StreamWriter _controlWriter;
private readonly object _lock = new object();
// FTP 指令常數
private const string CMD_USER = "USER ";
private const string CMD_PASS = "PASS ";
private const string CMD_PASV = "PASV";
private const string CMD_LIST = "LIST";
private const string CMD_RETR = "RETR ";
private const string CMD_DELE = "DELE ";
private const string CMD_RNFR = "RNFR ";
private const string CMD_RNTO = "RNTO ";
private const string CMD_MKD = "MKD ";
private const string CMD_QUIT = "QUIT";
// 檔案清單快取
public FTPFileInfo[] Files { get; private set; }
public int FileCount => Files?.Length ?? 0;
// 連線流程
public void Connect()
{
Task.Run(() =>
{
try
{
_controlClient = new TcpClient(Hostname, Port);
_controlReader = new StreamReader(_controlClient.GetStream());
_controlWriter = new StreamWriter(_controlClient.GetStream()) { AutoFlush = true, NewLine = "\r\n" };
ReadResponse(); // 歡迎訊息
OnConnected(new FTPEventArgs { Message = "Connected" });
SendCommand(CMD_USER + Username);
ReadResponse();
SendCommand(CMD_PASS + Password);
string resp = ReadResponse();
if (resp.StartsWith("230"))
{
OnLoggedIn(new FTPEventArgs { Message = "Logged in" });
}
else
{
OnError(new FTPEventArgs { Message = "Login failed: " + resp });
}
}
catch (Exception ex)
{
OnError(new FTPEventArgs { Message = ex.Message, Exception = ex });
}
});
}
// 取得目錄列表
public void ListDirectory()
{
Task.Run(() =>
{
try
{
if (PassiveMode) EnterPassiveMode();
SendCommand(CMD_LIST);
string resp = ReadResponse();
if (!resp.StartsWith("150") && !resp.StartsWith("125"))
throw new IOException("Server refused LIST: " + resp);
// 讀取資料連線
using (TcpClient dataClient = PassiveMode ? _passiveClient : CreateActiveDataConnection())
using (StreamReader dataReader = new StreamReader(dataClient.GetStream()))
{
var files = new List();
string line;
while ((line = dataReader.ReadLine()) != null)
files.Add(ParseListLine(line));
Files = files.ToArray();
}
ReadResponse(); // 226 Transfer complete
OnDirCompleted(new FTPEventArgs { Message = "Directory listed", TotalBytes = 0 });
}
catch (Exception ex)
{
OnError(new FTPEventArgs { Message = ex.Message, Exception = ex });
}
});
}
// 下載檔案 (支援拖放)
public void DownloadFile(string remoteFileName)
{
Task.Run(() =>
{
try
{
if (PassiveMode) EnterPassiveMode();
SendCommand(CMD_RETR + remoteFileName);
string resp = ReadResponse();
if (!resp.StartsWith("150") && !resp.StartsWith("125"))
throw new IOException("Server refused RETR: " + resp);
string localPath = Path.Combine(Environment.CurrentDirectory, remoteFileName);
long totalBytes = 0;
DateTime start = DateTime.Now;
using (TcpClient dataClient = PassiveMode ? _passiveClient : CreateActiveDataConnection())
using (Stream dataStream = dataClient.GetStream())
using (FileStream fs = new FileStream(localPath, FileMode.Create, FileAccess.Write))
{
byte[] buffer = new byte[8192];
int read;
while ((read = dataStream.Read(buffer, 0, buffer.Length)) > 0)
{
fs.Write(buffer, 0, read);
totalBytes += read;
}
}
ReadResponse(); // 226
OnFileDownloadCompleted(new FTPEventArgs
{
Message = $"Downloaded {remoteFileName}",
TotalBytes = totalBytes,
TimeElapsed = (int)(DateTime.Now - start).TotalMilliseconds
});
}
catch (Exception ex)
{
OnError(new FTPEventArgs { Message = ex.Message, Exception = ex });
}
});
}
// 刪除、重命名、建立目錄... (同樣模式發送命令、讀取回應)
public void DeleteFile(string name) => SendSimpleCommand(CMD_DELE + name);
public void RenameFile(string from, string to) { SendSimpleCommand(CMD_RNFR + from); SendSimpleCommand(CMD_RNTO + to); }
public void CreateDirectory(string name) => SendSimpleCommand(CMD_MKD + name);
// 斷開連線
public void Disconnect()
{
SendCommand(CMD_QUIT);
_controlReader?.Close();
_controlWriter?.Close();
_controlClient?.Close();
}
// === 私有輔助方法 ===
private void SendCommand(string cmd) => _controlWriter.WriteLine(cmd);
private string ReadResponse() => _controlReader.ReadLine();
private void SendSimpleCommand(string cmd) { Task.Run(() => { SendCommand(cmd); ReadResponse(); }); }
private void EnterPassiveMode()
{
SendCommand(CMD_PASV);
string resp = ReadResponse(); // 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
// 解析 IP:Port 並建立 _passiveClient
}
private TcpClient CreateActiveDataConnection() => null; // 略
private FTPFileInfo ParseListLine(string line) => new FTPFileInfo(); // 略
// 事件觸發器
protected virtual void OnConnected(FTPEventArgs e) => Connected?.Invoke(this, e);
protected virtual void OnLoggedIn(FTPEventArgs e) => LoggedIn?.Invoke(this, e);
protected virtual void OnDirCompleted(FTPEventArgs e) => DirCompleted?.Invoke(this, e);
protected virtual void OnFileDownloadCompleted(FTPEventArgs e) => FileDownloadCompleted?.Invoke(this, e);
protected virtual void OnError(FTPEventArgs e) => Error?.Invoke(this, e);
}
public class FTPEventArgs : EventArgs
{
public string Message { get; set; }
public long TotalBytes { get; set; }
public int TimeElapsed { get; set; }
public Exception Exception { get; set; }
}
public class FTPFileInfo
{
public string Name { get; set; }
public long Size { get; set; }
public bool IsDirectory { get; set; }
public DateTime ModifiedTime { get; set; }
public string Permissions { get; set; }
}
public partial class FtpClientForm : Form
{
private readonly FTPClient _ftp = new FTPClient();
private string _currentFolder = "/";
public FtpClientForm()
{
InitializeComponent();
WireEvents();
}
private void WireEvents()
{
_ftp.Connected += (s, e) => { _ftp.ListDirectory(); };
_ftp.LoggedIn += (s, e) => { /* 已在 Connected 觸發 ListDirectory */ };
_ftp.DirCompleted += Ftp_DirCompleted;
_ftp.FileDownloadCompleted += Ftp_FileDownloadCompleted;
_ftp.Error += (s, e) => MessageBox.Show(e.Message, "FTP Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private void btnConnect_Click(object sender, EventArgs e)
{
_ftp.Hostname = txtHost.Text;
_ftp.Username = txtUser.Text;
_ftp.Password = txtPass.Text;
_ftp.Port = int.Parse(txtPort.Text);
_ftp.Connect();
}
private void Ftp_DirCompleted(object sender, FTPEventArgs e)
{
if (InvokeRequired) { Invoke(new Action(() => Ftp_DirCompleted(sender, e))); return; }
lvServer.BeginUpdate();
lvServer.Items.Clear();
imgServerSmall.Images.Clear();
// 父目錄
lvServer.Items.Add(new ListViewItem("..") { ImageIndex = 0 });
int imgIdx = 1;
foreach (var f in _ftp.Files)
{
if (f.IsDirectory)
{
var item = new ListViewItem(new[] { f.Name, "", f.ModifiedTime.ToString() }) { Tag = f };
imgServerSmall.Images.Add(GetFolderIcon());
item.ImageIndex = imgIdx++;
lvServer.Items.Add(item);
}
}
foreach (var f in _ftp.Files)
{
if (!f.IsDirectory)
{
var item = new ListViewItem(new[] { f.Name, f.Size.ToString(), f.ModifiedTime.ToString() }) { Tag = f };
imgServerSmall.Images.Add(GetFileIcon(f.Name));
item.ImageIndex = imgIdx++;
lvServer.Items.Add(item);
}
}
lvServer.EndUpdate();
Cursor = Cursors.Default;
}
// 拖放下載:Server ListView -> Local ListView
private void lvServer_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && lvServer.SelectedItems.Count > 0)
{
string files = string.Join("\n", lvServer.SelectedItems.Cast().Select(i => i.Text));
lvServer.DoDragDrop(files, DragDropEffects.Copy);
}
}
private void lvLocal_DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.Data.GetDataPresent(DataFormats.Text) ? DragDropEffects.Copy : DragDropEffects.None;
}
private void lvLocal_DragDrop(object sender, DragDropEventArgs e)
{
string data = e.Data.GetData(DataFormats.Text).ToString();
foreach (string file in data.Split('\n'))
_ftp.DownloadFile(file.Trim());
}
private void Ftp_FileDownloadCompleted(object sender, FTPEventArgs e)
{
if (InvokeRequired) { Invoke(new Action(() => Ftp_FileDownloadCompleted(sender, e))); return; }
RefreshLocalList();
txtLog.AppendText($"{e.Message} - {e.TotalBytes} bytes in {e.TimeElapsed/1000.0}s\r\n");
}
// 刪除、重命名、新增資料夾... 類似模式
}
| 模式 | 應用 | 優點 |
|---|---|---|
| 事件驅動 (EAP) | Connected, DirCompleted, FileDownloadCompleted | UI 執行緒安全更新,解耦網路邏輯與介面 |
| 非同步 Task.Run | 所有網路操作 | 不阻塞 UI,.NET 4.0+ 簡單易用 |
| 被動模式 (PASV) | 資料連線建立 | 穿透防火牆/NAT,現代標準 |
| 拖放 | DoDragDrop / DragEnter / DragDrop | 直覺式下載操作 |
| InvokeRequired | 事件處理中更新 UI 控制項 | 確保跨執行緒存取安全 |
SslStream 包裝控制連線與資料連線,支援 AUTH TLS 指令。REST 指令斷點續傳。LIST,解析更標準化。FtpServer 模擬器 (如 FluentFTP 測試專案) 進行自動化測試。雖然 .NET Core / 5+ 可直接使用 FluentFTP、SSH.NET (SFTP) 等成熟函式庫,但理解底層 FTP 協定流程、命令/回應模式、被動模式資料連線建立、事件驅動非同步架構,對於除錯、客製化擴充、移椋舊系統仍具高價值。本文提供的架構可作為輕量級專案的起點,或教學參考。