bigywater.tech
网络协议 · 102

TFTP:最简单的基于 UDP 的文件传输

TFTP 是最简单的文件传输协议:5 种报文、没有认证、一次只发一个块等一个 ACK。它简单到可以写进网卡的启动固件里,所以至今仍用来网络启动和配置设备。这篇自己实现它,看清它的每个报文、为什么在有延迟的链路上那么慢,以及 blksize 选项能救多少。

9.94 → 3.66 秒
50 ms 往返下读 100000 字节,块大小 512 与协商 1428 的耗时
516 字节
一个满块 DATA 报文的长度(4 字节头 + 512 字节数据)
0.6 秒
丢一个包之后要多等的超时(正常往返只有 0.05 秒)

5 种报文

实测 · 本机执行输出
    C→S  客户端临时端口→ 服务器 69 端口(此处为 9400)    18 字节  RRQ 文件=small.txt 模式=octet
    C→S  客户端临时端口→ 服务器 69 端口(此处为 9400)    18 字节  RRQ 文件=small.txt 模式=octet
    S→C  服务器 TID→ 客户端   516 字节  DATA 块号=1 数据=512 字节
    C→S  客户端→ 服务器 TID 9402     4 字节  ACK 块号=1
    S→C  服务器 TID→ 客户端   516 字节  DATA 块号=2 数据=512 字节
    C→S  客户端→ 服务器 TID 9402     4 字节  ACK 块号=2
    S→C  服务器 TID→ 客户端   516 字节  DATA 块号=3 数据=512 字节
    C→S  客户端→ 服务器 TID 9402     4 字节  ACK 块号=3
    S→C  服务器 TID→ 客户端   468 字节  DATA 块号=4 数据=464 字节
    C→S  客户端→ 服务器 TID 9402     4 字节  ACK 块号=4
  文件 2000 字节 = 3 × 512 + 464:前 3 个 DATA 是 512 字节(满块),最后一个 DATA 只有 464 字节(不足 512)——“不足 512 字节的 DATA 就是传输结束”。如果文件大小恰好是 512 的整数倍,最后要多发一个 0 字节的 DATA
  服务器回应用的端口 9402 不是请求发往的端口 9400:服务器为每个传输新开一个端口(TID),后续两边都只认这一对端口;拿别的端口来插话会被回 ERROR 5(Unknown transfer ID)

RRQ / WRQ / DATA / ACK / ERROR;服务器每个传输换一个新端口(TID)。

结束与错误

实测 · 本机执行输出
  请求 nope.txt → ('ERR', 1, 'File not found');ERROR 码=1 信息='File not found'

不足 512 字节的 DATA 表示传输结束;文件不存在回 ERROR 1。

停等

实测 · 本机执行输出
  块大小  512:共 196 个 DATA 块,耗时 9.94 秒,吞吐 10 KB/s;内容正确;每个块一个往返 → 理论 196 × 0.05 = 9.80 秒
  块大小 1428:共 71 个 DATA 块,耗时 3.66 秒,吞吐 27 KB/s;内容正确;每个块一个往返 → 理论 71 × 0.05 = 3.55 秒;第一个包是 RRQ 文件=big.bin 模式=octet 选项=['blksize', '1428'],服务器回 OACK 选项=['blksize', '1428']
  吞吐 ≈ 块大小 ÷ 往返时间(512 ÷ 0.05 = 10 KB/s;1428 ÷ 0.05 ≈ 29 KB/s):TFTP 在有延迟的链路上很慢,因为它每次只允许一个块在路上

吞吐 ≈ 块大小 ÷ 往返时间;blksize 选项(OACK)把 512 提升到 1428,耗时降到 37%。

丢包

实测 · 本机执行输出
  丢包 0%:耗时 3.65 秒;客户端超时重传 0 次,服务器超时重传 0 次;内容正确
  丢包 3%:耗时 4.26 秒;客户端超时重传 0 次,服务器超时重传 1 次;内容正确
  每丢一个包(DATA 或 ACK)就要等一个完整的超时(0.6 秒)才重传,而正常一个往返只要 0.05 秒——丢包让 TFTP 的耗时按“超时”而不是按“往返”增长

重传靠超时,一次丢包多等一个完整超时。

分步演示

TFTP
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。TFTP 服务器和客户端都是我自己写的最小实现(只支持 RRQ 和 blksize 选项,没有 WRQ、没有 tsize / timeout 选项、没有重传上限之外的复杂逻辑),全在本机 lo0;“69 端口”用 9400 代替,数据端口也在 netlab 范围内;丢包测试只做了一次
    • 没有验证:WRQ 上传;netascii 模式;RFC 7440 的 windowsize(滑动窗口);“魔法学徒综合征”(重复 ACK 引起的重复 DATA)的防范;PXE 启动用 TFTP 的真实流程;TFTP 没有认证带来的安全问题(任意读取服务器文件的目录穿越);真实 TFTP 服务器(tftpd-hpa 等)的行为
    京ICP备2026067502号-1京公网安备11010502063487号