首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> C# 入門

C# WinForms Socket 檔案傳輸:點對點、監聽埠、非同步收發完整實作

     編輯:C# 入門

前言

Socket 是網路程式設計基石。本文實作一個 **WinForms 檔案傳輸工具**,包含伺服端 (監聽埠、非同步接收多檔案) 與客戶端 (連線、傳送檔案、進度回報),使用 TcpListener/TcpClient、NetworkStream、async/await 現代模式,避免傳統 Begin/End 回呼地獄。

通訊協定設計

簡易二進位協定:檔案資訊頭 + 檔案內容


// 傳送端序列化
// 1. 檔名長度 (4 bytes, int, big-endian)
// 2. 檔名 (UTF-8 bytes)
// 3. 檔案大小 (8 bytes, long, big-endian)
// 4. 檔案內容 (bytes)

public static class FileTransferProtocol
{
    public static async Task SendFileAsync(Stream stream, string filePath, IProgress progress = null, CancellationToken ct = default)
    {
        var fileInfo = new FileInfo(filePath);
        byte[] nameBytes = Encoding.UTF8.GetBytes(fileInfo.Name);
        
        // 寫入標頭
        await stream.WriteAsync(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(nameBytes.Length)), 0, 4, ct);
        await stream.WriteAsync(nameBytes, 0, nameBytes.Length, ct);
        await stream.WriteAsync(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(fileInfo.Length)), 0, 8, ct);
        
        // 寫入內容
        const int bufferSize = 81920; // 80KB
        byte[] buffer = new byte[bufferSize];
        long totalSent = 0;
        using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan))
        {
            int read;
            while ((read = await fs.ReadAsync(buffer, 0, buffer.Length, ct)) > 0)
            {
                await stream.WriteAsync(buffer, 0, read, ct);
                totalSent += read;
                progress?.Report(totalSent);
            }
        }
    }

    public static async Task<(string FileName, long FileSize, Stream ContentStream)> ReceiveFileAsync(Stream stream, string saveDir, IProgress progress = null, CancellationToken ct = default)
    {
        // 讀標頭
        byte[] lenBuf = new byte[4];
        await ReadExactlyAsync(stream, lenBuf, 0, 4, ct);
        int nameLen = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(lenBuf, 0));

        byte[] nameBuf = new byte[nameLen];
        await ReadExactlyAsync(stream, nameBuf, 0, nameLen, ct);
        string fileName = Encoding.UTF8.GetString(nameBuf);

        byte[] sizeBuf = new byte[8];
        await ReadExactlyAsync(stream, sizeBuf, 0, 8, ct);
        long fileSize = IPAddress.NetworkToHostOrder(BitConverter.ToInt64(sizeBuf, 0));

        // 儲存檔案
        string savePath = Path.Combine(saveDir, fileName);
        Directory.CreateDirectory(saveDir);
        
        const int bufferSize = 81920;
        byte[] buffer = new byte[bufferSize];
        long totalRead = 0;
        using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, FileOptions.SequentialScan))
        {
            while (totalRead < fileSize)
            {
                int toRead = (int)Math.Min(buffer.Length, fileSize - totalRead);
                int read = await ReadExactlyAsync(stream, buffer, 0, toRead, ct);
                await fs.WriteAsync(buffer, 0, read, ct);
                totalRead += read;
                progress?.Report(totalRead);
            }
        }
        return (fileName, fileSize, null);
    }

    private static async Task ReadExactlyAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken ct)
    {
        int totalRead = 0;
        while (totalRead < count)
        {
            int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead, ct);
            if (read == 0) throw new EndOfStreamException("連線提前關閉");
            totalRead += read;
        }
        return totalRead;
    }
}

伺服端 (Server Form)


public partial class ServerForm : Form
{
    private TcpListener _listener;
    private CancellationTokenSource _cts;
    private const int Port = 8888;
    private string _saveDir = @"D:\ReceivedFiles";

    public ServerForm()
    {
        InitializeComponent();
        Control.CheckForIllegalCrossThreadCalls = false; // 示範用,實務用 Invoke/Progress
    }

    private async void btnStart_Click(object sender, EventArgs e)
    {
        _cts = new CancellationTokenSource();
        _listener = new TcpListener(IPAddress.Any, Port);
        _listener.Start();
        AppendLog($"伺服器啟動,監聽 {Port}...");

        try
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                TcpClient client = await _listener.AcceptTcpClientAsync().WithCancellation(_cts.Token);
                _ = HandleClientAsync(client, _cts.Token); // 不等待,並行處理多客戶端
            }
        }
        catch (OperationCanceledException) { }
        finally { _listener.Stop(); AppendLog("伺服器已停止"); }
    }

    private async Task HandleClientAsync(TcpClient client, CancellationToken ct)
    {
        string ip = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
        AppendLog($"客戶端連線: {ip}");
        try
        {
            using (client)
            using (var stream = client.GetStream())
            {
                while (!ct.IsCancellationRequested)
                {
                    var result = await FileTransferProtocol.ReceiveFileAsync(stream, _saveDir, 
                        progress: new Progress(p => UpdateProgress(ip, p)), ct);
                    AppendLog($"接收完成: {result.FileName} ({result.FileSize} bytes)");
                }
            }
        }
        catch (Exception ex) when (!(ex is OperationCanceledException))
        {
            AppendLog($"客戶端 {ip} 錯誤: {ex.Message}");
        }
        AppendLog($"客戶端斷開: {ip}");
    }

    private void btnStop_Click(object sender, EventArgs e) => _cts?.Cancel();

    private void AppendLog(string msg) => txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\r\n");
    private void UpdateProgress(string ip, long bytes) => /* 更新 UI 進度條 */;
}

客戶端 (Client Form)


public partial class ClientForm : Form
{
    private TcpClient _client;
    private NetworkStream _stream;
    private CancellationTokenSource _cts;

    private async void btnConnect_Click(object sender, EventArgs e)
    {
        _cts = new CancellationTokenSource();
        _client = new TcpClient();
        await _client.ConnectAsync(txtServerIP.Text, 8888).WithCancellation(_cts.Token);
        _stream = _client.GetStream();
        AppendLog("已連線至伺服器");
        btnConnect.Enabled = false;
        btnSend.Enabled = true;
    }

    private async void btnSend_Click(object sender, EventArgs e)
    {
        if (openFileDialog1.ShowDialog() != DialogResult.OK) return;
        string file = openFileDialog1.FileName;
        btnSend.Enabled = false;
        progressBar.Value = 0;

        try
        {
            var fi = new FileInfo(file);
            progressBar.Maximum = (int)Math.Min(fi.Length, int.MaxValue);
            await FileTransferProtocol.SendFileAsync(_stream, file, 
                progress: new Progress(p => progressBar.Value = (int)Math.Min(p, int.MaxValue)), 
                _cts.Token);
            AppendLog($"傳送完成: {file}");
        }
        catch (Exception ex)
        {
            AppendLog($"傳送失敗: {ex.Message}");
        }
        finally { btnSend.Enabled = true; }
    }

    private void btnDisconnect_Click(object sender, EventArgs e)
    {
        _cts?.Cancel();
        _client?.Close();
        btnConnect.Enabled = true;
        btnSend.Enabled = false;
    }
}

關鍵輔助類別


// CancellationToken 擴充:支援 AcceptTcpClientAsync/ConnectAsync 取消
public static class SocketExtensions
{
    public static async Task AcceptTcpClientAsync(this TcpListener listener, CancellationToken ct)
    {
        var acceptTask = listener.AcceptTcpClientAsync();
        var completed = await Task.WhenAny(acceptTask, Task.Delay(Timeout.Infinite, ct));
        if (completed == acceptTask) return acceptTask.Result;
        ct.ThrowIfCancellationRequested();
        return null; // unreachable
    }

    public static async Task ConnectAsync(this TcpClient client, string host, int port, CancellationToken ct)
    {
        var connectTask = client.ConnectAsync(host, port);
        var completed = await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct));
        if (completed == connectTask) await connectTask; // propagate exception
        else ct.ThrowIfCancellationRequested();
    }
}

// Task 延伸:WithCancellation (NET 6+ 內建)
public static class TaskExtensions
{
    public static async Task WithCancellation(this Task task, CancellationToken ct)
    {
        var tcs = new TaskCompletionSource();
        using (ct.Register(() => tcs.TrySetCanceled(ct)))
        {
            var completed = await Task.WhenAny(task, tcs.Task);
            if (completed == tcs.Task) ct.ThrowIfCancellationRequested();
            return await task;
        }
    }
}

進階功能擴展

  • 斷點續傳:標頭加入 FileOffset,客戶端 FileStream.Seek,伺服端 FileMode.Append。
  • 多檔案批次:協定加入 FileCount,迴圈收發。
  • 傳輸加密:使用 SslStream 包裝 NetworkStream。
  • 壓縮傳輸:GZipStream 或 BrotliStream 包裝檔案流。
  • NAT 穿透:STUN/TURN 或 UPnP 端口對映。

常見問題與除錯

現象可能原因解決
傳大檔案卡住/記憶體暴增一次性讀入 byte[]分塊讀寫 (80KB 緩衝區)、FileOptions.SequentialScan
中文檔名亂碼編碼不一致雙方統一 UTF-8,Encoding.UTF8.GetBytes
防火牆阻擋埠未開放Windows 防火牆新增輸入規則允許 TCP 8888
客戶端關閉伺服端報錯未處理 IOExceptiontry-catch 包裝、檢查 client.Connected

總結

現代 C# Socket 檔案傳輸核心在於:非同步流 (async/await)、分塊緩衝、自訂二進位協定、CancellationToken 取消控制。封裝 FileTransferProtocol 靜態類別分離協定邏輯,搭配 Progress<T> 回報 UI,程式碼清晰、可測試、易擴展。此架構可直接移植至 WPF、ASP.NET Core、.NET MAUI 等平台。

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