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