乐于分享
好东西不私藏

Tinyftp 源码学习笔记:3000 行 C 实现的 FTP 服务器

Tinyftp 源码学习笔记:3000 行 C 实现的 FTP 服务器

学完 socket 网络编程之后,只会写一些小demo,往往还缺一个更接近真实服务的练习项目:既能用标准客户端对接,代码量又不会太大。整理了一个网络编程的项目 Tinyftp——一个用 C 写的简易 FTP 服务器,大约三千行代码,这个项目参考 vsftpd,十分具备学习意义。

1. 项目概览

Tinyftp 适合作为网络编程练手项目,主要因为:

  • 兼容常见 FTP 命令,可用于 FileZilla 等标准客户端
  • 功能较完整:登录、PORT/PASV、上传下载、目录操作、限速、断点续传、连接数限制
  • 架构清晰:多进程 + nobody 特权分离,接近精简版 vsftpd

一句话:代码量不是太大,符合RFC协议规范,框架还不错。

2. FTP 基础

FTP 使用两根连接:

控制连接 持续存在,传输命令与应答,例如 USERPASSRETR,以及 220230226 等状态码。

数据连接 临时建立,用于传文件、列目录;传输结束后关闭。 数据连接有两种模式:

  • PORT(主动)
    :服务器连接客户端
  • PASV(被动)
      :客户端连接服务器

Tinyftp 两种模式均已实现。主动模式需绑定 20 端口,相关特权操作由 nobody 进程完成,这是源码阅读的重点之一。

3. 源码阅读顺序与模块分工

  1. tinyftpd.conf
     —— 配置项
  2. main.c
     —— 监听与 accept
  3. session.c
     —— 会话内二次 fork
  4. ftpproto.c
     —— 命令表与协议主循环
  5. privparent.c
    privsock.* —— nobody 与 FD 传递

src/ 模块大致如下:

文件
作用
main.c
监听、连接数限制、fork 会话
session.c
会话启动:服务进程 / nobody
ftpproto.c
命令分发、上传下载、LIST 等
privparent.c
nobody:绑 20 端口、建立数据连接
privsock.*
父子进程通信、传递文件描述符
hash.*
按 IP 统计连接数
ftpcodes.h
FTP 状态码
parseconf
 / tunable
配置加载

常见配置:listen_port(示例多为 5188)、PASV/PORT 开关、最大连接数、上下行限速、空闲超时等。

4. 核心结构:三层进程模型

第一层:主进程

以 root 启动,读取配置并 listen。 客户端接入后:accept → 统计该 IP 连接数 → fork:

  • 子进程:持有 connfd,进入 begin_session
  • 父进程:关闭 connfd,继续监听;在 SIGCHLD 中回收子进程并更新连接计数

超过限制时返回 421 这一层把「接客」与「处理会话」分开。

第二层:会话内再 fork

begin_session() 中再次 fork:

  • 子进程handle_child
    :解析命令、传输数据;登录成功后切换为普通用户权限
  • 父进程handle_parent
    :降权为 nobody,负责特权相关操作

可概括为:

协议处理运行在较低权限;需要特权(如绑定 20 端口)时,由 nobody 代为执行。

第三层:socketpair 与传 FD

父子进程通过内部通道交换命令,例如:

  • 按 PORT 信息连接客户端
  • 开启 PASV 监听 / accept
  • 成功后将 data_fd 传回服务进程

5. 协议主循环:命令表驱动

ftpproto.c 采用表驱动方式组织命令,例如:

USER → do_userPASS → do_passPORT → do_portPASV → do_pasvRETR → do_retrSTOR → do_storLIST → do_listREST → do_rest(另有 CWD / MKD / DELE / ABOR 等)

handle_child 主循环可记为四步:

  1. 发送欢迎应答 220
  2. readline
     读取一行命令
  3. 拆分命令与参数,命令转大写
  4. 查表调用处理函数;未知命令返回 500

应答格式统一为:

状态码 + 空格 + 说明文字 + \r\n

多数应用层协议服务器的骨架与此类似:读入 → 解析 → 分发 → 回写

6. 登录流程

未使用自建用户表,而是对接系统账号:

  1. USER
     → getpwnam,存在则返回 331
  2. PASS
     → 读取 shadow,经 crypt 校验,成功返回 230
  3. seteuid
     / setegid,并 chdir 到用户家目录

因此需要 root 启动(读取 shadow 后再降权)。 作为学习项目结构清晰;若部署到公网,需考虑网络安全问题。

7. PORT / PASV 流程笔记

PORT(主动)

客户端告知服务器目标 IP 与端口。 服务器先返回 200;真正进行 LIST / RETR / STOR 时,由 nobody 从 20 端口发起 connect,再将数据套接字 FD 交回服务进程。

PASV(被动)

服务进程请求 nobody 监听随机端口,返回类似:

227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)

客户端连入后,accept 得到的 FD 同样回传给服务进程用于传数。

两种模式最终都会进入 get_transfer_fd() 若尚未执行 PORT 或 PASV,则返回 425 Use PORT or PASV first

阅读建议:可先沿 PASV → LIST/RETR 跟完整条路径,再对照 PORT 实现。

8. 其他值得记录的点

  • REST
    :断点续传偏移
  • limit_rate
    :根据当前速率 sleep,实现限速
  • ABOR
    :传输过程中中断(与 SIGURG 相关)
  • 连接数限制
    :总连接数 / 每 IP 连接数,配合哈希表维护

这些点使项目更接近可用的 FTP 服务实现,而不只是最小 demo。

9. 编译与运行备忘

git clone 仓库后进入目录,按 README 编译(部分版本脚本名为 bulid.sh)。

注意:

  • 需以 root 运行
  • 监听端口以 tinyftpd.conf 为准
  • 可用 FileZilla 连接本机验证
  • 建议仅用于学习,勿直接作为公网 FTP 服务暴露

10. 小结

通过阅读 Tinyftp,可将以下内容串起来:

  1. 应用层协议如何用「状态码 + 命令表」组织
  2. FTP 双连接模型,以及 PORT / PASV 的连接方向
  3. 特权分离思想,nobody 进程的职责
  4. 进程间协作与文件描述符传递

项目链接

Gitee:https://gitee.com/vbirds/Tinyftp
GitHub:https://github.com/vbirds/Tinyftp