動態

詳情 返回 返回

TFTP(Trivial File Transfer Protocol,簡單文件傳輸協議) - 動態 詳情

TFTP(Trivial File Transfer Protocol,簡單文件傳輸協議)是一種輕量級的文件傳輸協議,主要用於局域網(LAN)環境中的簡單文件傳輸。它的設計目標是 極簡,因此去除了 FTP 的複雜功能(如用户認證、目錄列表等),僅支持最基本的文件讀寫操作。

  1. 協議基礎
  2. 傳輸層協議:基於 UDP(端口號 69),而非 TCP,因此不保證可靠性(需應用層自己處理丟包和亂序)。
  3. 無狀態:服務器不記錄客户端狀態,每個請求獨立處理。
  4. 僅支持 5+1 種操作

    • RRQ (Read Request) :客户端請求下載文件。
    • WRQ (Write Request) :客户端請求上傳文件。
    • DATA :傳輸文件數據塊。
    • ACK :確認收到的數據塊。
    • ERROR :錯誤響應。
    • RET_TOTAL_SIZE_OPCODE請求文件大小(非標準 TFTP,屬於 RFC 2349 擴展)
  5. 文件傳輸流程
    下載文件(RRQ)
  6. 1 客户端 → 服務器:發送 RRQ 包(含文件名和傳輸模式:netascii/octet/mail)。
  7. 2 服務器 → 客户端:發送第一個 DATA 包(數據塊編號從 1 開始,每個塊默認 512 字節)。
  8. 3 客户端 → 服務器:回覆 ACK(確認收到的塊編號)。
  9. 4 重複 DATA + ACK:直到 DATA 包長度 < 512 字節(表示傳輸結束)。

上傳文件(WRQ)

  • 1 客户端 → 服務器:發送 WRQ 包。
  • 2 服務器 → 客户端:回覆 ACK 0(表示準備接收)。
  • 3 客户端 → 服務器:發送 DATA 1(第一個數據塊)。
  • 4 服務器 → 客户端:回覆 ACK 1。
  • 5 重複 DATA + ACK 直到傳輸完成。
  1. 關鍵機制
    (1)塊編號(Block Number)
  2. 每個 DATA 包和 ACK 包包含一個 16 位塊編號(從 1 開始遞增)。
  3. 通過編號確認數據包順序,解決 UDP 的亂序問題。

(2)超時重傳

  • 如果發送方未收到 ACK,會在超時(默認 5 秒)後重傳數據包。
  • 重傳次數超過限制(通常 5 次)則終止傳輸。

(3)固定塊大小

  • 默認每個 DATA 包 512 字節,若某包 < 512 字節 表示文件傳輸結束。
  • 如果支持 塊大小協商(RFC 2348),可調整塊大小以提高效率。
  1. 與 FTP 的對比
  2. 典型應用場景
    1 網絡設備固件升級(如路由器、交換機)。
    2 無盤工作站啓動(通過 TFTP 下載操作系統鏡像)。
    3 嵌入式系統 中簡單文件傳輸(資源受限環境很有效率)。
  3. 安全問題
  4. 無認證機制:任何知道 IP 的用户均可讀寫文件(需依賴網絡隔離)。
  5. 明文傳輸:數據未加密,容易被嗅探。

數據結構相關

在TFTP協議中,當客户端發送 RRQ(Read Request)請求後,服務器返回的數據包(buffer)的結構取決於操作是否成功。以下是可能的響應數據結構:
image.png

  • 關鍵字段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
user avatar wodekouwei 頭像 zouzaidadaomanshihuaxiang 頭像
點贊 2 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.