首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# FTP 元件與簡易用戶端實作:從連線、目錄列表到拖放下載

     編輯:關於C語言

前言

.NET Framework 早期未內建完整 FTP 用戶端類別 (直到 .NET 2.0 的 FtpWebRequest,且功能有限)。許多專案需自行封裝或使用第三方元件。本文介紹一個經典的 FTP 元件架構 (原作者 Jerome Lacaille),並示範如何建構支援事件驅動、非同步操作、拖放下載的 WinForms FTP 用戶端。

核心元件架構:FTPClient

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; }
}

WinForms 用戶端整合:事件驅動 UI 更新

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, FileDownloadCompletedUI 執行緒安全更新,解耦網路邏輯與介面
非同步 Task.Run所有網路操作不阻塞 UI,.NET 4.0+ 簡單易用
被動模式 (PASV)資料連線建立穿透防火牆/NAT,現代標準
拖放DoDragDrop / DragEnter / DragDrop直覺式下載操作
InvokeRequired事件處理中更新 UI 控制項確保跨執行緒存取安全

實務補充建議

  • SSL/TLS (FTPS):使用 SslStream 包裝控制連線與資料連線,支援 AUTH TLS 指令。
  • 大檔案續傳:實作 REST 指令斷點續傳。
  • MLSD/MLST:現代 FTP 指令取代傳統 LIST,解析更標準化。
  • 錯誤碼處理:完整解析 FTP 回應碼 (1xx, 2xx, 3xx, 4xx, 5xx) 區分暫時/永久錯誤。
  • 單元測試:使用 FtpServer 模擬器 (如 FluentFTP 測試專案) 進行自動化測試。

結語

雖然 .NET Core / 5+ 可直接使用 FluentFTP、SSH.NET (SFTP) 等成熟函式庫,但理解底層 FTP 協定流程、命令/回應模式、被動模式資料連線建立、事件驅動非同步架構,對於除錯、客製化擴充、移椋舊系統仍具高價值。本文提供的架構可作為輕量級專案的起點,或教學參考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved