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;
}
}
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 進度條 */;
}
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 包裝檔案流。| 現象 | 可能原因 | 解決 |
|---|---|---|
| 傳大檔案卡住/記憶體暴增 | 一次性讀入 byte[] | 分塊讀寫 (80KB 緩衝區)、FileOptions.SequentialScan |
| 中文檔名亂碼 | 編碼不一致 | 雙方統一 UTF-8,Encoding.UTF8.GetBytes |
| 防火牆阻擋 | 埠未開放 | Windows 防火牆新增輸入規則允許 TCP 8888 |
| 客戶端關閉伺服端報錯 | 未處理 IOException | try-catch 包裝、檢查 client.Connected |
現代 C# Socket 檔案傳輸核心在於:非同步流 (async/await)、分塊緩衝、自訂二進位協定、CancellationToken 取消控制。封裝 FileTransferProtocol 靜態類別分離協定邏輯,搭配 Progress<T> 回報 UI,程式碼清晰、可測試、易擴展。此架構可直接移植至 WPF、ASP.NET Core、.NET MAUI 等平台。