iOS開發(fā)-TFTP客戶端和服務(wù)器的實(shí)現(xiàn)

TFTP(Trival File Transfer Protocal),簡單文件傳輸協(xié)議,該協(xié)議在端口69上使用UDP服務(wù)。TFTP協(xié)議常用于無盤工作站或路由器從別的主機(jī)上獲取引導(dǎo)配置文件,由于TFTP報文比較小,能個迅速復(fù)制這些文件

因為作者是做智能家居方向的開發(fā),公司初期實(shí)現(xiàn)硬件的升級是先通過手機(jī)端從服務(wù)器下載固件,然后在通過某種協(xié)議把固件傳輸給硬件設(shè)備,硬件設(shè)備接收完成之后進(jìn)行升級。這里就涉及到一個協(xié)議的選擇,固件本身并不是很大,一般在1M以內(nèi),這時候TFTP協(xié)議無非是最好的選擇之一,輕量級的傳輸不顯得復(fù)雜,對系統(tǒng)的開銷也小。但是網(wǎng)上找了很久都沒發(fā)現(xiàn)有相關(guān)的Demo,所以今天就簡單的基于OC搭建一個TFTP通信的客戶端和服務(wù)器(Demo簡單,暫不支持IPv6和數(shù)據(jù)包超時計時(Demo里面已經(jīng)加上超時處理和錯誤拋出))。

1.TFTP概況

TFTP是一個傳輸文件的簡單協(xié)議,它基于UDP協(xié)議而實(shí)現(xiàn),但是我們也不能確定有些TFTP協(xié)議是基于其它傳輸協(xié)議完成的。此協(xié)議設(shè)計的時候是進(jìn)行小文件傳輸?shù)?。因此它不具備通常的FTP的許多功能,它只能從文件服務(wù)器上獲得或?qū)懭胛募?br> TFTP傳輸起自一個讀取或?qū)懭胛募恼埱?,這個請求也是連接請求。如果服務(wù)器批準(zhǔn)此請求,則服務(wù)器打開連接,數(shù)據(jù)以定長傳輸(一般定在512字節(jié)以內(nèi))。每個數(shù)據(jù)包包括數(shù)據(jù)塊號和一塊數(shù)據(jù),服務(wù)器發(fā)出下一個數(shù)據(jù)包以前必須得到客戶對上一個數(shù)據(jù)包的確認(rèn)。如果一個數(shù)據(jù)包的大小小于規(guī)定長度,則表示傳輸結(jié)束。通信的雙方都是數(shù)據(jù)的發(fā)出者與接收者,一方傳輸數(shù)據(jù)接收應(yīng)答,另一方發(fā)出應(yīng)答接收數(shù)據(jù)。大部分的錯誤會導(dǎo)致連接中斷,錯誤由一個錯誤的數(shù)據(jù)包引起。這個協(xié)議限制很多,這些都是為了實(shí)現(xiàn)起來比較方便而進(jìn)行的。

2.TFTP協(xié)議

既然寫TFTP通信,上來最重要的肯定是協(xié)議

TFTP協(xié)議.png

1:對于數(shù)據(jù)長度以字節(jié)來計算和標(biāo)識。
2:對于TFTP數(shù)據(jù)包我這里面只寫了其中五中,對于簡單的開發(fā)已經(jīng)足夠了,操作碼分別對應(yīng)RRQ讀請求、WRQ寫請求、DATA數(shù)據(jù)包、ACK數(shù)據(jù)包確認(rèn)、ERROR差錯包。
3:文件名、模式、差錯信息這些數(shù)據(jù)長度都是不固定的,文件分成數(shù)據(jù)塊傳輸,數(shù)據(jù)塊一般定在0-512個字節(jié)之間,太大可能傳輸?shù)木筒灰欢ò踩?,因為TFTP是基于UDP來進(jìn)行數(shù)據(jù)傳輸,在數(shù)據(jù)鏈路層有MTU的限制每個數(shù)據(jù)包的大?。?500字節(jié))
MTU(1500 byte)-PPP的包頭包尾的開銷(8 byte)-IP頭(20 byte)-UDP頭(8 byte) = 1464(byte)
從上面看出實(shí)際每個UDP包數(shù)據(jù)是在1464字節(jié)以內(nèi),如果超過這個臨界值,系統(tǒng)內(nèi)部會對數(shù)據(jù)包進(jìn)行分片傳輸,由于UDP數(shù)據(jù)包的發(fā)送和接收都是無確認(rèn),讓系統(tǒng)分包去傳送可能存在數(shù)據(jù)丟失,所以業(yè)界一般沒有將數(shù)據(jù)塊定太大,通用512個字節(jié)。
4:文件名和差錯信息最好都用英文字母或者英文字符,一般硬件是低級的單片機(jī),內(nèi)部存儲空間有限,所以一般里面不一定裝有UTF-8數(shù)據(jù)編碼表,一般都是ASCII編碼表,摻雜其他字符可能解析不出來,這個編碼方式具體看硬件

3.TFTP通信流程

TFTP通信流程.png

1.首先服務(wù)器綁定固定端口號開始監(jiān)聽客戶端的連接
2.一切從客戶端發(fā)送的第一個數(shù)據(jù)包開始,里面包含有讀文件還是寫文件的操作碼,需要操作的文件名
3.服務(wù)器找到對應(yīng)的文件,開始分成塊傳送給設(shè)備
4.設(shè)備收到對應(yīng)的數(shù)據(jù)塊后回應(yīng)個服務(wù)器確認(rèn)包,里面包含確認(rèn)塊號,告訴服務(wù)器這塊我收到了你可以傳下塊了
5.直到最后一個分包傳給設(shè)備,設(shè)備根據(jù)包的大小和規(guī)定分片的大小做對比,如果小于規(guī)定的分片則表示是最后一個數(shù)據(jù)包,向服務(wù)器發(fā)送一個ACK確認(rèn)包,然后關(guān)閉連接,服務(wù)器收到最后一個確認(rèn)的ACK后也關(guān)掉自己的Socket,本次傳輸完成;如果最后一個包正好也是分片的大小,服務(wù)器接下來還得傳輸一個操作碼后面數(shù)據(jù)長度為0的數(shù)據(jù)包過去,這樣客戶端才知道沒有數(shù)據(jù)了

4.代碼實(shí)現(xiàn)部分

話不多說,先看Demo效果(效果圖為GIF動態(tài)圖,動畫只執(zhí)行一次,看不到效果可以刷新下頁面重新播放)

TFTP-Demo效果.gif

話不多說,再上Demo代碼地址(點(diǎn)擊文字下載)

(1)服務(wù)器

① 首先初始化套接字,并綁定到指定端口(我這里用傳進(jìn)來的一個端口號,并沒有寫成固定的69),同時檢查創(chuàng)建的套接字的讀寫能力(下面要用這個套接字監(jiān)聽設(shè)備數(shù)據(jù)返回和向設(shè)備發(fā)送數(shù)據(jù))

//套接字初始化(Create Socket)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd <= 0) {
    [self throwErrorWithCode:errno reason:@"Failed to create socket"];
    return;
}
    
//綁定監(jiān)聽地址
struct sockaddr_in addr_server;
addr_server.sin_len = sizeof(struct sockaddr_in);
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(bindPort);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
    
if (bind(_sockfd, (struct sockaddr*)&addr_server, addr_server.sin_len) < 0) {
    [self throwErrorWithCode:errno reason:@"Binding socket failed"];
    return;
}

②監(jiān)聽客戶端的連接,直到有數(shù)據(jù)請求,同時設(shè)置等待超時時間為30s(30s內(nèi)沒有連接則關(guān)閉套接字) ->如果有連接則進(jìn)入步驟③,數(shù)據(jù)解析

//申明一個接受客戶端連接套接字的地址
struct sockaddr_in addr_clict;
socklen_t addr_clict_len = sizeof(struct sockaddr_in);
addr_clict.sin_len = addr_clict_len;
    
//設(shè)置接收請求連接超時時間為30s
struct timeval timeout = {30,0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
    printf("開始設(shè)置Socket服務(wù)器接收連接超時失敗: %s\n",strerror(errno));
}

while (1) {
        
    if (_isOpen == NO) return; //服務(wù)器關(guān)閉直接退出
        
    char recv_buffer[1024];     //接收數(shù)據(jù)緩沖區(qū)
    ssize_t result_recv = recvfrom(_sockfd, recv_buffer, sizeof(recv_buffer), 0, (struct sockaddr*)&addr_clict, &addr_clict_len);
    if (result_recv < 0 && _isOpen) {
        [self throwErrorWithCode:errno reason:@"Read data error"];
        return;
    }
        
    if (result_recv < 4) continue; //數(shù)據(jù)包長度必須大于或等于4,否則不是我們想要的數(shù)據(jù)
        
   //有接收到數(shù)據(jù),進(jìn)入下面的數(shù)據(jù)解析部分 ->③
}

③有接收到數(shù)據(jù),開始解析數(shù)據(jù)是否是客戶端的連接請求,如果是客戶端的連接請求則‘連接’該套接字,實(shí)際上叫注冊更準(zhǔn)確,這里的connect() 并不創(chuàng)建實(shí)質(zhì)意義上的連接,只是向套接字中注冊目的地址信息,方便后面數(shù)據(jù)的收發(fā)(不用每次發(fā)送和接收數(shù)據(jù)都注冊地址信息),接著解析出請求文件名并將該文件加載到緩存,開始下面的數(shù)據(jù)傳輸

if (recv_buffer[1] == TFTP_RRQ) { //操作碼是讀請求 -> 有客戶端連接
            
    //注冊客戶端地址信息
    if (connect(_sockfd, (struct sockaddr*)&addr_clict, sizeof(addr_clict)) != 0) {
        [self throwErrorWithCode:errno reason:@"Registration destination address failed"];
        return;
    }
            
    //1. 解析出文件名
    char* cFileName = &recv_buffer[2];
    NSLog(@"[TFTPServer] 收到第一個請求包IP: %s, 文件名: %s",inet_ntoa(addr_clict.sin_addr),cFileName);

    //2. 拼接路徑
    _filepath = [_filepath stringByAppendingPathComponent:[NSString stringWithCString:cFileName encoding:NSUTF8StringEncoding]];
            
    //3. 初始化一些數(shù)據(jù)
     _fileTotalLen = self.fileData.length;
     NSLog(@"[TFTPServer] 文件長度: %lu",(unsigned long)_fileTotalLen);
     if (_fileTotalLen == 0) {
                
        char send_buffer[512];
        NSUInteger len = [TFTPServerPacket makeErrorDataWithCode:1000
                                                          reason:"Request file name error"
                                                      sendBuffer:send_buffer];
        sendto(_sockfd, send_buffer, len, 0, (struct sockaddr*)&addr_clict, addr_clict.sin_len);
                
        [self throwErrorWithCode:errno reason:@"Request file name error"];
        return;
    }

    //一切準(zhǔn)備就緒,開始傳輸數(shù)據(jù) ->④        
    [self beganToTransportData];
    return;
}

④開始向客戶端地址傳送數(shù)據(jù),發(fā)送第一個數(shù)據(jù)包,記錄下發(fā)送文件的長度并且判斷是否是最后一個數(shù)據(jù)包(數(shù)據(jù)包拼接部分很簡單,按照上面的協(xié)議來拼接數(shù)據(jù),這里就不貼代碼了,具體的自己可以下載下面的demo看),并且設(shè)置套接字接收數(shù)據(jù)超時時間為6s,防止中間傳輸失敗重傳->進(jìn)入步驟⑤,循環(huán)監(jiān)聽客戶端數(shù)據(jù)返回

//1. 局部變量的聲明
char recv_buffer[1024];     //接收數(shù)據(jù)緩沖區(qū)
char send_buffer[1024];     //發(fā)送數(shù)據(jù)緩沖區(qū)
NSUInteger sendLen = 0;     //發(fā)送數(shù)據(jù)的長度
    
//2. 初始化一些數(shù)據(jù)
_blocknum = 1;
_alreadySendLen = 0;
int retry = 0;                  //同一個包重傳次數(shù)
BOOL isLastPacket = false;      //記錄是否是最后一個數(shù)據(jù)包
    
//3. 第一個數(shù)據(jù)包的發(fā)送
sendLen = [TFTPServerPacket makeDataWithTotalData:self.fileData
                                       sendBuffer:send_buffer
                                         location:_alreadySendLen
                                           length:TFTP_BlockSize
                                         blocknum:_blocknum];
if (sendLen < (TFTP_BlockSize + 4)) isLastPacket = YES; //記錄下是發(fā)送的最后一個數(shù)據(jù)包
    
if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
    [self throwErrorWithCode:errno reason:@"Send data error"];
    return;
}
_alreadySendLen = sendLen - 4;
    
//開始傳輸數(shù)據(jù)時,定個數(shù)據(jù)包接收超時時間段為6s
struct timeval timeout = {6,0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
    printf("設(shè)置Socket通信過程中,接收客戶端數(shù)據(jù)超時失?。?s\n",strerror(errno));
}

//開始下面的監(jiān)聽ACK返回,發(fā)送接下來的數(shù)據(jù)包 ->⑤

⑤循環(huán)持續(xù)讀取輸入緩沖中的數(shù)據(jù),如果讀取超時則重發(fā)上次的數(shù)據(jù)包,連續(xù)三次超時則拋出錯誤關(guān)閉套接字,若收到客戶端的數(shù)據(jù) -> 進(jìn)入步驟⑥,解析數(shù)據(jù)包

//4. while循環(huán)監(jiān)聽數(shù)據(jù)包的返回
while (1) {
    
    if (_isOpen == NO) return; //服務(wù)器關(guān)閉直接退出監(jiān)聽
    
    ssize_t result_recv = recv(_sockfd, recv_buffer, sizeof(recv_buffer), 0);
    if (result_recv < 0 && _isOpen) {
        if (errno == EAGAIN) { //接收超時重傳
            retry ++;
            if (retry >= MAX_RETRY) {
                NSLog(@"[TFTPServer] 接收ACK超時,發(fā)送差錯包給客戶端");
                sendLen = [TFTPServerPacket makeErrorDataWithCode:1001
                                                           reason:"The maximum number of retransmissions"
                                                       sendBuffer:send_buffer];
                send(_sockfd, send_buffer, sendLen, 0);
                [self throwErrorWithCode:1001 reason:@"The maximum number of retransmissions"];
                return;
            }else {
                NSLog(@"[TFTPServer] 接收客戶端確認(rèn)包超時 -> 重傳上次的包(塊號:%u)",_blocknum);
                if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
                    [self throwErrorWithCode:errno reason:@"Send data error"];
                    return;
                }
                continue;
            }
        }else {
            [self throwErrorWithCode:errno reason:@"Read data error"];
            return;
        }
    }
    
    //數(shù)據(jù)包長度小于4不要
    if (result_recv < 4) continue;

    //接下來解析客戶端發(fā)送回來的數(shù)據(jù)和對應(yīng)數(shù)據(jù)發(fā)送還有服務(wù)器端操作 ->⑥
}

⑥解析客戶端發(fā)送回來的數(shù)據(jù),首先確認(rèn)操作碼(是否是ACK數(shù)據(jù)包,此時ACK才是我們需要的,錯誤包做下解析和對應(yīng)的操作,其他的數(shù)據(jù)包可以忽略),如果是ACK數(shù)據(jù)包 ->進(jìn)行步驟⑦,解析ACK數(shù)據(jù)包并判斷

//先解析操作碼
char opCode = recv_buffer[1];
if (opCode == TFTP_RRQ || opCode == TFTP_WRQ || opCode == TFTP_DATA) {
    NSLog(@"[TFTPServer] 客戶端發(fā)錯了數(shù)據(jù)包(操作碼: %d), 不理",opCode);
}else if (opCode == TFTP_ACK) { //收到ACK數(shù)據(jù)包

     //收到設(shè)備端的ACK確認(rèn)包->進(jìn)行ACK塊號確認(rèn),發(fā)送對應(yīng)數(shù)據(jù)包 ->⑦

}else if (opCode == TFTP_ERROR) {
    
    //客戶端那邊發(fā)送過來了錯誤包
    NSString *errStr = [[NSString alloc] initWithBytes:&recv_buffer[4] length:result_recv-4 encoding:NSUTF8StringEncoding];
    NSLog(@"[TFTPServer] 客戶端傳送過來差錯信息:  錯誤碼 -> %u 錯誤信息 -> %@",(((recv_buffer[2] & 0xff) << 8) | (recv_buffer[3] & 0xff)),errStr);
    [self throwErrorWithCode:(((recv_buffer[2] & 0xff) << 8) | (recv_buffer[3] & 0xff)) reason:errStr];
    return;
}else {
    NSLog(@"[TFTPServer] 客戶端發(fā)錯了數(shù)據(jù)包(操作碼: %d), 不理",opCode);
}

⑦收到客戶端的ACK確認(rèn)包,首先判斷是否是最后一個數(shù)據(jù)包的確認(rèn)塊號,如果是,則關(guān)閉服務(wù)器(斷掉Socket),本次傳輸完成,如果不是則進(jìn)行步驟⑧,解析塊號判斷,并做對應(yīng)的操作

//①. 解析出確認(rèn)塊號
uint clict_sureblocknum = ((recv_buffer[2]&0xff)<<8)|((recv_buffer[3]&0xff));
//NSLog(@"[TFTPServer] 收到客戶端的ACK數(shù)據(jù)包,塊號:%d",clict_sureblocknum);

//②. 判斷是否是最后一個包的確認(rèn)
if (isLastPacket == YES && _blocknum == clict_sureblocknum) { //是最后一個包了
    
    [self sendComplete];
    return;
    
}else {
    
    //不是最后一個數(shù)據(jù)包的確認(rèn)塊號,解析出塊號并判斷 ->⑧
}

⑧解析出塊號,與自己已經(jīng)發(fā)送的塊號做對比,如果是剛發(fā)的塊號,則確認(rèn)進(jìn)行下一個包的發(fā)送,如果塊號是上一個數(shù)據(jù)包的塊號,則進(jìn)入步驟⑨(數(shù)據(jù)包重發(fā)),否則則為塊號錯亂,退出重新發(fā)送

if (_blocknum == clict_sureblocknum) {
    _blocknum ++;
    retry = 0;
    
    sendLen = [TFTPServerPacket makeDataWithTotalData:self.fileData
                                           sendBuffer:send_buffer
                                             location:_alreadySendLen
                                               length:TFTP_BlockSize
                                             blocknum:_blocknum];
    
    if (sendLen < (TFTP_BlockSize + 4)) isLastPacket = YES; //記錄下是發(fā)送的最后一個數(shù)據(jù)包
    
    if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
        [self throwErrorWithCode:errno reason:@"Send data error"];
        return;
    }
    _alreadySendLen += (sendLen - 4);
    
}else if (clict_sureblocknum == (_blocknum - 1)) {
    //ACK塊號不對,進(jìn)入重發(fā)機(jī)制
    
    //上一個數(shù)據(jù)包客戶端接收有誤, 重傳 ->⑨
}else {
    
    NSLog(@"[TFTPServer] 客戶端返回的確認(rèn)塊號不對 _blocknum:%u clict_sureblocknum:%u",_blocknum,clict_sureblocknum);
    [self throwErrorWithCode:1002 reason:@"Request block number error"];
    return;
}

⑨如果收到的是上次發(fā)送包的確認(rèn)塊號(數(shù)據(jù)丟失可能設(shè)備沒有收到),則判斷對上個包的重發(fā)次數(shù)有沒有達(dá)到上限,如果達(dá)到上限,則向客戶端發(fā)送一個差錯包,告訴客戶端此次傳輸有問題,服務(wù)器要斷開連接了,如果沒有達(dá)到上限則將上次發(fā)送的數(shù)據(jù)包重新發(fā)送

retry ++;
if (retry >= MAX_RETRY) {
    NSLog(@"[TFTPServer] 接收ACK錯誤次數(shù)達(dá)到上限,發(fā)送差錯包給客戶端");
    sendLen = [TFTPServerPacket makeErrorDataWithCode:1001
                                               reason:"The maximum number of retransmissions"
                                           sendBuffer:send_buffer];
    
    send(_sockfd, send_buffer, sendLen, 0);
    [self throwErrorWithCode:1001 reason:@"The maximum number of retransmissions"];
    return;
}else {
    NSLog(@"[TFTPServer] 客戶端發(fā)送ACK塊號有誤(塊號:%u), 重傳上次的包(塊號:%u)",_blocknum,clict_sureblocknum);
    if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
        [self throwErrorWithCode:errno reason:@"Send data error"];
        return;
    }
}
(2)客戶端

①創(chuàng)建Socket套接字,綁定固定端口并檢查套接字的讀寫情況,用來接收服務(wù)器數(shù)據(jù)返回

//初始化套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);

if (_sockfd <= 0) {
    [self throwErrorWithCode:errno reason:@"Failed to create socket"];
    return ;
}

struct sockaddr_in addr_bind;
addr_bind.sin_len = sizeof(struct sockaddr_in);
addr_bind.sin_family = AF_INET;
addr_bind.sin_port = htons(port);
addr_bind.sin_addr.s_addr = htonl(INADDR_ANY);

if (bind(_sockfd, (struct sockaddr*)&addr_bind, addr_bind.sin_len) < 0) {
    [self throwErrorWithCode:errno reason:@"Binding socket failed"];
    return;
}

②初始化服務(wù)器地址信息,并注冊服務(wù)器地址信息,方便后面直接接收數(shù)據(jù)和發(fā)送數(shù)據(jù),并且設(shè)置接讀取輸入緩沖超時時間為6s,防止傳輸過程中出現(xiàn)異常

//注冊套接字目的地址
struct sockaddr_in addr_server;
addr_server.sin_len = sizeof(struct sockaddr_in);
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(port);
inet_pton(AF_INET, host.UTF8String, &addr_server.sin_addr);

if (connect(_sockfd, (struct sockaddr*)&addr_server, addr_server.sin_len) < 0) {
    [self throwErrorWithCode:errno reason:@"Registration destination address failed"];
    return;
}

//設(shè)置讀取數(shù)據(jù)超時
struct timeval timeout = {6, 0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
    printf("[TFTPClient] 設(shè)置接收數(shù)據(jù)超時失?。?s",strerror(errno));
}

③開始向服務(wù)器發(fā)送第一個數(shù)據(jù)包,文件請求數(shù)據(jù)包

//1. 初始化一些變量
char sendBuffer[1024];      //發(fā)送數(shù)據(jù)緩存區(qū)
NSUInteger sendLen;         //發(fā)送數(shù)據(jù)長度
char recvBuffer[1024];      //接收數(shù)據(jù)緩存區(qū)
_blocknum = 0;              //接收塊號記錄
self.fileData.length = 0;   //接收文件緩存區(qū)
int retry = 0;              //記錄同一個包的請求次數(shù)
BOOL isLastPacket = false;  //記錄是否是最后一個數(shù)據(jù)包

//2. 發(fā)送文件請求包
sendLen = [TFTPClientPacket makeRRQWithFileName:filename
                                     sendBuffer:sendBuffer];
if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
    [self throwErrorWithCode:errno reason:@"Read data error"];
    return;
}

//開始監(jiān)聽服務(wù)器數(shù)據(jù)發(fā)送了 -> ④

④循環(huán)監(jiān)聽服務(wù)器發(fā)送數(shù)據(jù),首先判斷是否讀取超時,如果超時則重發(fā)上一次的確認(rèn)塊,連續(xù)三次超時則拋出錯誤關(guān)閉套接字。如果收到數(shù)據(jù)則判斷數(shù)據(jù)長度是否滿足自己的需求,都滿足則進(jìn)行接下來的解析->步驟⑤,不滿足跳過本地循環(huán)讀取,進(jìn)行下次讀取

//3. 開始監(jiān)聽數(shù)據(jù)返回
while (1) {
    
    ssize_t result_recv = recv(_sockfd, recvBuffer, sizeof(recvBuffer), 0);
    if (result_recv < 0 && _isOpen) {
        
        if (errno == EAGAIN) { //讀取數(shù)據(jù)超時
            retry++;
            if (retry >= MAX_RETRY) {
                NSLog(@"[TFTPClient] 請求超時,發(fā)送差錯包給服務(wù)器");
                sendLen = [TFTPClientPacket makeErrorDataWithCode:1001
                                                           reason:"The maximum number of retransmissions"
                                                       sendBuffer:sendBuffer];
                send(_sockfd, sendBuffer, sendLen, 0);
                [self throwErrorWithCode:1001 reason:@"The maximum number of retransmissions"];
                return;
            }else {
                //重發(fā)上一個ACK確認(rèn)包
                NSLog(@"[TFTPClient] 客戶端請求數(shù)據(jù)塊超時,重發(fā)上個ACK(塊號:%u)",_blocknum);
                if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
                    [self throwErrorWithCode:errno reason:@"Send data error"];
                    return;
                }
                continue;
            }
        }else {
            [self throwErrorWithCode:errno reason:@"Read data error"];
            return;
        }
    }
    
    //數(shù)據(jù)長度過短或不是我們需要服務(wù)器地址發(fā)送過來的數(shù)據(jù)都不是我們想要的數(shù)據(jù), 直接丟掉
    if (result_recv < 4) continue;
    
    //是自己需要的數(shù)據(jù),解析出操作碼,進(jìn)行相應(yīng)的操作 ->⑤
}

⑤首先解析出接收到數(shù)據(jù)包的操作碼,判斷是否是DATA對應(yīng)的操作碼,如果不是拋出對應(yīng)的信息和進(jìn)行對應(yīng)的操作,如果是則進(jìn)行接下來的操作->步驟⑥,塊號的確認(rèn)

//解析操作碼
char opCode = recvBuffer[1];
if (opCode == TFTP_RRQ || opCode == TFTP_WRQ || opCode == TFTP_ACK) {
    
    NSLog(@"[TFTPClient] 服務(wù)器發(fā)送了錯誤數(shù)據(jù)包(操作碼: %d),不理",opCode);
    
}else if (opCode == TFTP_DATA) {
    /* 服務(wù)器發(fā)送過來數(shù)據(jù)包 */
    
    //進(jìn)行接下來的數(shù)據(jù)解析和拼接,發(fā)送給確認(rèn)包 ->⑥
    
}else if (opCode == TFTP_ERROR) {
    
    NSString *errStr = [[NSString alloc] initWithBytes:&recvBuffer[4] length:result_recv-4 encoding:NSUTF8StringEncoding];
    NSLog(@"[TFTPClient] 服務(wù)器傳送過來差錯信息: 錯誤碼 -> %u 錯誤信息 -> %@",(((recvBuffer[2] & 0xff) << 8) | (recvBuffer[3] & 0xff)),errStr);
    [self throwErrorWithCode:(((recvBuffer[2] & 0xff) << 8) | (recvBuffer[3] & 0xff)) reason:errStr];
    return;
}else {
    NSLog(@"[TFTPClient] 服務(wù)器傳過來不知名的數(shù)據(jù)包(操作碼: %d)",opCode);
}

⑥解析服務(wù)器發(fā)送過來的數(shù)據(jù)包,拿到塊號和自己這邊記錄的塊號做對比,如果塊號正確則向服務(wù)器發(fā)送本次的塊號確認(rèn)ACK包,并把數(shù)據(jù)拼接到緩存,如果收到數(shù)據(jù)包塊號不正確,則進(jìn)行步驟⑦重確認(rèn)操作

//解析出塊號, 與自己的塊號作比較, 看看服務(wù)器有沒有發(fā)錯
uint blocknum = (recvBuffer[2]&0xff)<<8 | (recvBuffer[3]&0xff);
//NSLog(@"[TFTPClient] 服務(wù)器發(fā)送過來數(shù)據(jù)包,塊號:%u",blocknum);

if (blocknum == (_blocknum + 1)) {
    
    retry = 0;
    _blocknum = blocknum;
    
    //解析數(shù)據(jù)包, 并且判斷是否是最后一個數(shù)據(jù)包
    NSData *data = [NSData dataWithBytes:&recvBuffer[4] length:result_recv-4];
    [self.fileData appendData:data];
    
    if (data.length < TFTP_BlockSize) isLastPacket = YES;
    
    //發(fā)送ACK確認(rèn)包
    sendLen = [TFTPClientPacket makeACKWithBlockNum:_blocknum sendBuffer:sendBuffer];
    if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
        [self throwErrorWithCode:errno reason:@"Send data error"];
        return;
    }
}else {
    //塊號不對,進(jìn)入重發(fā)機(jī)制
    //傳送數(shù)據(jù)塊號不對,進(jìn)行重新確認(rèn) -> ⑦
}

⑦服務(wù)器發(fā)送的數(shù)據(jù)塊號和自己將要接收的塊號對不上,拿到自己最后一個接收到的數(shù)據(jù)包塊號,先判斷對該數(shù)據(jù)包的確認(rèn)次數(shù)有沒有達(dá)到上限,達(dá)到上限則向服務(wù)器發(fā)送差錯包,表示本次傳輸出錯,并關(guān)掉套接字,如果沒有達(dá)到上限,那么重發(fā)這個確認(rèn)塊號,向服務(wù)器確認(rèn)得到正確的數(shù)據(jù)塊

retry++;
if (retry >= MAX_RETRY) {
    NSLog(@"[TFTPClient] 接收數(shù)據(jù)包錯誤次數(shù)達(dá)到上限,發(fā)送差錯包給客戶端");
    sendLen = [TFTPClientPacket makeErrorDataWithCode:1001
                                               reason:"The maximum number of retransmissions"
                                           sendBuffer:sendBuffer];
    send(_sockfd, sendBuffer, sendLen, 0);
    [self throwErrorWithCode:1001 reason:@"The maximum number of retransmissions"];
    return;
}else {
    NSLog(@"[TFTPClient] 服務(wù)器發(fā)送塊號不對(塊號:%u), 重發(fā)送上個ACK確認(rèn)包(塊號:%u)",blocknum,_blocknum);
    if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
        [self throwErrorWithCode:errno reason:@"Send data error"];
        return;
    }
}

⑧確認(rèn)包發(fā)送完或者確認(rèn)包重發(fā)完,判斷是否是最后一個數(shù)據(jù)包的確認(rèn),如果是則關(guān)閉Socket,本次傳輸完成,不是則跳過接著監(jiān)聽

if (isLastPacket) { //就收完成
     [self recevComplete];
     return;
}

5.結(jié)語

上面的代碼部分只是一個大概的思路講解,主要為了方便理解demo里面的代碼邏輯,具體的還是要看demo。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容