TFTP(Trivial File Transfer Protocol,简单文件传输协议)是一种轻量级的文件传输协议,主要用于局域网(LAN)环境中的简单文件传输。它的设计目标是 极简,因此去除了 FTP 的复杂功能(如用户认证、目录列表等),仅支持最基本的文件读写操作。
- 协议基础
- 传输层协议:基于 UDP(端口号 69),而非 TCP,因此不保证可靠性(需应用层自己处理丢包和乱序)。
- 无状态:服务器不记录客户端状态,每个请求独立处理。
仅支持 5+1 种操作:
- RRQ (Read Request) :客户端请求下载文件。
- WRQ (Write Request) :客户端请求上传文件。
- DATA :传输文件数据块。
- ACK :确认收到的数据块。
- ERROR :错误响应。
- RET_TOTAL_SIZE_OPCODE请求文件大小(非标准 TFTP,属于 RFC 2349 扩展)
- 文件传输流程
下载文件(RRQ) - 1 客户端 → 服务器:发送 RRQ 包(含文件名和传输模式:netascii/octet/mail)。
- 2 服务器 → 客户端:发送第一个 DATA 包(数据块编号从 1 开始,每个块默认 512 字节)。
- 3 客户端 → 服务器:回复 ACK(确认收到的块编号)。
- 4 重复 DATA + ACK:直到 DATA 包长度 < 512 字节(表示传输结束)。
上传文件(WRQ)
- 1 客户端 → 服务器:发送 WRQ 包。
- 2 服务器 → 客户端:回复 ACK 0(表示准备接收)。
- 3 客户端 → 服务器:发送 DATA 1(第一个数据块)。
- 4 服务器 → 客户端:回复 ACK 1。
- 5 重复 DATA + ACK 直到传输完成。
- 关键机制
(1)块编号(Block Number) - 每个 DATA 包和 ACK 包包含一个 16 位块编号(从 1 开始递增)。
- 通过编号确认数据包顺序,解决 UDP 的乱序问题。
(2)超时重传
- 如果发送方未收到 ACK,会在超时(默认 5 秒)后重传数据包。
- 重传次数超过限制(通常 5 次)则终止传输。
(3)固定块大小
- 默认每个 DATA 包 512 字节,若某包 < 512 字节 表示文件传输结束。
- 如果支持 块大小协商(RFC 2348),可调整块大小以提高效率。
- 与 FTP 的对比
- 典型应用场景
1 网络设备固件升级(如路由器、交换机)。
2 无盘工作站启动(通过 TFTP 下载操作系统镜像)。
3 嵌入式系统 中简单文件传输(资源受限环境很有效率)。 - 安全问题
- 无认证机制:任何知道 IP 的用户均可读写文件(需依赖网络隔离)。
- 明文传输:数据未加密,容易被嗅探。
数据结构相关
在TFTP协议中,当客户端发送 RRQ(Read Request)请求后,服务器返回的数据包(buffer)的结构取决于操作是否成功。以下是可能的响应数据结构:
关键字段tsize(非标准 TFTP,属于 RFC 2349 扩展):
- "tsize":固定字符串,表示选项名。
- "0":客户端用 "0" 表示请求服务器返回文件大小(若为 WRQ,客户端可填写实际文件大小)。字符“0”在ASCII中就是0x30=48编号
- tsize 是 TFTP 的扩展选项,需客户端和服务器共同支持。
- 构建关键:在 RRQ/WRQ 中添加 "tsize\0值\0",服务器通过 OACK 返回实际大小。
- 用途:优化用户体验(显示进度)或校验资源(如磁盘空间)。
- OACK 操作码:0x0006(非标准 TFTP,属于 RFC 2349 扩展)
以下为可以使用的OC源码,分为xx.h & xx.m,iOS和macOS平台都可以使用
----xx.h---
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TFTPFileDownloader : NSObject
@property (nonatomic, assign) BOOL isRunning;
@property (nonatomic, strong) NSString *serverIP;
@property (nonatomic, assign) int serverPort;
@property (nonatomic, strong) NSString *filename;
@property (nonatomic, strong) void (^completionHandler)(BOOL success, NSString *__nullable filePath, NSString *__nullable error);
- (instancetype)initWithServerIP:(NSString *)ip port:(int)port filename:(NSString *)filename;
- (void)startDownloadWithCompletion:(void (^)(BOOL success, NSString *filePath, NSString *error))completion;
- (void)stopDownload;
@end
----xx.m---
#import "TFTPFileDownloader.h"
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#define TFTP_PORT 69
#define BLOCK_SIZE 512
#define RRQ_OPCODE 1
#define DATA_OPCODE 3
#define ACK_OPCODE 4
#define ERROR_OPCODE 5
#define RET_TOTAL_SIZE_OPCODE 6
@implementation TFTPFileDownloader {
int _socket;
NSThread *_downloadThread;
}
- (instancetype)initWithServerIP:(NSString *)ip port:(int)port filename:(NSString *)filename {
self = [super init];
if (self) {
_serverIP = ip;
_serverPort = port;
_filename = filename;
_isRunning = NO;
}
return self;
}
- (void)startDownloadWithCompletion:(void (^)(BOOL success, NSString *filePath, NSString *error))completion {
self.completionHandler = completion;
if (self.isRunning) {
if (self.completionHandler) {
self.completionHandler(NO, nil, @"Download already in progress");
}
return;
}
_downloadThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadThread) object:nil];
[_downloadThread start];
}
- (void)stopDownload {
self.isRunning = NO;
if (_socket > 0) {
close(_socket);
_socket = 0;
}
NSLog(@"TFTPFileDownloader(run) stop run");
}
- (void)downloadThread {
self.isRunning = YES;
NSLog(@"TFTPFileDownloader(run) start run");
int sockfd = 0;
NSFileHandle *fileHandle = nil;
@try {
// 创建 UDP socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
[self handleError:@"Failed to create socket"];
return;
}
// 配置服务器地址
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(self.serverPort ?: TFTP_PORT);
inet_pton(AF_INET, [self.serverIP UTF8String], &serverAddr.sin_addr);
// 发送 RRQ 包
NSData *rrqPacket = [self buildRRQPacket];
sendto(sockfd, [rrqPacket bytes], [rrqPacket length], 0,
(struct sockaddr *)&serverAddr, sizeof(serverAddr));
uint8_t receiveBuffer[BLOCK_SIZE + 4];
int blockNumber = 1;
// 创建本地文件
NSString *localFilePath;
#if TARGET_OS_OSX
localFilePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject
stringByAppendingPathComponent:self.filename];
[[NSFileManager defaultManager] createFileAtPath:localFilePath contents:nil attributes:nil];
#else
localFilePath = [self getAppropriateFilePathForFilename:self.filename];
BOOL success = [[NSFileManager defaultManager] createFileAtPath:localFilePath
contents:nil
attributes:@{
//用于控制文件在设备锁屏状态下的可访问性
NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication
}];
if (!success) {
[self handleError:[NSString stringWithFormat:@"Failed to create file at %@", localFilePath]];
return;
}
#endif
//文件写入操作
fileHandle = [NSFileHandle fileHandleForWritingAtPath:localFilePath];
if (!fileHandle) {
[self handleError:[NSString stringWithFormat:@"Failed to create file at %@", localFilePath]];
return;
}
// 设置接收超时(例如30秒)
struct timeval tv;
tv.tv_sec = 30;
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in fromAddr;
socklen_t fromAddrLen = sizeof(fromAddr);
while (self.isRunning) {
NSLog(@"TFTPFileDownloader(run) waite package");
ssize_t received = recvfrom(sockfd, receiveBuffer, sizeof(receiveBuffer), 0,
(struct sockaddr *)&fromAddr, &fromAddrLen);
if (received < 0) {
[self handleError:@"Failed to receive packet"];
break;
}
// 字节处理方式
int opcode = ((receiveBuffer[0] & 0xff) << 8) | (receiveBuffer[1] & 0xff);
NSLog(@"TFTPFileDownloader(run) opcode:%d", opcode);
if (opcode == DATA_OPCODE) {
int receivedBlockNumber = ((receiveBuffer[2] & 0xff) << 8) | (receiveBuffer[3] & 0xff);
if (receivedBlockNumber == blockNumber) {
int dataLength = (int)received - 4;
NSLog(@"TFTPFileDownloader(run) rcv data:%d", dataLength);
@try {
[fileHandle writeData:[NSData dataWithBytes:receiveBuffer + 4 length:dataLength]];
} @catch (NSException *exception) {
[self handleError:[NSString stringWithFormat:@"File write error: %@", exception.reason]];
break;
}
// 发送 ACK
NSData *ackPacket = [self buildACKPacket:blockNumber];
sendto(sockfd, [ackPacket bytes], [ackPacket length], 0,
(struct sockaddr *)&fromAddr, fromAddrLen);
if (dataLength < BLOCK_SIZE) {
NSLog(@"TFTPFileDownloader(run) 下载完成");
[self handleSuccess:localFilePath];
break;
}
blockNumber++;
} else {
NSLog(@"TFTPFileDownloader(run) 无效的blockNumber, 需要:%d,返回:%d,重新请求",
blockNumber, receivedBlockNumber);
// 重新发送上一个块的ACK
NSData *ackPacket = [self buildACKPacket:blockNumber - 1];
sendto(sockfd, [ackPacket bytes], [ackPacket length], 0,
(struct sockaddr *)&fromAddr, fromAddrLen);
}
} else if (opcode == ERROR_OPCODE) {
int errorCode = ((receiveBuffer[2] & 0xff) << 8) | (receiveBuffer[3] & 0xff);
NSString *errorMessage = [[NSString alloc] initWithBytes:receiveBuffer + 4
length:received - 4
encoding:NSASCIIStringEncoding];
NSLog(@"TFTPFileDownloader(run) TFTP 错误: 代码 %d, 消息: %@", errorCode, errorMessage);
[self handleError:[NSString stringWithFormat:@"TFTP Error %d: %@", errorCode, errorMessage]];
break;
} else if (opcode == RET_TOTAL_SIZE_OPCODE) {
NSString *msg = [[NSString alloc] initWithBytes:receiveBuffer + 2
length:received - 2
encoding:NSASCIIStringEncoding];
//此处单独处理读区请求后服务端返回的文件字节大小,格式为0006\0tsize\0文件字数\0
NSData *data = [NSData dataWithBytes:receiveBuffer length:received];
NSString * s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"TFTPFileDownloader(run) 收到响应:%d, 数据: %@", opcode, msg);
NSLog(@"TFTPFileDownloader(run) 开始请求数据");
// 发送初始ACK
NSData *ackPacket = [self buildACKPacket:0];
sendto(sockfd, [ackPacket bytes], [ackPacket length], 0,
(struct sockaddr *)&fromAddr, fromAddrLen);
blockNumber = 1;
} else {
[self handleError:[NSString stringWithFormat:@"未知的响应码:%d", opcode]];
break;
}
}
}
@catch (NSException *exception) {
NSLog(@"TFTPFileDownloader(run) 文件下载失败: %@", exception);
[self handleError:[NSString stringWithFormat:@"Exception: %@", exception.reason]];
}
@finally {
if (fileHandle) {
[fileHandle closeFile];
}
if (sockfd > 0) {
close(sockfd);
}
self.isRunning = NO;
NSLog(@"TFTPFileDownloader(run) finish");
}
}
- (NSData *)buildRRQPacket {
// 创建可变数据对象用于构建数据包
NSMutableData *packet = [NSMutableData data];
// 1. 写入起始0x00字节
uint8_t zeroByte = 0x00;
[packet appendBytes:&zeroByte length:1];
// 2. 写入RRQ操作码
uint8_t opcode = RRQ_OPCODE;
[packet appendBytes:&opcode length:1];
// 3. 写入文件名(不带长度前缀)
const char *filename = [self.filename UTF8String];
[packet appendBytes:filename length:strlen(filename)];
// 4. 写入0x00分隔符
[packet appendBytes:&zeroByte length:1];
// 5. 写入"octet"模式(不带长度前缀)
const char *octet = "octet";
[packet appendBytes:octet length:strlen(octet)];
// 6. 写入0x00分隔符
[packet appendBytes:&zeroByte length:1];
// 7. 写入"tsize"选项(不带长度前缀)
const char *tsize = "tsize";
[packet appendBytes:tsize length:strlen(tsize)];
// 8. 写入0x00分隔符
[packet appendBytes:&zeroByte length:1];
// 9. 写入'0'特殊字节
uint8_t specialByte = 0x30;
[packet appendBytes:&specialByte length:1];
// 10. 写入最后的0x00结束符
[packet appendBytes:&zeroByte length:1];
return packet;
}
- (NSData *)buildACKPacket:(uint16_t)blockNumber {
uint8_t ack[4];
ack[0] = 0;
ack[1] = ACK_OPCODE;
ack[2] = (blockNumber >> 8) & 0xff;
ack[3] = blockNumber & 0xff;
return [NSData dataWithBytes:ack length:4];
}
- (void)handleSuccess:(NSString *)filePath {
if (self.completionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.completionHandler(YES, filePath, nil);
});
}
}
- (void)handleError:(NSString *)error {
NSLog(@"TFTPFileDownloader: download failed: %@", error);
if (self.completionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.completionHandler(NO, nil, error);
});
}
}
- (void)dealloc {
[self stopDownload];
}
- (NSString *)getAppropriateFilePathForFilename:(NSString *)filename {
// 1. 获取合适的存储目录
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *directoryURL;
// 优先尝试 Documents 目录
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
if (documentPaths.count > 0) {
directoryURL = [NSURL fileURLWithPath:documentPaths.firstObject];
} else {
// 回退到临时目录
directoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()];
}
// 2. 确保文件名安全
NSString *safeFilename = [self sanitizeFilename:filename];
// 3. 创建唯一文件路径(避免覆盖)
NSString *baseName = [safeFilename stringByDeletingPathExtension];
NSString *extension = [safeFilename pathExtension];
NSURL *fileURL = [directoryURL URLByAppendingPathComponent:safeFilename];
NSUInteger counter = 1;
while ([fileManager fileExistsAtPath:fileURL.path]) {
NSString *numberedFilename = [NSString stringWithFormat:@"%@_%lu.%@", baseName, (unsigned long)counter++, extension];
fileURL = [directoryURL URLByAppendingPathComponent:numberedFilename];
}
return fileURL.path;
}
- (NSString *)sanitizeFilename:(NSString *)filename {
// 移除非法字符
NSCharacterSet *illegalCharacters = [NSCharacterSet characterSetWithCharactersInString:@"/\\?%*|\"<>"];
NSArray *components = [filename componentsSeparatedByCharactersInSet:illegalCharacters];
NSString *sanitized = [components componentsJoinedByString:@"_"];
// 限制长度
if (sanitized.length > 255) {
sanitized = [sanitized substringToIndex:255];
}
// 确保有有效扩展名
if ([sanitized pathExtension].length == 0) {
sanitized = [sanitized stringByAppendingPathExtension:@"dat"];
}
return sanitized;
}
@end
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。