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种报文类型。我画了一张流程图,帮你理清它们之间的关系:
每种报文的具体格式如下:
| 报文类型 | 操作码 | 报文结构 | 说明 |
|---|---|---|---|
| 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作为"众所周知端口",但实际数据传输并不走这个端口。具体流程是这样的:
- 客户端向服务端的端口69发送RRQ或WRQ报文
- 服务端收到请求后,分配一个临时端口(通常是随机高位端口)
- 后续所有的DATA/ACK报文都在客户端端口和服务端临时端口之间传输
- 传输结束后,临时端口立即释放
这种设计有什么好处?我举个例子:如果所有数据都走端口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吧。