
简介这是一套基于C#开发的FTP服务器完整源码同时提供Web端与后台管理界面着力弥补目前C#实现的FTP服务器开源项目较少的缺憾。面向需要搭建自定义FTP服务、学习FTP协议实现或进行.NET二次开发的读者代码涵盖了文件管理、传输控制、权限分配、日志记录等核心功能。客户端访问方式灵活既支持IE浏览器和Windows资源管理器直接浏览也可通过ftp命令行操作还能对接CuteFTP等专业FTP工具。源码可以编译并安装为Windows系统服务后台运行更稳定同时内置特殊文件过滤等实用机制便于生产环境部署或实验教学。压缩包共包含148个文件主体是36个cs源码文件以及可还原工程的sln、csproj、resx等配置与资源文件另有20个resources资源、11个resx窗体资源、9个htm页面配合7个exe可执行程序、13个exception异常处理代码和若干dll库、log日志、ico图标等辅助内容整个压缩包约11.78MB。作者还随包提供了开发过程文档和更新记录便于按目录快速定位。目前该资源已有1461人浏览学习对希望获取C#服务端完整范例、减少重复开发的读者来说是一份具备参考价值的源码集合。1. 一份能跑的 C# FTP 服务器源码先看清它补的是哪个空档做 C# 后端或工业上位机的人大概率遇到过这个尴尬文档里写着文件服务用 FTP真去搜 FTP 服务器怎么搭建出来的全是 vsftpd、FileZilla Server 这类 C/C 闭源程序功能挺全可一旦要加权限过滤、嵌进自己程序、改协议细节就无从下手。这份纯代码的 C# 版 FTP 服务器源码补的正是这个空档Server 工程实现协议和文件操作Service 工程把它注册成 Windows 服务FTPClient.cs 可以直接当客户端库用web 端访问靠 IE 或资源管理器就能完成。适合三类人要内网文件服务又不想用黑匣子的 C# 开发、需要 FTP 模块接入后台管理系统的集成工程师、拿真实协议实现练手的新手。下面拆给你看。2. 拆工程结构Server 核心、Service 服务壳与 FTPClient 的分工2.1 先分清三个工程各扛什么活拿到源码先别看协议代码先按 csproj 把工程边界画出来。这套源码里能看到 Server.csproj、Service.csproj 和 FTPClient.cs 三个关键文件对应三层职责。Server.csproj 是 FTP 协议引擎活都在这——控制连接监听、命令解析、文件目录操作、权限校验、日志记录。它到底是类库还是主程序取决于 Server 目录下有没有 Program.cs 入口老式 .NET Framework 项目两种写法都存在。Service.csproj 是 Windows 服务的壳服务安装、启动、停止逻辑在 ServiceBase 派生类里。把协议引擎和服务壳拆成两个工程是 C# 服务端项目相当标准的做法协议逻辑可以脱离 Windows 服务单独测试服务壳坏了不影响引擎代码。FTPClient.cs 是客户端实现适合拿来当测试工具或者直接嵌进你的 C# 上位机程序让上位机既能当 FTP 服务端又能当客户端。对 C# 入门阶段的人来说这套结构本身就是分层参考协议、宿主、客户端三块职责边界比那种一个 Program.cs 写到底的教程代码清晰得多。改代码之前先划定改动范围——加过滤规则动 Server改部署方式动 Service做联调用 FTPClient这样三个工程互不踩脚。2.2 工程里那一串 .cache 文件是什么刚解压源码时会被 DesignTimeResolveAssemblyReferencesInput.cache、DesignTimeResolveAssemblyReferences.cache、Server.csprojResolveAssemblyReference.cache、Service.csprojResolveAssemblyReference.cache、Server.csproj.GenerateResource.Cache 这一串文件唬住。这些不是源码是 Visual Studio 编译期自动生成的中间文件。DesignTimeResolveAssemblyReferences 是 VS 设计时解析程序集引用留下的缓存ResolveAssemblyReference.cache 是编译时解析引用关系的缓存GenerateResource.Cache 是资源文件编译的副产品它们和 bin、obj 目录同属可再生物品。提示拿到任何 C# 源码第一件事是删掉所有 bin、obj 目录和 *.cache 文件再编译能避免换机器、换 VS 版本后出现一堆莫名其妙的引用报错。搜 FTP 源码时经常看到有人把 cache 文件一起打包上传这不算问题但你要知道它们可以安全删除不参与编译也不影响运行。真正要保护的只有 .cs、.csproj、配置文件这几类。2.3 FTP 协议在 C# 里的落点控制连接、数据连接与命令循环FTP 和 HTTP 最大的不同是它有两条连接控制连接走 21 端口传命令和应答数据连接用于传目录列表和文件内容分主动模式PORT和被动模式PASV。C# 里实现控制连接就是用 TcpListener 监听 21Accept 之后循环读一行、解析命令、回一行应答。核心循环大致是这样TcpListener listener new TcpListener(IPAddress.Any, 21); listener.Start(); CancellationToken token _stopToken; while (!token.IsCancellationRequested) { TcpClient tcp await listener.AcceptTcpClientAsync(); // 每个会话一个线程互不阻塞 _ Task.Run(() HandleSession(tcp)); }逻辑说明listener 绑定所有网卡的 21 端口Accept 到新连接后立刻丢给线程池处理主循环继续等下一个连接这样并发会话不会互相卡死。HandleSession 内部维护一个 FtpSession 对象里面存用户名、当前目录、数据连接模式等会话状态。private async Task HandleSession(TcpClient tcp) { using (tcp) using (var reader new StreamReader(tcp.GetStream(), Encoding.ASCII)) using (var writer new StreamWriter(tcp.GetStream(), Encoding.ASCII)) { writer.WriteLine(220 FTP Server Ready); writer.Flush(); while (!_stop tcp.Connected) { string line reader.ReadLine(); if (string.IsNullOrEmpty(line)) break; string cmd line.Split( )[0].ToUpperInvariant(); string arg line.Contains( ) ? line.Substring(line.IndexOf( ) 1) : ; string reply ExecuteCommand(cmd, arg, session); writer.WriteLine(reply); writer.Flush(); } } }参数说明控制通道必须用 ASCII 编码很多乱码问题就出在这——命令本身是 ASCII但文件名参数需要用 UTF-8 或 GBK后面避坑部分会展开。命令解析用第一空格切分命令统一大写参数保留原样这是 FTP 命令格式的基本约定。ExecuteCommand 是一长串 switch分别处理 USER、PASS、CWD、LIST、RETR、STOR 等二十多个命令。数据连接是第一次写 FTP 时最容易翻车的地方。被动模式的标准做法是服务端再开一个 TcpListener端口传 0 让系统分配TcpListener dataListener new TcpListener(IPAddress.Any, 0); dataListener.Start(); int dataPort ((IPEndPoint)dataListener.LocalEndpoint).Port; session.DataListener dataListener; // 227 应答格式IP 四段加端口高低位 string reply string.Format(227 Entering Passive Mode ({0},{1},{2},{3},{4},{5}), ip1, ip2, ip3, ip4, dataPort / 256, dataPort % 256);参数说明端口传 0 表示让操作系统随机分配空闲端口拿到实际端口后把 IP 四段和端口高低字节拼进 227 应答。客户端收到后主动连这个端口LIST 和 RETR 都在这条数据连接上完成。主动模式则反过来客户端先告诉服务端它的 IP 和端口服务端去连它在 NAT 环境下基本必死。最后提一个 C# 线程的坑FtpSession 同时被控制连接命令线程、数据连接传输线程读写上传进度计数、当前目录切换这些共享字段要加锁或用并发集合不然并发会话一多日志里会出现忽大忽小的怪数字。这一章看完你应该能回答一个问题把这个源码的协议引擎抽出来塞进自己项目当类库要剥离的是 Service 壳保留的是 Server 的会话管理和命令分发这正是这份资源最值钱的部分。3. 编译与部署把源码装成 Windows 服务三种客户端轮着验3.1 编译前清场缓存、目标框架、端口占用标题标了纯代码意味着下载下来是源码工程没有现成 exe 可跑第一步永远是编译。编译前把上一步说的 bin、obj、*.cache 全部清掉用 Visual Studio 打开 Server.csproj。老项目通常默认 .NET Framework 4.x如果你本机只装了 .NET 5/6 运行库会提示目标框架不可用——常见做法是在项目属性里把目标框架改成 4.8Windows 10/11 自带 4.8 运行库改动很小。如果 VS 提示整体升级 .NET Core 版建议第一次先不升选暂不迁移直接编译跑通再谈升级。不想开 VS 的话msbuild 也能编msbuild Server.csproj /p:ConfigurationRelease。编译产物在 bin\Release 下用 msbuild 时如果报 NU 开头的包错误检查 NuGet 源是否可达老项目还原依赖时需要联网。编译前顺手查一下端口netstat -ano | findstr :21如果有输出说明本机已经有别的 FTP 服务在监听——最常见的是 Windows 自带 IIS FTP 服务或者之前装过 FileZilla 没卸载干净。处理方式停掉对应服务或者把配置里的端口改成 2121 这类非常规端口。凡是改了端口客户端连接串就要跟着写端口比如 ftp://192.168.1.10:2121。3.2 把服务装进 WindowsInstallUtil 和 sc.exe 两条路这套源码自带 Service 工程意味着最终形态是 Windows 服务而不是控制台程序。.NET Framework 的老式服务推荐用 InstallUtil 安装它会往注册表写服务名、显示名、启动类型这些信息C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe C:\ftp-server\Service.exe提示64 位系统如果跑的是 x86 编译的服务用 Framework64 目录下的 InstallUtil别混用否则服务装上后可能起不来。装好之后用 sc 控制启停sc start CSharpFtp sc query CSharpFtp sc stop CSharpFtp参数说明sc start 后面跟的是服务名不是显示名如果 InstallUtil 安装时给服务命名的是别的名字先 sc query 查看已装服务列表确认。新手常犯的错是再用 sc create 创建一遍其实 InstallUtil 装过之后服务已存在sc create 反而会因重名报错。服务启动失败看事件查看器Windows 日志-应用程序里来源是服务名错误信息通常直接写着服务没有及时响应启动请求或端口绑定失败。启动失败多数是端口被占、配置路径不存在两类对照日志定位很快。服务跑起来之后顺手把配置里的端口、数据端口段、匿名登录开关、根目录这几项过一遍改完必须重启服务才生效。这条要记住很多人改完配置发现没变其实是忘了重启。3.3 三种客户端访问方式web 端、命令行和专用工具的差异这份源码支持三种客户端访问方式也对应三条调试路径先列成表客户端数据连接模式连接方式备注IE / Windows 资源管理器被动地址栏 ftp://用户名:密码IP资源管理器支持拖拽传文件IE 只浏览不能传ftp 命令主动默认命令行逐条输入防火墙环境下先敲 passive 切被动CuteFTP 等专用工具站点配置可选填 IP/端口/账号选 UTF-8功能最全适合做最终验收IE 和资源管理器就是 web 端的入口——地址栏输 ftp:// 开头的地址登录框弹出目录浏览和文件下载走的是服务端响应客户端不用装任何软件。这也是网管最常见的用法内网共享文件替代方案给每个部门开一个只读账号浏览器里就能查资料。ftp 命令行适合快速连通性测试脚本化也方便ftp 192.168.1.10 ftp user ftptest ftp pass ftp passive ftp ls ftp put d:\test.bin ftp quit说明Windows 的 ftp 命令默认发 PORT 主动模式在家用路由器或防火墙环境下会卡在 ls 不出数据先敲 passive 再操作这是最常用的救场操作。put 传二进制文件前先敲 binary 切换传输类型否则换行符会被转换导致文件损坏。CuteFTP 这类工具没这些麻烦站点管理器里把传输类型设二进制、编码设 UTF-8、被动模式打勾基本一次连通。另外资源附带的开发过程文档和更新记录值得先读一遍——更新记录能告诉你最后修过哪些 bug开发文档能补上代码里没写注释的设计意图。这是我搜 FTP 源码资源的习惯先读 changelog 再读代码。4. 功能落点权限、日志、特殊文件过滤在代码里的位置4.1 权限分配用户表、根目录与路径穿越防护配置用户建议用独立配置文件实现常见有 INI 键值对和 JSON 两种。这套源码的功能点包括文件管理、传输控制、权限分配代码里必然有一块用户到权限的映射。按最通用的设计每条用户记录带用户名、密码、根目录、权限位权限位对应操作说明RLIST / RETR列出目录、下载WSTOR上传、覆盖DDELE / RMD删除文件、目录MMKD / RNFR / RNTO建目录、重命名APWD / CWD切换目录、查看路径每个用户锁在自己的根目录里这是 FTP 安全的基本盘。路径拼接处要防目录穿越稳妥做法是先 Path.GetFullPath 归一化再比对前缀string baseDir Path.GetFullPath(user.HomeDir); string fullPath Path.GetFullPath(Path.Combine(baseDir, relativePath)); if (!fullPath.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase)) return 550 Path is outside home directory.; // 路径不存在直接拒绝避免暴露目录结构 if (!Directory.Exists(fullPath) !File.Exists(fullPath)) return 550 No such file or directory.;参数说明relativePath 是客户端提交的相对路径绝不能直接把客户端字符串拼进路径。Path.GetFullPath 会把 ../ 这类相对段归一化成绝对路径再做前缀比较这是最廉价也最有效的防穿越手段。注意比较要用 OrdinalIgnoreCase因为 Windows 文件系统大小写不敏感如果移植到 Linux 上的 Mono 跑这行要改成 StringComparison.Ordinal这是移植时容易漏掉的边界差异。4.2 日志记录从登录到传输建一个不丢行的审计口日志这个功能容易被当成写个 txt 完事但放进 FTP 服务端就有讲究谁在什么时候登录、传了什么文件、传了多大、成功还是失败这些是出问题时唯一能翻的账。C# 里最简单可靠的做法是追加写文件public void WriteAudit(string user, string action, string file, long bytes, bool ok) { string line string.Format({0:yyyy-MM-dd HH:mm:ss}|{1}|{2}|{3}|{4}|{5}, DateTime.Now, user, action, file, bytes, ok ? OK : FAIL); File.AppendAllText(_logPath, line Environment.NewLine, Encoding.UTF8); }逻辑说明竖线分隔是为了避免路径里的空格影响解析用 UTF-8 写入是为了中文路径不花屏。File.AppendAllText 每次都打开-写入-关闭性能不理想但换来日志不丢行——服务崩溃时最后一条记录已经在磁盘上。流量大了再用 StreamWriter 加自动 flush 替代。日志里至少要有四个埋点登录成功/失败、文件上传完成、文件下载完成、权限拒绝。这四类事件能定位百分之九十的故障尤其是甲方的文件传不上来这种反馈日志能直接告诉你他是没权限还是根本没连上。有 C# 高级编程经验的人可以顺手把日志模块改成事件委托挂多个消费者——一个写文件、一个写 EventLog这套结构不用动协议代码。4.3 特殊文件过滤上传下载前先过扩展名这一关摘要里提到的特殊文件过滤是这个源码的鲜明卖点。典型场景是内网文件服务器禁止散播可执行文件或者对特定类型文档做只读保护。实现位置在 ExecuteCommand 的 STOR 和 RETR 分支传输动作发生之前先拦截private static readonly HashSetstring BlockedExt new HashSetstring( new[] { .exe, .bat, .cmd, .ps1, .dll, .tmp, .log }, StringComparer.OrdinalIgnoreCase); private bool IsBlocked(string fullPath) { string ext Path.GetExtension(fullPath); if (string.IsNullOrEmpty(ext)) return false; return BlockedExt.Contains(ext); }参数说明HashSet 加 OrdinalIgnoreCase 是为了匹配不区分大小写Windows 文件系统根本不区分 .EXE 和 .exe不这样写的话大小写稍微一变过滤就失效。被拦截时返回 550 Operation not permitted.客户端会明确看到拒绝原因而不是默默失败。这里有个容易被忽略的点过滤不仅要管 STOR上传还要管 RETR下载——很多单位的诉求是服务器上已有的 exe 也不许被拉走拦截方向做反功能就白做了。更完整的设计可以加文件大小上限、按用户名过滤、按目录过滤把 BlockedExt 从硬编码改成配置文件这部分放到最后一章说。5. 避坑指南数据连接、中文乱码、服务自停等五类真实故障5.1 现象客户端登录成功ls 却永远转圈登录没问题说明控制连接是通的ls 转圈说明数据连接没建成。数据连接建不起来的头号原因是主动/被动模式不匹配服务端等客户端连它客户端却在等服务端连自己两边互等直到超时。其次是服务端被动模式的数据端口段没在防火墙放行Windows 防火墙默认只放行 21 端口数据连接用随机端口直接被拦。解决固定服务端数据端口段常见区间 50000-50100防火墙高级设置里新增入站规则放行整个区间客户端站点配置强制被动模式。这是 FTP 调试里最经典的翻车点没有之一。还有一个隐蔽场景服务器有多个网卡时227 应答里返回的 IP 是绑定监听时的本地 IP客户端和服务器跨网段时拿这个 IP 连数据端口会失败。常见做法是在配置里固定一个对外可达 IP或者按客户端来源网段选择应答 IP。这类问题日志里通常看不到错误只能抓包确认 227 应答内容。5.2 现象中文文件名在 CuteFTP 里正常ftp 命令里乱码原因几乎可以断定在编码协商上。FTP 协议本身没有强制规定文件名编码老服务端用 ANSIWindows 下就是 GBK现代客户端默认 UTF-8。CuteFTP 正常是因为它会话开始时发 FEAT 和 OPTS UTF8 ON服务端没响应 UTF8 时它回退成本地编码恰好和 GBK 对上ftp 命令则直接按系统 ANSI 解析。解决在命令循环里处理 FEAT返回 211 列表时声明 UTF8 支持收到 OPTS UTF8 ON 后把文件名解析切到 UTF-8。对应补丁大概是这个样子case FEAT: return 211-Features\r\n UTF8\r\n211 End; case OPTS: if (arg.ToUpperInvariant().Contains(UTF8)) { session.UseUtf8 true; return 200 UTF8 mode enabled; } return 501 Option not supported;注意切编码要在命令解析时全局生效别只改 RETR 分支不然 LIST 正常、RETR 又乱。检查办法ls 看到的中文正常之后再传一个中文名文件下载回来验证两边都过才算真修好。5.3 现象服务装上后 start 一下就 stop事件查看器里有来源自启动即自停九成是三个原因之一。一是端口被占用服务启动时 bind 21 失败OnStart 抛异常二是程序集路径不对InstallUtil 装的注册表指向已不存在的 Service.exe多发生在移动过安装目录之后三是服务账号权限不够默认 LocalSystem 一般没事如果你指定了特定账号而它对文件根目录没有读权限服务能启动但所有会话全拒。解决顺序先看事件查看器具体报错再 netstat 查端口最后右键服务属性-登录改回 LocalSystem 试一次。记住一个习惯改服务前先 sc stop别在运行中直接覆盖 exeWindows 会锁文件覆盖后服务重启就会报不能加载程序集。5.4 现象大文件传到一半断线日志记 426426 是连接关闭传输中止。常见原因数据连接的 Socket 超时设短了或者中间交换机对空闲 TCP 会话有老化回收还有一种情况是客户端在主动模式下 NAT 会话超时把数据连接回收了。解决调大 socket 的 ReceiveTimeout 和 SendTimeout建议至少十分钟以上大文件传输期间服务端不要给数据连接加 idle 超时控制连接倒是可以设五分钟无命令超时并主动发 421 提示。代码上传输循环里对 SocketException 要区分捕获——超时可重试、对方关闭就结束会话别把所有异常都当致命错误直接崩线程。还有个细节传输循环用 Stream.CopyTo 是最省事的写法但记得手动刷缓冲区不然最后一段数据会丢。5.5 现象有人把源码拷到 Ubuntu 上跑外面连不上源码是 C# 加 Windows 服务组合拿到 Ubuntu 上靠 Mono 能编译出一部分但 Windows 服务壳跑不了监听和权限模型也不适配 Linux。如果你搜到 ubuntu 部署 ftp 服务器的需求正路是 apt install vsftpd 然后改 /etc/vsftpd.conf而不是硬搬这套 C# 服务。反过来如果你就是想要 C# 实现那就留在 Windows 上用 InstallUtil 装服务别跟跨平台较劲。这是最典型的方向选错——判断依据很简单看源码里有没有 ServiceBase、Environment.UserInteractive 这类 Windows 专属类型有就果断走 Windows 部署路线。方向错了代码本身没毛病也要背锅。6. 二次开发验证用 FTPClient 拉通链路把过滤规则外置成配置6.1 三步自检编译、装服务、FTPClient 联调拿到源码跑通只是开始改代码之前先建立一条自检链路。把 FTPClient.cs 编译进一个控制台工程写一个最小验证脚本每次改动后跑一遍using (var client new FTPClient()) { client.Connect(127.0.0.1, 21); client.Login(ftptest, pass123); var items client.List(/); Console.WriteLine($目录项: {items.Count}); client.Upload(d:\tmp\测试文件.bin, /测试文件.bin); client.Download(/测试文件.bin, d:\tmp\下载回来.bin); client.Login(guest, readonly); // 验证权限隔离 client.Quit(); }逻辑说明这一段同时验证了连接、登录、目录、上传、下载五条链路特意用中文文件名验证编码协商用 .bin 后缀验证二进制传输不受换行符转换影响。跑通之后再开防火墙用 CuteFTP 做一轮外部验收整个资源的可信度就立住了。我自己的习惯是把它做成一个批处理编译、装服务、启动、控制台测试、查日志一条命令走完改完代码一键回归。6.2 把过滤规则外置成配置让运维也能改BlockedExt 硬编码在类里每次改规则都要重新编译。最小的改动是把它挪到一个 JSON 配置里public class FilterConfig { public Liststring BlockedExtensions { get; set; } } FilterConfig cfg JsonSerializer.DeserializeFilterConfig( File.ReadAllText(filter.json)); public bool IsBlocked(string path) { string ext Path.GetExtension(path); return cfg.BlockedExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase); }参数说明配置文件放在 Service.exe 同目录属性里设成如果较新则复制以后运维加一个 .zip 后缀只需要改 JSON不用动代码。这是把特殊文件过滤做成可持续功能的最小改动五分钟能完成。从那以后我拿到任何一份陌生源码都强制先走清缓存、编译、装服务、三种客户端各连一次、传一个中文名大文件这五步流程这五步过不了就说明环境或源码有问题绝不带着疑问改代码。这份 FTP 源码更是如此——协议这东西玄学少骗过你的基本都是环境。希望帮到你。本文还有配套的精品资源点击获取