ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PCMan‘s FTP Server:Windows免安装FTP服务端配置与排错

PCMan‘s FTP Server:Windows免安装FTP服务端配置与排错

简介:PCMan's FTP Server 是一款面向初学者的开源轻量级 FTP 服务端软件,目标是让不熟悉网络配置的使用者也能快速完成基本服务器的搭建,设计上以简单易用为先,功能与安全性并非重点,适合刚接触 FTP 协议、希望了解服务端工作原理的入门者与自学者。压缩包共 143 个文件、约 154KB,主体是 VC++ 工程源码:34 个 cpp 与 39 个 h 文件构成服务端核心逻辑,涉及命令解析、用户管理与主框架界面;43 个 ini 及 rc、bmp、ico 等资源文件承载界面配置与图标位图,另有 dsp、dsw、clw 等工程与类向导文件,可直接用 Visual C++ 打开编译。该资源已有 204 人学习下载,读者可由此获得一套可运行的 FTP 服务端完整实现,对照源码理解连接建立、命令响应与用户权限控制的处理流程,也可在其基础上裁剪扩展,作为课程设计、协议学习与 MFC 界面实践的参考样本。

1. PCMan's FTP Server:一个不用安装的开源 FTP 服务端能解决什么

某次在内网给几台设备刷升级包,手边只有一台 Windows 机器,没有外网,也不允许装需要授权的服务端。先试的是系统自带的 FTP 角色,角色装上、站点建好、授权配完,客户端连上去还是一句 530。最后救场的是一份一百多 KB 的开源软件——PCMan's FTP Server,解压双击就能当服务端使。

这类开源软件在 Windows 上不算多见:不写注册表、不装运行库、不驻留后台服务,从老版本 Windows 到现在的桌面版和服务器版都能开起来。装顺了它就是一台随身的文件交换站;装不顺时现象也很集中——能登录却列不出目录、大文件传一半断掉、中文名全变问号。它适合内网分发、设备取包、临时给同事传大文件这类活;要域名、虚拟主机、强制加密那一套的,它接不住。

装之前先想清楚一件事:这东西小,但小不等于没有前置条件。端口、防火墙、被动模式端口段、NTFS 权限,缺一个就是半小时的排查。

2. 装前清单:Windows 版本、21 端口与被动端口段怎么定

很多人拿到这个 exe 的第一反应是双击、建账号、发地址,然后在客户端那边卡住,回头怀疑软件有问题。实际上绝大多数「连不上」都不是软件的问题,而是装之前没把三件事定下来:跑在哪个系统上、用哪个端口、数据目录给谁读写。这一章把这三件事按顺序捋一遍,后面的步骤才好往下走。

2.1 运行环境:它到底依赖哪些东西

它是原生的 Win32 程序,这一点很关键。不需要 .NET Framework,不需要 VC++ 运行库,不需要 Java,也不需要先装什么「服务端框架」。这也是它在老机器、封闭内网、临时救急场景里特别顺手的原因——拷过去就能开,不用先联网下一堆依赖。程序本身是 32 位的,在 64 位系统上照样跑,不会有什么兼容层的问题。

系统覆盖范围从 Windows XP SP3 一直到现在的桌面版和服务器版都能用。硬件要求基本可以忽略,几十 MB 内存、几百 KB 磁盘就能撑住十几个人的小流量。网上流传最广的版本是 2.0.7,这个项目本身早就停止更新了,所以不必去追什么「最新版」——它没有活跃的迭代分支,你看到的各种版本号差别往往只是汉化包和打包方式不同。

项目要求说明
操作系统Windows XP SP3 及以上桌面版、服务器版均可,32/64 位系统都行
运行库无不需要 .NET、VC++、Java
磁盘占用几百 KB程序本体极小,空间主要留给数据目录
内存占用几十 MB 量级随并发连接数增长,个人用途压力很小
权限普通用户可运行只有加防火墙规则、注册服务需要管理员
网络一个固定内网 IP建议手工配静态地址,别用自动获取

需要提醒的是老程序在新系统上的显示问题:高 DPI 屏幕上界面可能发虚,窗口不会自动缩放。这不影响功能,别把它当成装坏了。

2.2 端口规划:21、被动端口段和防火墙规则

FTP 跟普通 Web 服务最大的区别是它用两条连接:一条控制连接走命令和返回码,一条数据连接专门搬文件。数据连接有两种建立方式,主动模式下由服务端回头连客户端,被动模式下由服务端开一个端口等客户端来连。前者在客户端位于 NAT 后面时基本必挂,所以真正要配的是被动模式,也就是常说的 PASV 端口段。

控制端口默认 21。Windows 并不限制普通用户绑定 1024 以下的端口,所以哪怕不用管理员身份也能绑上 21;但如果这台机器上还跑着别的 FTP 组件,21 会被占,那就换 2121 之类的端口,客户端连的时候显式指定即可。被动端口段别划太大,十个就够十几个人用,划成 50000-50009 这种连续区间最省事,防火墙规则也好写。

# 管理员身份执行:放行控制端口和被动数据端口段 netsh advfirewall firewall add rule name="PCManFTP-Ctrl" dir=in action=allow protocol=TCP localport=21 netsh advfirewall firewall add rule name="PCManFTP-PASV" dir=in action=allow protocol=TCP localport=50000-50009 # 确认端口已经被监听,第二列是本地地址,最后一列是进程 PID netstat -ano | findstr ":21"

第一条规则管登录和命令交互,第二条管文件传输。dir=in表示入站方向,action=allow是放行,localport可以直接写区间,这是 netsh 比较省事的地方,不用一条一条加。规则名自己起,起完以后用netsh advfirewall firewall show rule name="PCManFTP-PASV"能回查,删的时候按名字删,别去翻图形界面找。

netstat那条是装完必跑的确认动作。如果 21 端口没有出现在 LISTENING 状态,说明服务端根本没起来,或者绑到了别的地址上,这时候去查防火墙是白费功夫。真遇到端口被占用,用netstat -ano | findstr :21拿到最后一列的 PID,再tasklist | findstr <PID>就知道是哪个进程抢的。

提示:不要为了「排除问题」直接把防火墙整个关掉。关掉只能证明问题出在防火墙上,证明不了是哪个端口,排查完还得一条条加回来,来回折腾。

2.3 目录与身份:别用管理员账号跑服务

数据目录的选择比想象中重要。我一般单独划一个目录,比如D:\ftp,下面分root(对外可见的根)和upload(收文件的地方)。三个地方坚决不放:用户目录下的 Desktop 和 Downloads,因为权限继承混乱、清理时容易误删;网盘同步目录,同步客户端会持续占用文件句柄,FTP 写入时报共享冲突,属于典型的玄学问题;以及含中文或空格的路径,老程序对宽字符路径的处理并不严谨,这是踩过坑之后定下来的规矩。

身份也要单独给。不要用 Administrator 或者当前登录的管理员账号跑服务,一旦目录权限配错,等于把整台机器的写权限开放出去了。建一个专用本地账号,只对这个数据目录有修改权限,其他盘一概不给。建目录和授权两条命令就能搞定。

# 建一个只碰 FTP 数据目录的本地账号 $p = ConvertTo-SecureString 'ChangeThis_2024!' -AsPlainText -Force New-LocalUser -Name 'ftpsvc' -Password $p -PasswordNeverExpires -Description 'FTP data account' New-Item -ItemType Directory -Force -Path 'D:\ftp\root','D:\ftp\upload' | Out-Null # (OI) 对象继承、(CI) 容器继承、M 修改权限、/T 递归到子目录 icacls 'D:\ftp' /grant 'ftpsvc:(OI)(CI)M' /T

PasswordNeverExpires这一项别省。服务化之后如果这个账号的密码到期,服务会静默启动失败,表现出来就是「昨天还好好的,今天连不上」,而且日志里什么都不会写,非常难查。icacls里的(OI)和(CI)是让权限往下继承,不加这两个参数只对当前目录生效,子目录里的文件照样写不进去。给M(修改)而不是F(完全控制),是因为完全控制里包含改权限的能力,没必要给。

3. 从解压到第一个账号:PCMan's FTP Server 的落地步骤

环境定完就可以动手了。这一章的节奏是:先把文件放对地方,再启动看界面,然后建第一个能登录的账号,最后处理「重启之后它不自己起来」这个必然会遇到的问题。每一步都有可以回退的检查点,别一口气全做完再测。

3.1 落地目录:程序和数据分开放

下载下来一般是个压缩包,解开之后是主程序和可能附带的语言文件、说明文档。把整个文件夹一起搬到D:\apps\pcmanftp,不要只挑那个 exe 拷出来——有些打包版本带语言文件,缺了会变成英文界面,功能不受影响但配置项看着费劲。

程序目录定下来之后就别再挪了。这类绿色程序的配置通常跟着程序目录走,账号的主目录路径、端口、限速这些设置都存在旁边,路径一变,之前配的账号主目录很可能全部失效,表现是登录成功但目录是空的。这也是我建议一开始就用固定路径的原因,别图省事放在桌面临时跑一下。

另外不要放在C:\Program Files底下。那个目录受 UAC 保护,程序写配置的时候会被重定向或者直接失败,表现是「设置改完重启就还原」,很容易误判成软件 bug。

3.2 首次启动:托盘图标、主窗口和日志窗

双击主程序,任务栏右下角会出现一个托盘图标,双击它才弹出主窗口。界面上大致分三块:账号管理、目录或虚拟目录设置、以及一个滚动的日志窗。第一次启动时默认没有任何正式账号,匿名访问可能是开着的也可能是关着的,取决于打包版本的默认配置,所以第一步先去看账号列表,别急着发地址给同事。

日志窗是这个软件最有价值的排错入口,比任何第三方抓包工具都直接。客户端连上来之后,它会把收到的命令和服务端返回的码逐行打出来:USER、PASS、PWD、PASV、LIST、RETR,哪一步断的一目了然。养成习惯——客户端报错的第一时间不是去改配置,而是先看日志窗停在哪一行。

如果双击之后托盘没有图标,先怀疑端口占用。有些机器上装过别的 FTP 组件,21 被占了,程序启动时绑定失败但界面不一定弹提示。这时候用 2.2 节那条netstat查一下,或者干脆先把控制端口改成 2121 试一次,能起来就说明是端口冲突。

3.3 建第一个账号:主目录、权限位和密码

建账号这一步建议保守一点:第一次只开读权限,确认能登录能列目录能下载,再把写权限加上去。一次性把权限全打开,出了问题分不清是认证的问题还是权限的问题。

参数建议值原因
用户名纯英文加数字中文和空格在部分老客户端上会解析出错
密码12 位以上,混合字符这类轻量服务端没有防暴力破解机制
主目录D:\ftp\root或每人一个子目录多人的时候分目录,删库不至于全没
初始权限只勾读取和列目录先验证通路,再逐步放开
匿名访问默认关闭内网也不建议长期开着

密码这件事值得单独说一句。很多人装完随手设个ftp或者123456,觉得反正是内网。但内网里通常还挂着打印机、摄像头、各种默认口令的设备,一台机器被当成跳板之后,第一个被扫的就是这种无防护的 FTP。设一个正经密码,成本几乎为零。

建完之后立刻用客户端连一次,别等到配完限速、虚拟目录、服务化之后再测。一次只改一个变量,出问题才知道是哪个改动引起的。

3.4 无人值守:把它注册成 Windows 服务

托盘程序的固有问题:注销之后进程跟着会话结束,机器重启后不会自动起来,远程桌面断开连接时间长了也可能被回收。放在自己电脑上临时用无所谓,放在一台专门的文件交换机上就必须解决。

常见做法是用 nssm 这类服务包装器,把托盘程序包成一个标准的 Windows 服务。

# 把主程序注册成服务,指定工作目录,设置自动启动和异常重启 nssm install PCManFTP "D:\apps\pcmanftp\PCManFTPd.exe" nssm set PCManFTP AppDirectory "D:\apps\pcmanftp" nssm set PCManFTP Start SERVICE_AUTO_START nssm set PCManFTP AppExit Default Restart nssm set PCManFTP ObjectName ".\ftpsvc" "ChangeThis_2024!" nssm start PCManFTP

AppDirectory必须设,因为程序会去当前工作目录找配置文件,不设的话服务起来读的是C:\Windows\System32,配置全是空的,表现就是「服务在跑但账号全没了」。AppExit Default Restart让它在异常退出时自动拉起,对付偶发的崩溃有用。ObjectName指定用刚才建的那个低权限账号跑服务,不要用 LocalSystem,理由和 2.3 节一样。

注意:服务化之后托盘图标不再出现,界面里改的配置有可能不落到服务实例上,或者被服务重启覆盖。正确顺序是先在界面里把账号、端口、限速全部调好、测通,最后再注册服务,之后所有改动走配置文件,不要再依赖界面。

4. 配置逐项抠:权限位、虚拟目录、限速与中文编码

到这一步服务已经能跑、账号已经能登,剩下的就是把参数调到一个长期不惹事的状态。这一章的四组配置项,前两组决定「谁能干什么」,后两组决定「出事的时候损失多大」。每一项都说清楚为什么这么设,方便你按自己的场景改。

4.1 六个权限位:勾选之前先想清楚谁在用

账号的权限是一组复选框,看着简单,但组合出来的效果差别很大。核心是区分「发文件」和「收文件」两类场景,不要一律全勾。

权限位作用什么场景给
列表/浏览能看见目录里有什么几乎所有场景都要给,不给就是登录成功但一片空白
读取/下载能把文件取走分发升级包、共享资料时给
写入/上传能往目录里放新文件收包场景给,注意不能覆盖同名文件
追加能在已有文件末尾续写断点续传需要;风险是磁盘被慢慢撑满
删除能删文件只给维护账号,普通收发账号不给
重命名能改文件名同上,改名的破坏力常被低估

三个典型组合可以直接抄:只发不收的账号给「列表 + 读取」,客户端只能下载不能动手;只收不发的账号给「列表 + 写入 + 建目录」,不给删除和重命名,这样对方传错了你还能救回来;双向交换的账号才全开,但同时要配上磁盘配额或者在目录所在分区留足余量。

追加权限单独提一下,它是断点续传的前提,但也是唯一一个能让单个文件无限增长的权限。如果开给了外部设备,最好在数据目录上挂一个磁盘配额,或者用计划任务定期清理超期文件。

4.2 虚拟目录:把三块盘拼成一棵树

一个账号只有一个主目录,这是硬限制。虚拟目录解决的就是这个:把别的物理路径挂成主目录下的一个名字,客户端看到的是一个连续的目录树,实际文件散落在不同磁盘上。

最典型的用法是主目录放只读的发布包(D:\ftp\root),上传目录单独放一块大容量盘(E:\incoming),挂成root/upload。客户端登录之后看到的就是一个目录,不用关心后面是几块盘。另一个好处是不暴露真实盘符结构,少一点信息泄露。

挂载的时候有两个坑。一是挂载点名字不要和主目录里已有的真实子目录重名,冲突时的行为不好预期,有的版本会让真实目录直接不可见。二是虚拟目录如果指向 UNC 路径(\\某主机\共享名),而服务是用本地账号跑的,会因为拿不到网络凭据而访问失败,日志里表现为列目录超时或者 550。这种场景要么把服务登录账户换成有共享权限的账号,要么先把共享映射成本地盘再挂。

4.3 限速、连接数与超时:小机器的自保参数

这组参数平时没存在感,出问题的时候就是整台机器被拖死。估算方法很简单:先看上行带宽,再除以单个人愿意接受的传输速度。10 Mbps 上行、单人给 2 Mbps,那就是五个人同时传,设成 8 留点余量。

  • 最大连接数:按上面的估算填,别拍脑袋填 100。超出的连接会被拒,客户端表现是「有时候能连有时候不能」。
  • 单 IP 连接数:建议 2 到 3。多线程下载工具默认会开五到十条连接,不限的话一个人就能把队列占满。
  • 上传/下载限速:单位通常是 KB/s,填的时候注意别把 KB 当 Kb,差八倍。
  • 空闲超时:60 到 300 秒之间。太短会把正在思考人生的客户端踢掉,太长会让半死的连接一直占着名额。

还有一个容易忽略的联动:被动端口段的大小必须大于等于最大连接数。只划了 5 个端口却允许 20 个连接,同时传文件的时候就会有一部分人连不上数据连接,现象是能登录、能列目录、下载卡住不动。这个坑非常隐蔽,因为控制连接一切正常。

4.4 中文乱码:GBK 还是 UTF-8

乱码是这个软件被抱怨最多的地方,但根源不在软件,在 FTP 协议本身。控制连接默认没有规定字符编码,各家客户端各按各的理解来:Windows 资源管理器按系统本地编码(简体中文环境是 GBK)解释,很多第三方客户端默认按 UTF-8 解释。同一个目录,换个客户端打开就是一堆问号。

服务端如果提供编码选项,优先选 UTF-8,这是现在的主流。但如果客户端里有老设备——嵌入式系统、老版本的系统自带客户端——它们只会按本地编码走,那就只能退回 GBK,两边不能同时满足。

折中做法是干脆绕开这个问题:目录名和文件名全部用英文数字加下划线,中文只出现在文件内容里。我现在所有 FTP 目录都是这个规矩,省下来的排查时间远比改名的麻烦多。真要用中文名,就固定一种客户端,别今天用资源管理器明天用别的工具,然后怀疑服务端坏了。

5. 踩坑排查:能登录却列不出目录的五个原因

下面五条是这套环境里出现频率最高的故障,按「现象、原因、解决」写,遇到问题时按顺序对号入座。每一条都对应前面某一节的配置项,回头改就行。

5.1 能登录、目录列不出来

现象:客户端提示登录成功,命令行里敲ls卡十几秒,最后报数据连接建立失败;图形客户端表现为一直转圈然后空白。

原因:九成是被动模式端口段没有放行。控制连接走的是 21,通的;数据连接要连 50000 段,被防火墙拦了。剩下的一成是服务端返回的 PASV 地址是内网 IP,而客户端在另一个网段,连过去自然不通。

解决:先按 2.2 节把 PASV 段加到防火墙规则里,重试。还不行就判断是不是跨网段:把客户端放到同一网段测一次,通就说明是地址回填的问题,服务端有对外 IP 填写项就填上,没有就只能在同网段使用,或者做端口映射时把 PASV 段一起映射。

5.2 输入账号密码一直 530

现象:用户名密码明明是对的,返回 530 登录失败。

原因:按可能性排序——账号没有启用,很多版本新建账号默认是停用状态;账号的主目录路径写错了或者目录被删了,路径不存在时认证阶段就会被拒;密码里带了特殊字符,在某些配置文件里被截断;连接数达到上限,新连接直接被拒。

解决:先看日志窗服务端到底收到了什么用户名,如果连USER都没打印出来,问题在客户端;打印了但紧跟着 530,就去核对主目录是否存在。密码先用纯字母数字试一次,能进就说明是特殊字符的问题,换掉即可。

5.3 传大文件传一半就断

现象:几十 MB 的包传到六成左右断掉,重传又从零开始。

原因:空闲超时设得太短,传输过程中控制连接长时间没有命令往来被判定空闲;磁盘空间不足;杀毒软件的实时扫描锁住了正在写入的文件;网络本身抖动。

解决:先把超时调到 300 秒再试,这是最常见的一条。然后看数据目录所在分区的剩余空间。还断就把数据目录从实时扫描的范围里排除(这条要按自己环境的策略评估,别一刀切)。最后才怀疑网络,方法是同一台机器本地回环测一次,本地不断说明是链路问题。

5.4 中文文件名变成问号

现象:用系统资源管理器打开,中文目录全是问号;换成别的客户端又正常。

原因:控制连接的编码不一致,一端按 GBK 发,一端按 UTF-8 解。这不是权限问题,也不是文件系统问题,改权限改目录都无效。

解决:统一编码,服务端和客户端选同一种。做不到统一就把文件名改成英文,见 4.4 节。别在这上面反复重装软件,重装一百次结果一样。

5.5 重启之后服务没自己起来

现象:手动双击能用,机器一重启就连不上,登录机器一看托盘图标也没有。

原因:托盘程序本来就不是服务,开机自启项有时也不生效;已经服务化的,常见是服务账户密码到期、或者账户没有「作为服务登录」的权限,服务启动失败但系统不弹提示。

解决:用 3.4 节的方式注册服务,账户密码设成永不过期;给 nssm 配上输出重定向,把标准输出和错误写到文件里,服务起不来的时候那个文件里会有直接原因,比翻系统日志快得多。

6. 装完怎么验:三条命令、一张对照表和我的收尾习惯

装完不等于装好。我的标准是:换一台机器、用另一种客户端、走一遍完整的上传下载、校验哈希一致,这才算交付。下面这套冒烟流程十分钟能走完,但能挡掉后面八成的问题。

6.1 三条命令走完一轮冒烟

第一条验登录和列目录,重点是把被动模式开关各试一次。同一条命令,UsePassive设 true 通、设 false 不通,基本可以确定是 NAT 或防火墙方向的问题,而不是账号问题。

# 冒烟一:登录并列出根目录,true 走 PASV,false 走 PORT,各跑一次对比 $cred = New-Object System.Net.NetworkCredential('ftpuser','YourPass123') $req = [System.Net.FtpWebRequest]::Create('ftp://192.168.1.20/') $req.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectory $req.Credentials = $cred $req.UsePassive = $true # 关键变量:换成 false 再跑一次 $req.Timeout = 10000 # 控制连接超时,单位毫秒 $resp = $req.GetResponse() (New-Object System.IO.StreamReader($resp.GetResponseStream())).ReadToEnd() $resp.Close()

第二条验写权限和磁盘空间,传一个几十 MB 的文件,别用小文件,小文件测不出超时和限速的问题。

# 冒烟二:上传一个真实大小的文件 $up = [System.Net.FtpWebRequest]::Create('ftp://192.168.1.20/upload/test.bin') $up.Method = [System.Net.WebRequestMethods+Ftp]::UploadFile $up.Credentials = $cred $up.UseBinary = $true # 必须开,否则换行符会被转换,哈希对不上 $up.UsePassive = $true $bytes = [System.IO.File]::ReadAllBytes('D:\tmp\test.bin') $s = $up.GetRequestStream() $s.Write($bytes, 0, $bytes.Length) $s.Close() $up.GetResponse().Close()

UseBinary这一项千万别漏。文本模式下客户端会做换行符转换,传上去的文件大小和哈希都会变,然后你会以为是传输坏了,其实是模式选错了。

第三条是哈希比对,两端各算一次,值一样才算真正传对了。

# 冒烟三:服务端和客户端各跑一次,比对 SHA256 certutil -hashfile "D:\ftp\upload\test.bin" SHA256 certutil -hashfile "D:\tmp\test.bin" SHA256

6.2 一张验收对照表

检查项动作期望结果
端口监听netstat -ano | findstr :21状态为 LISTENING,PID 是主程序
被动端口客户端强制 PASV 传输数据连接建立成功,无超时
登录认证错误密码试一次返回 530,日志窗有记录
读权限下载一个文件并比对哈希哈希一致
写权限上传一个文件目录中出现且大小一致
权限边界用只读账号尝试上传被拒绝,返回 550 类错误
中文名建一个中文名目录再访问客户端显示正常或按约定全英文
重启演练重启机器后直接连接无需人工干预即可登录

倒数第二行那个「权限边界」最容易被跳过。只测了「能做的都能做」,没测「不能做的确实做不了」,等于权限配置根本没验证过。用一个只读账号去传一次,被明确拒绝,才说明权限位生效了。

6.3 我的收尾习惯

从那以后我每次交付这类环境,都要强制走一遍「换机验证 + 重启演练」。换机验证是拿一台没参与过配置的机器当客户端,避免把配置机的缓存和环境变量当成理所应当;重启演练是直接重启一次服务器,不做任何人工操作就看能不能连上——服务化配得对不对,只有这一步能验出来。

另外两个小习惯:一是把日志窗的输出定期留档,出问题的时候能倒推出是哪天哪个 IP 传了什么;二是数据目录所在分区的剩余空间挂个提醒,FTP 这种长期无人看守的服务,最后出问题十有八九是磁盘满了,而不是软件坏了。这套东西不复杂,把该配的配到位、该验的验一遍,剩下的就是让它安静地跑着。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进