TcpListener 監聽 → AcceptTcpClient() 取得 TcpClient → 讀取命令字串 → 解析 GET_FILE|path → NetworkStream 發送檔案(FileStream.CopyTo)。TcpClient.Connect → 發送命令 → NetworkStream.Read 循環接收 → 寫入本地檔案。
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
public class FileServer {
private readonly TcpListener _listener;
private readonly string _basePath; // 安全根目錄
public FileServer(int port, string basePath) {
_listener = new TcpListener(IPAddress.Any, port);
_basePath = Path.GetFullPath(basePath);
}
public async Task StartAsync(CancellationToken ct = default) {
_listener.Start();
Console.WriteLine($"Server listening on port {((IPEndPoint)_listener.LocalEndpoint).Port}");
while (!ct.IsCancellationRequested) {
var client = await _listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client); // Fire-and-forget,或用 SemaphoreSlim 限制並發
}
}
private async Task HandleClientAsync(TcpClient client) {
using (client)
using (var stream = client.GetStream())
using (var reader = new StreamReader(stream, leaveOpen: true))
using (var writer = new StreamWriter(stream, leaveOpen: true) { AutoFlush = true }) {
string cmdLine = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(cmdLine)) return;
if (cmdLine.StartsWith("GET_FILE|", StringComparison.OrdinalIgnoreCase)) {
string relPath = cmdLine.Substring(9).Trim();
string fullPath = Path.GetFullPath(Path.Combine(_basePath, relPath));
// 安全檢查:防止目錄遍歷
if (!fullPath.StartsWith(_basePath, StringComparison.OrdinalIgnoreCase)) {
await writer.WriteLineAsync("ERROR: Access denied");
return;
}
if (!File.Exists(fullPath)) {
await writer.WriteLineAsync("FILE_NOT_FOUND");
return;
}
await writer.WriteLineAsync("FILE_OK");
await writer.WriteLineAsync(new FileInfo(fullPath).Length.ToString()); // 檔案大小
await stream.FlushAsync();
using (var fs = File.OpenRead(fullPath)) {
await fs.CopyToAsync(stream);
}
Console.WriteLine($"Sent: {relPath}");
}
}
}
}
public class FileClient {
public async Task DownloadFileAsync(string serverIp, int port, string remotePath, string localPath) {
using var client = new TcpClient();
await client.ConnectAsync(serverIp, port);
using var stream = client.GetStream();
using var writer = new StreamWriter(stream, leaveOpen: true) { AutoFlush = true };
using var reader = new StreamReader(stream, leaveOpen: true);
await writer.WriteLineAsync($"GET_FILE|{remotePath}");
string status = await reader.ReadLineAsync();
if (status == "FILE_NOT_FOUND") throw new FileNotFoundException(remotePath);
if (status != "FILE_OK") throw new Exception($"Server error: {status}");
long fileSize = long.Parse(await reader.ReadLineAsync());
Directory.CreateDirectory(Path.GetDirectoryName(localPath));
using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None)) {
byte[] buffer = new byte[8192];
long totalRead = 0;
int read;
while (totalRead < fileSize && (read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
await fs.WriteAsync(buffer, 0, read);
totalRead += read;
// 進度回報...
}
}
Console.WriteLine($"Downloaded: {localPath} ({fileSize} bytes)");
}
}
| 決策點 | 方案 | 理由 |
|---|---|---|
| 協定格式 | 文字命令行 + 回應行 + 二進位檔案流 | 簡單、可除錯、Telnet 可測試 |
| 檔案大小傳輸 | Server 先送 FILE_OK\n{size}\n | Client 預知大小,可顯示進度、驗證完整性 |
| 安全性 | Path.GetFullPath + StartsWith(basePath) | 防止 ../../etc/passwd 類攻擊 |
| 大檔案支援 | CopyToAsync / 手動 ReadAsync/WriteAsync 迴圈 | 避免載入整個檔案到記憶體 |
| 並發 | AcceptTcpClientAsync + 非阻塞處理 | 單執行緒處理大量連線 |
GET_FILES|path1|path2|...,Server 依序傳送每檔(大小+內容),或壓縮打包後單檔傳送。CHECK_VERSION|file|lastModified,Server 比對回傳 NEED_UPDATE|size 或 UP_TO_DATE。PUT_FILE|path|size 命令,Server 接收流存檔;即成迷你 FTP。EXEC_CMD|{json},雙方約定 DTO,實現 RPC 風格呼叫。MOVE|piece|from|to,Server 廣播給對手,即時同步棋盤狀態。SslStream 包裝 NetworkStream,需憑證。PING/PONG,Client 重連續傳。TCP/IP 檔案傳輸是網路程式設計的入門經典。掌握 TcpListener/Client、NetworkStream、非同步模式、協定設計與安全防禦,即能靈活構建檔案同步、升級、RPC、遊戲同步等多樣系統。代碼結構清晰、異常處理完善,可直接作為專案基礎模組擴充。