1. TFTP协议基础:从零开始理解这个"小"协议

各位同学好,我是你们的嵌入式Linux讲师。今天咱们开始第一讲——TFTP协议基础。说实话,我在做嵌入式开发的头两年,对TFTP一直有点"看不上眼",觉得它功能太简单、连个认证都没有。直到有一次在产线上调试一块新板子,FTP死活连不上,最后靠TFTP三分钟就把镜像烧进去了……嗯,从那以后我再也不敢小看它了。

1.1 TFTP协议概述

TFTP,全称Trivial File Transfer Protocol,翻译过来就是"简单文件传输协议"。它基于UDP协议,端口号是69。说白了,它就是FTP的"精简版"——去掉了目录浏览、用户认证、权限控制这些花里胡哨的功能,只保留最核心的文件上传和下载能力。

为什么嵌入式领域这么爱用它?我总结三个原因:

  • 代码量极小——TFTP客户端/服务端的实现代码通常只有几百行,非常适合资源受限的Bootloader环境
  • 无状态设计——每个包都是独立的,丢了就重传,不需要维护复杂的连接状态
  • UDP传输——不需要TCP的三次握手,启动快、延迟低,特别适合局域网内的快速烧录

核心要点:TFTP不是用来替代FTP的,它们各有各的战场。FTP适合复杂文件管理,TFTP适合嵌入式系统启动阶段的快速文件传输。我在项目中经常把TFTP和FTP配合使用——调试阶段用TFTP,正式部署用FTP。

1.2 TFTP与FTP的区别

很多初学者会问:既然有FTP,为什么还要搞个TFTP?我直接给你一张对比表,看完就明白了:

对比项 TFTP FTP
传输层协议 UDP(无连接) TCP(面向连接)
端口号 69(固定) 21(控制)+ 20(数据)
认证机制 无 用户名/密码
目录操作 不支持 支持ls/cd/mkdir等
传输模式 octet / netascii binary / ascii / ebcdic等
可靠性 停等协议+超时重传 TCP滑动窗口
代码体积 约500行C代码 约5000行C代码
典型应用场景 Bootloader、无盘工作站 文件服务器、网站管理

看到没?TFTP的"简单"是刻在骨子里的。它连最基本的"列出目录"功能都没有——你只能通过文件名直接读写文件。我刚开始用的时候也觉得不方便,但后来想通了:在U-Boot那个阶段,你连文件系统都没挂载,要目录浏览干嘛?直接知道文件名就够了。

1.3 TFTP的传输模式:octet与netascii

TFTP支持两种传输模式,这个知识点在实际项目中特别容易踩坑:

  • octet(八位字节模式):按原始二进制数据逐字节传输,不做任何转换。烧录内核镜像、设备树、根文件系统时,必须用这个模式。
  • netascii(网络ASCII模式):将换行符转换为CR-LF对(\r\n)。这个模式主要用于传输纯文本配置文件,比如一些启动参数脚本。

我曾经踩过的坑:有一次给客户部署设备,TFTP烧录内核时忘了指定模式,默认用了netascii。结果内核启动到一半就崩溃了——因为二进制文件里的0x0A字节被自动转换成了0x0D 0x0A,整个镜像都乱了。从那以后,我所有嵌入式烧录脚本里都强制写 mode octet,绝不偷懒。

1.4 TFTP的报文格式

TFTP的报文结构非常简洁,一共只有5种报文类型。我画了一张流程图,帮你理清它们之间的关系:

TFTP 报文交互流程 TFTP 客户端 TFTP 服务端 ① RRQ(读请求) ② DATA(数据块) ③ ACK(确认) 重复②③直到文件传输完毕 ④ WRQ(写请求) ⑤ ERROR(错误) 说明:RRQ/WRQ由客户端发起,服务端响应DATA/ACK/ERROR。 每个DATA包最大512字节,不足512字节表示传输结束。

每种报文的具体格式如下:

报文类型 操作码 报文结构 说明
RRQ(读请求) 0x01 操作码 + 文件名 + 0 + 模式 + 0 客户端请求从服务端下载文件
WRQ(写请求) 0x02 操作码 + 文件名 + 0 + 模式 + 0 客户端请求向服务端上传文件
DATA(数据) 0x03 操作码 + 块编号(2字节) + 数据(0~512字节) 实际传输的数据块
ACK(确认) 0x04 操作码 + 块编号(2字节) 确认收到某个数据块
ERROR(错误) 0x05 操作码 + 错误码(2字节) + 错误信息 + 0 报告传输过程中的错误

小技巧:抓包分析TFTP时,重点关注块编号字段。如果发现连续出现相同的块编号,说明发生了丢包重传。我调试网络问题时,经常用Wireshark过滤 tftp 来观察重传频率——如果重传率超过5%,基本可以断定网络环境有问题。

1.5 TFTP的端口号与传输流程

TFTP使用UDP端口69作为"众所周知端口",但实际数据传输并不走这个端口。具体流程是这样的:

  1. 客户端向服务端的端口69发送RRQ或WRQ报文
  2. 服务端收到请求后,分配一个临时端口(通常是随机高位端口)
  3. 后续所有的DATA/ACK报文都在客户端端口和服务端临时端口之间传输
  4. 传输结束后,临时端口立即释放

这种设计有什么好处?我举个例子:如果所有数据都走端口69,那服务端同时处理多个TFTP请求时就会乱套。用临时端口隔离每个会话,互不干扰。这有点像餐厅里前台(端口69)只负责接单,然后分配给不同的厨师(临时端口)去处理。

完整的传输流程我总结成五个步骤:

步骤 方向 报文 关键信息
1 客户端 → 服务端 RRQ/WRQ 文件名 + 传输模式
2 服务端 → 客户端 DATA(块1) / ACK(写请求) 块编号从1开始
3 客户端 → 服务端 ACK(块1) / DATA(块1) 确认收到第一个数据块
4 重复2~3 DATA/ACK交替 每次块编号递增1
5 服务端 → 客户端 DATA(最后一块) 数据长度 < 512字节表示结束

注意:TFTP的"停等协议"意味着——发送方发完一个数据包后,必须等到接收方的ACK才能发下一个。如果ACK超时(默认5秒),发送方会重传上一个数据包。这种机制虽然简单可靠,但传输大文件时效率很低。我个人建议:TFTP只用来传小于100MB的文件,超过这个量级还是换FTP或HTTP吧。