少女祈祷中...

一、TFTP 协议基础

协议概述

  • TFTP(Trivial File Transfer Protocol)是一种简单的文件传输协议,基于 UDP 实现,无连接、轻量级,常用于网络设备启动或嵌入式环境。
  • 端口号:69(服务器监听)。
  • 传输模式:通常使用“octet”(二进制)和“netascii”(文本)两种模式。

报文格式

操作码 含义 报文格式描述
1 RRQ 2 字节 Opcode + 文件名 + 0 + 模式 + 0
2 WRQ 同上
3 DATA Opcode(2B) + Block#(2B) + Data(0-512B)
4 ACK Opcode(2B) + Block#(2B)
5 ERROR Opcode(2B) + ErrorCode(2B) + ErrMsg + 0

传输流程

读取文件(RRQ)
  1. 客户端向服务器发送 RRQ 请求,包含文件名和传输模式。
  2. 服务器回复第一个 DATA 数据包(块号从1开始)。
  3. 客户端写入数据并发送对应 ACK。
  4. 服务器发送下一块 DATA。
  5. 重复以上步骤,直到收到数据长度小于 512 字节,表示文件传输结束。
写入文件(WRQ)
  1. 客户端发送 WRQ 请求,告知服务器准备上传文件。
  2. 服务器回应 ACK (块号 0),表示可以开始发送数据。
  3. 客户端从块号 1 开始发送 DATA。
  4. 服务器对每个 DATA 发送 ACK。
  5. 当客户端发送最后一块数据,且数据长度小于 512 字节时,传输完成。

传输可靠性

  • TFTP 使用简单的停止等待(Stop-and-Wait)机制。
  • 每个数据包发送后等待对应 ACK,超时重传。
  • 支持重传次数限制,超过则中断传输。

二、程序结构与设计

主要数据结构

1
2
3
4
5
6
7
8
9
10
11
struct tftp_handle {
int sockfd; // UDP 套接字
int request_mode; // 请求类型:RRQ=1,WRQ=2
int retry; // 重试计数
int block_idx; // 当前块号(从1开始)
FILE *fp; // 本地文件指针
char filename[256]; // 文件名
char encode[16]; // 传输模式 "octet" 或 "netascii"
struct sockaddr_in server_addr; // 服务器地址
struct sockaddr_in client_addr; // 客户端地址(接收端地址)
};

核心函数设计

  • socket_init()
    创建 UDP 套接字。
    初始化服务器地址结构。
    打开本地文件,RRQ 以写入模式打开文件(接收数据),WRQ 以读取模式打开(发送数据)。
    参数校验(文件名长度、IP 地址格式)。
  • send_request()
    构造 RRQ 或 WRQ 请求报文。
    格式为:Opcode(2B) + 文件名 + 0 + 传输模式 + 0。
    使用 sendto() 发送给服务器69端口。
  • Read_data()
    通过 recvfrom() 接收 DATA 报文。
    验证操作码是否为 DATA。
    检查块号是否符合预期(防止重复或乱序)。
    将数据写入本地文件。
    发送 ACK 给服务器确认收到。
    处理重复包,发送 ACK 并重试,防止丢包。
    返回数据长度,数据长度小于512字节时认为传输结束。
  • Write_data()
    初次调用时等待服务器对 WRQ 的 ACK(块号0),以便初始化 client_addr。
    读取本地文件数据,封装成 DATA 报文发送。
    等待服务器 ACK,支持重传机制。
    检查 ACK 的块号,防止重复 ACK。
    发送完最后一块(<512字节)数据后结束。
  • send_ack()
    构造 ACK 包(Opcode=4,Block#)。
    发送给服务器,确认数据包接收。
  • resource_free()
    关闭文件和套接字,释放资源。

错误处理机制

超时和重传:重试计数 retry,超过最大次数后停止传输。
收到错误报文(Opcode=5)后终止传输。
文件打开失败、网络异常等均有提示并终止。

程序流程简述

初始化套接字和文件。 ->
发送 RRQ/WRQ 请求。 ->
进入数据收发循环:{ RRQ:调用 Read_data() 循环接收数据包并 ACK。WRQ:调用 Write_data() 循环发送数据包并等待 ACK。 } ->
传输完成后释放资源。

抓包分析

调用 Write 模式时 抓包

block0
客户端发 WRQ 请求给服务器 UDP 69 端口。
服务器收到 WRQ 后:会分配一个新的临时端口(非 69)作为后续数据传输的目标端口。这个在抓包中的表现为:服务器通过新的端口(58372)向客户端发送 ACK(block0)。
客户端socket收到这个 ACK 包需要“绑定”服务器的这个新端口(client_addr),确保后续的发送/接收针对正确的端口。
last_packet
整个写的过程就是 客户端向服务器发送数据和block, 服务器向客户端回复ACK和已收到的block,当客户端发送的数据长度小于512时,就是最后一个包,发完就结束了。

调用 Read 模式时 抓包

block0
第一个包都是请求包,有请求的类型(读/写),请求的文件名,文件的编码方式等,也只有请求包是和服务器的69端口在通讯,后面都是和服务器的一个随机端口通讯。
这个发包的过程和 Write 一样,只不过是服务器发数据包,客户端回复block号以表示确认。

代码链接

https://github.com/tlxchen/TFTP_Learn