尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

手写C# FTP服务器:协议解析、权限控制与Windows服务化部署

发布时间:2026/9/24 21:14:39

资讯中心
01
ARTICLE

手写C# FTP服务器:协议解析、权限控制与Windows服务化部署

手写C# FTP服务器:协议解析、权限控制与Windows服务化部署
简介这份源码提供了一套基于C#实现的FTP服务器完整工程覆盖Web管理端与后台服务适合.NET开发人员、网络管理员及需要二次开发定制FTP服务的技术人员。它具备文件管理、传输控制、权限分配、日志记录等核心功能支持IE浏览器、Windows命令行ftp命令以及CuteFTP等专用客户端三种访问方式并且可以安装为Windows系统服务还带有特殊文件过滤等实用特性。压缩包共148个文件约11.78MB以cs源码文件为主辅以resources资源文件、resx界面资源、htm页面、exe可执行程序以及开发过程文档与更新记录结构清晰便于阅读。目前已有1250人学习使用适合希望深入理解FTP协议实现或快速搭建自有FTP服务的读者参考。1. 为什么还要自己写一个 C# 版 FTP 服务器做内网文件交换系统的时候我第一反应是找现成的 FTP 服务器软件。FileZilla Server 确实成熟但它是 C 写的想改协议细节、嵌进自己的 C# 上位机项目里处处是黑匣子。更麻烦的是我需要程序里直接调 FTP 服务的能力还要能控制权限、过滤文件、看日志甚至装成 Windows 服务开机自启。找了一圈基于 C# 开发且功能完整的 FTP 服务器源码网上真的不多。这个资源把这个问题给解了它是一套纯 C# 实现的 FTP 服务器自带管理后台和 WEB 端核心能力包括文件管理、传输控制、权限分配、日志记录能编译成 Windows 服务。对于想快速搭一个可二次开发的 FTP 服务器、或者正在学 C# 网络编程的人来说值得直接下载下来跑一遍。2. 解开源码包从缓存文件到核心模块2.1 先分清哪些是源码哪些是 VS 自动生成的噪音解压这个源码包第一眼看到DesignTimeResolveAssemblyReferencesInput.cache、Server.csprojResolveAssemblyReference.cache这类文件很容易被绕晕。这些.cache文件是 Visual Studio 在编译过程中自动生成的临时缓存用来记录程序集引用解析的结果不是源码的一部分可以手动删掉。真正需要关心的是三类文件Server.csproj、Service.csproj和FTPClient.cs。Server.csproj是 FTP 服务端的核心工程负责协议解析、文件传输、用户权限校验。Service.csproj是将服务端包装成 Windows 服务的工程方便用 Windows 服务管理器统一管理。FTPClient.cs则是一个测试用的客户端实现用来验证服务端功能也可以当作 C# 调用 FTP 协议的一个参考范例。我一般会把*.cache文件加进.gitignore避免在版本管理里反复出现冲突。这些文件删掉之后用 Visual Studio 打开Server.csproj重新生成一次就会自动重建不影响编译结果。2.2 服务端的模块划分与数据流从源码结构看服务端的逻辑大致分成四块。第一块是监听模块负责在指定端口默认 21上等待客户端连接每来一个连接就开一个会话线程。第二块是命令解析模块FTP 协议本质上是文本协议客户端发来的命令比如USER、PASS、LIST、RETR都是明文文本服务端按行读取并解析分支处理。第三块是文件系统模块负责把客户端请求映射到磁盘目录执行列目录、上传、下载、重命名、删除等操作。第四块是权限模块根据用户名判断可访问目录范围和读写权限。数据流向是这样客户端连接 21 端口建立控制连接用户登录后每执行一次传输操作就临时建立一个数据连接来传文件。所以 FTP 有两个通道控制连接管命令数据连接管文件这也是后面排查问题时的关键点。2.3 WEB 端和后台管理这个源码还带一个 WEB 端管理界面方便在浏览器里配置用户、查看日志、设置目录权限。实际上就是把服务端的管理接口暴露成 HTTP 请求WEB 页面通过 Ajax 调用这些接口。这让我想起来做后台管理系统的时候哪怕是一个内部工具也一定要做登录鉴权。这个源码里的管理后台如果部署到公网默认的 admin 密码必须第一时间改掉不然很容易被扫到然后直接接管。项目里把管理功能和 FTP 服务端放在同一套解决方案里方便统一编译。如果你的业务是「给客户部署独立 FTP 节点总公司在后台统一管」可以在这个架构上继续拆成两个独立进程WEB 后台和管理接口走单独的端口跟 FTP 服务端口隔离开。3. FTP 服务器核心实现监听、会话与命令处理的落地写法3.1 控制连接的监听与会话分发FTP 服务端的入口是监听 21 端口。C# 里最直接的做法是用TcpListener监听然后每接到一个客户端就交给一个后台线程去处理。这段代码是整个服务端的骨架。// FTP 服务端监听入口 public void Start() { _listener new TcpListener(IPAddress.Any, 21); _listener.Start(); _acceptThread new Thread(AcceptLoop) { IsBackground true }; _acceptThread.Start(); } private void AcceptLoop() { while (_isRunning) { try { TcpClient client _listener.AcceptTcpClient(); ThreadPool.QueueUserWorkItem(HandleClient, client); } catch (SocketException ex) { // 服务停止时 AcceptTcpClient 会抛异常属于正常退出路径 if (_isRunning) LogError(ex); } } }这段代码的逻辑是TcpListener绑定任意本机 IP 的 21 端口启动监听后进入死循环每收到一个连接请求就丢到线程池处理。用ThreadPool.QueueUserWorkItem而不是每个连接 new 一个 Thread是避免高并发时线程数爆炸。.IsBackground true保证服务停止时监听线程不会阻塞进程退出。参数上需要注意两点端口 21 是 FTP 默认端口如果你的机器上已经跑了 IIS FTP 或其他 FTP 服务这里会冲突需要改成 2121 这类自定义端口SocketException的捕获要保留对_isRunning的判断因为 Stop 的时候AcceptTcpClient会抛异常这是正常的停止流程不用记成错误日志。3.2 命令解析把文本协议变成可执行的分支逻辑FTP 协议的命令层非常简单所有命令都是一行一行的文本比如USER anonymous PASS testtest.com PWD LIST RETR readme.txt STOR upload.zip QUIT服务端的任务就是按行读取这些命令提取命令字和参数然后分发到对应的方法去执行。我用switch语句处理命令分支核心代码大概是这样。public void ProcessCommand(string line) { // 按第一个空格切分命令和参数 string[] parts line.Split(new char[] { }, 2); string cmd parts[0].ToUpperInvariant(); string arg parts.Length 1 ? parts[1] : null; switch (cmd) { case USER: HandleLogin(arg); // 用户名验证 break; case PASS: HandlePassword(arg); // 密码验证 break; case SYST: SendReply(215 UNIX Type: L8); // 系统类型标识 break; case PWD: SendReply($257 \{_currentDir}\ is current directory); break; case CWD: ChangeDirectory(arg); // 切换目录 break; case PASV: EnterPassiveMode(); // 进入被动模式 break; case PORT: EnterActiveMode(arg); // 进入主动模式 break; case LIST: ListFiles(arg); // 列目录 break; case RETR: SendFile(arg); // 下载文件 break; case STOR: ReceiveFile(arg); // 上传文件 break; case QUIT: SendReply(221 Goodbye.); _client.Close(); break; default: SendReply(502 Command not implemented); break; } }命令切分用Split加字符数组、指定最大分割数为 2这样参数里的空格不会被错误拆分。ToUpperInvariant()是为了兼容大小写混合的命令输入。每个命令对应一个方法代码的可读性和可维护性都更好。这里有个协议层的细节值得注意每个命令的响应都必须以\r\n结尾这是 FTP 协议的硬性要求。如果只发\n部分严格实现的客户端会解析异常。我在SendReply方法里统一拼好行尾符避免每个分支重复写错。3.3 数据传输模式主动与被动模式的一个差异说明FTP 最容易被忽略的是数据连接建立方式。主动模式下客户端发PORT命令告诉服务端「你连我的某个端口」服务端主动发起数据连接。被动模式下客户端发PASV命令服务端开放一个随机端口并告诉客户端「你连我这个端口」。C# 实现对被动模式的处理是在服务端临时开一个TcpListener监听随机端口把这个端口号通过227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)回复给客户端。private void EnterPassiveMode() { // 随机绑定一个高端口作为被动模式数据通道 _dataListener new TcpListener(IPAddress.Any, 0); _dataListener.Start(); int port ((IPEndPoint)_dataListener.LocalEndpoint).Port; // 把 IP 和端口拼成 FTP 协议规定的六段格式 string ipPart GetLocalIPv4().Replace(., ,); string portPart ${port / 256},{port % 256}; SendReply($227 Entering Passive Mode ({ipPart},{portPart})); }被动模式下服务端和客户端之间多了一层 NAT 和防火墙的交互端口范围需要固定不能完全随机。常见做法是配置一个被动端口范围比如 50000-50100服务端从这个范围内分配端口。固定范围的好处是防火墙规则好写只放行这一百个端口就行随机端口的话防火墙就没法配了。4. 三种客户端接入 Windows 服务化部署把服务器真正跑起来4.1 编译出来的服务端怎么注册成 Windows 服务解决方案里Service.csproj的作用就是把 FTP 服务端包装成 Windows 服务。编译之后用管理员权限打开命令行执行安装命令。常见做法是用sc create命令注册。sc create FTPServer binPath C:\FTP\FTPServer.exe start auto displayname C# FTP ServerbinPath指向编译出来的可执行文件start auto表示开机自启displayname是服务管理器中显示的名称。注意后面必须有一个空格这是sc命令的一个怪癖。注册之后用net start FTPServer启动服务用sc delete FTPServer卸载服务。如果你的编译环境有 Visual Studio 开发命令行也可以用InstallUtil.exe Server.exe来安装服务。这种方式需要在工程里添加ServiceInstaller组件sc create则没有这个要求我一般优先用sc。4.2 用户权限分配与目录隔离这个源码的权限分配功能核心思想是给每个用户绑定一个根目录用户只能在这个目录范围内操作不能跳到磁盘其他地方。这一点很重要尤其是多个部门共用一台服务器的时候不做目录隔离的 FTP 等于裸奔。权限项说明典型配置用户根目录用户登录后所在的默认目录D:\FTPData\u001是否允许上传控制 STOR 命令是否可用部门共享账号开只读账号关是否允许删除控制 DELE/RMD 命令是否可用管理员开普通用户关最大连接数限制单用户并发会话数默认 2防止下载占满带宽IP 限制只允许特定网段访问内网192.168.1.*可访问这些配置在实际落地时应该落成配置文件或数据库表。源码里管理后台可以配置这些项配置保存在本地文件里服务启动时加载。如果想让权限配置支持远程修改可以把配置存储换成 SQLite 或者 SQL ServerWEB 管理端改完数据库直接生效不需要重启服务。4.3 三种客户端访问方式怎么选摘要里提到访问 FTP 服务器有三种方法IE 浏览器或 Windows 资源管理器、ftp命令、专用 FTP 客户端工具。用 Windows 资源管理器访问的方式是直接在地址栏输入ftp://192.168.1.10回车之后会弹出登录框。这种方式适合偶尔传个文件的场景不用装任何工具。但有一个坑IE 浏览器从 IE10 开始默认不再支持 FTP 协议ftp:的地址会提示找不到服务器只有 Windows 资源管理器还保留着这个功能。遇到浏览器访问不了的换资源管理器试一下。用ftp命令适合脚本化操作批处理里写上传下载任务非常方便。ftp -n 192.168.1.10 EOF quote USER ftpuser quote PASS ftppass binary prompt off cd /upload put report.pdf quit EOF-n参数表示不自动登录后续用quote USER和quote PASS手动传账号密码。binary切换到二进制模式传图片压缩包必须加这一句不然会丢字节。prompt off配合mput批量上传时不会逐文件确认。专用客户端工具比如 CuteFTP、FileZilla Client适合频繁传输、需要断点续传的场景。这些工具默认对主被动模式的处理更智能失败率最低。我的建议是人工少量传文件用资源管理器脚本自动化用ftp命令大量频繁传文件用专业客户端。4.4 WEB 端管理的部署方式WEB 管理端和 FTP 服务端在同一套代码里实际部署时可以跑在同一个进程里给 WEB 管理单独开一个端口比如 8080。为什么不直接跑在 80 端口因为 80 端口容易被其他 Web 服务占用而且管理后台直接暴露在公网端口上更容易被扫描到换个不常见端口至少减少一部分扫描流量。我在部署内部工具的时候有个习惯就是 WEB 管理端一定要加一层限制比如只允许内网 IP 访问或者在前面套一层反向代理做 Basic Auth。虽然源码自带账号密码登录但再叠一层访问控制总是更安心。5. 部署与运行避坑五条真实翻车记录5.1 防火墙放行了 21 端口客户端还是连不上现象在 Windows 服务端上开启了防火墙放行规则允许 TCP 21 端口入站但客户端连接提示超时。原因客户端使用了被动模式控制连接确实到 21 端口但数据连接需要服务端开放额外的高端口。如果被动模式端口范围是随机分配的防火墙没法放行数据通道建立失败表现就是登录成功但列目录超时。解决把服务端的被动模式端口范围固定下来比如设置为 50000-50100然后在防火墙里放行这个端口段。netsh advfirewall firewall add rule nameFTP Passive Ports dirin actionallow protocolTCP localport50000-50100这条命令用netsh添加一条入站规则作用范围是 50000 到 50100 的 TCP 端口。跑完之后再用客户端连一次列目录和传输文件就应该正常了。5.2 注册成 Windows 服务后启动就自动停止现象net start FTPServer提示服务启动后停止去 Windows 事件查看器里看到服务异常退出的记录。原因服务账户权限不够无法读取配置目录或写入日志文件。默认的 LocalSystem 账户理论上权限很高但如果配置目录指定到某个受限的网络共享路径或者代码里用了对当前用户依赖较强的路径就会启动失败。解决把配置文件和日志目录改成服务进程有写权限的本地路径比如C:\ProgramData\FTPServer\。如果用了自定义服务账户给该账户分配目录的读写权限。启动失败后不要反复net start先去事件查看器看具体异常信息大多数时候错误提示已经指出了问题点。5.3 中文文件名上传后变成乱码现象客户端上传一个名为项目资料.zip的文件在服务端磁盘上看到文件名是乱码。原因FTP 协议在 RFC 标准里没有明确文件名编码服务端和客户端都默认用本地系统编码。Windows 系统默认用 GBK 编码Linux 客户端默认用 UTF-8。双方不一致时文件名按字节传输解析出来就是乱码。解决在服务端代码里统一把文件名转换为 UTF-8。FTP 有一个扩展命令OPTS UTF8 ON客户端登录后发送这个命令表示后续使用 UTF-8 编码。服务端的做法是检测到该命令后返回200 UTF8 enabled后续所有文件名传输都按 UTF-8 处理。通知客户端优先开启 UTF-8 选项FileZilla Client 默认就是开启状态问题不大。5.4 上传大文件过程中断且没有断点续传现象上传一个 2GB 的压缩包传到一半网络抖动整个文件上传失败需要重头再传。原因FTP 协议本身支持断点续传但需要客户端发送REST命令指定偏移量服务端也要支持APPE或STOR时的偏移写入。部分客户端要么没开断点续传选项要么服务端没实现REST命令处理。解决检查服务端命令解析里是否实现了REST命令。在ProcessCommand的switch里加上这个分支记录偏移量在STOR时从偏移位置开始写入文件流。命令处理逻辑大概是收到REST 1048576后标记_restOffset 1048576随后收到STOR时打开文件流并将写入位置移动到偏移处。命令行客户端用reget命令触发断点续传。5.5 目录权限配置错误导致用户看到整个磁盘现象某用户登录后可以用CWD ..跳出自己的根目录看到服务端整个磁盘的文件。原因权限校验只检查了用户路径前缀没有做路径穿越防护。如果用户根目录配置为D:\FTPData\u001客户端发送CWD ..服务端拼出D:\FTPData\u001\..解析为D:\FTPData就不再是根目录范围了。解决在目录切换方法里做路径合规校验。把用户根目录的完整物理路径作为前缀每次CWD之后用Path.GetFullPath解析出最终路径判断是否以根目录前缀开头不是就拒绝并回复550 Access denied。private bool IsPathInsideRoot(string fullPath) { string root Path.GetFullPath(_userRoot).TrimEnd(\\) \\; string target Path.GetFullPath(fullPath); return target.StartsWith(root, StringComparison.OrdinalIgnoreCase); }这段代码把目标路径规范化之后判断是否在根目录内能挡住..的路径穿越尝试。凡是文件操作下载、上传、删除、重命名进方法之前都应该先过一遍这个校验不能只在CWD时校验一次。6. 进阶技巧让 FTP 服务器在长时间运行下更稳6.1 日志异步写入避免磁盘 IO 拖慢文件传输默认情况下每一条命令和传输记录都同步写日志文件磁盘压力大的时候文件传输会被阻塞。一个简单有效的优化是把日志写入丢到独立线程用队列攒一批再批量落盘。生产环境里可以把ConcurrentQueuestring当缓冲区后台线程每 2 秒批量取一次队列里的日志写入文件。这样文件传输路径上不再直接碰磁盘 IO吞吐量提升明显。日志内容要包含时间、客户端 IP、用户名、命令、操作结果排查问题的时候这就是后悔药。6.2 连接数上限和空闲超时FTP 连接占用的是线程和句柄不控制上限的话连接数一多服务就会卡死。在服务端加两个参数最大并发连接数和空闲超时秒数。达到最大连接数时直接回复421 Too many connections不再接受新会话。每个会话如果超过 300 秒没有任何命令主动断开连接。这两个参数在配置文件的[Network]段里配置调优的时候不用重新编译。实际部署的经验值内存 2GB 的 Windows 服务器单机撑 100 个并发会话没有问题如果超过这个量级优先考虑加带宽而不是加内存因为 FTP 的瓶颈通常是网络吞吐。6.3 下载限速与带宽控制文件传输不计代价地占满带宽会拖垮同机房的其他服务。做限速很简单读取文件流往网络流写入的时候按固定速率控制写入节奏。比如限制每条数据连接最多 1MB/s每写 64KB 就Thread.Sleep(64)毫秒按这个比例控制速率。不过睡 64 毫秒粒度太粗更好的做法是按小包限速每写 16KB 数据根据已用时间和目标速率计算剩余需要等待的时间。注意限速是对每一条数据连接单独生效如果有 10 个用户同时下载总带宽消耗是 10 倍需要在全局再做一层流量整形。6.4 我的一些习惯从那以后每次部署完 FTP 服务器我都会强制走一遍检查清单防火墙端口范围是否放行、被动模式端口是否固定、服务账户对目录是否有写权限、中文文件名是否正常、断点续传是否生效、用户是否可以用..跳出根目录。这套流程走完上线之后的翻车概率大大降低。FTP 看起来是个老协议但真正跑一遍之后就会发现协议本身不难难的是把这些边角细节全部摸透。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。