TFTP(Trival File Transfer Protocal),簡單文件傳輸協議,該協議在端口69上使用UDP服務。TFTP協議常用于無盤工作站或路由器從別的主機上獲取引導配置文件,由于TFTP報文比較小,能個迅速復制這些文件
因為作者是做智能家居方向的開發,公司初期實現硬件的升級是先通過手機端從服務器下載固件,然后在通過某種協議把固件傳輸給硬件設備,硬件設備接收完成之后進行升級。這里就涉及到一個協議的選擇,固件本身并不是很大,一般在1M以內,這時候TFTP協議無非是最好的選擇之一,輕量級的傳輸不顯得復雜,對系統的開銷也小。但是網上找了很久都沒發現有相關的Demo,所以今天就簡單的基于OC搭建一個TFTP通信的客戶端和服務器(Demo簡單,暫不支持IPv6和數據包超時計時(Demo里面已經加上超時處理和錯誤拋出))。
1.TFTP概況
TFTP是一個傳輸文件的簡單協議,它基于UDP協議而實現,但是我們也不能確定有些TFTP協議是基于其它傳輸協議完成的。此協議設計的時候是進行小文件傳輸的。因此它不具備通常的FTP的許多功能,它只能從文件服務器上獲得或寫入文件。
TFTP傳輸起自一個讀取或寫入文件的請求,這個請求也是連接請求。如果服務器批準此請求,則服務器打開連接,數據以定長傳輸(一般定在512字節以內)。每個數據包包括數據塊號和一塊數據,服務器發出下一個數據包以前必須得到客戶對上一個數據包的確認。如果一個數據包的大小小于規定長度,則表示傳輸結束。通信的雙方都是數據的發出者與接收者,一方傳輸數據接收應答,另一方發出應答接收數據。大部分的錯誤會導致連接中斷,錯誤由一個錯誤的數據包引起。這個協議限制很多,這些都是為了實現起來比較方便而進行的。
2.TFTP協議
既然寫TFTP通信,上來最重要的肯定是協議
1:對于數據長度以字節來計算和標識。
2:對于TFTP數據包我這里面只寫了其中五中,對于簡單的開發已經足夠了,操作碼分別對應RRQ
讀請求、WRQ
寫請求、DATA
數據包、ACK
數據包確認、ERROR
差錯包。
3:文件名、模式、差錯信息這些數據長度都是不固定的,文件分成數據塊傳輸,數據塊一般定在0-512個字節之間,太大可能傳輸的就不一定安全,因為TFTP是基于UDP來進行數據傳輸,在數據鏈路層有MTU的限制每個數據包的大小(1500字節)
MTU(1500 byte)-PPP的包頭包尾的開銷(8 byte)-IP頭(20 byte)-UDP頭(8 byte) = 1464(byte)
從上面看出實際每個UDP包數據是在1464字節以內,如果超過這個臨界值,系統內部會對數據包進行分片傳輸,由于UDP數據包的發送和接收都是無確認,讓系統分包去傳送可能存在數據丟失,所以業界一般沒有將數據塊定太大,通用512個字節。
4:文件名和差錯信息最好都用英文字母或者英文字符,一般硬件是低級的單片機,內部存儲空間有限,所以一般里面不一定裝有UTF-8數據編碼表,一般都是ASCII編碼表,摻雜其他字符可能解析不出來,這個編碼方式具體看硬件
3.TFTP通信流程
1.首先服務器綁定固定端口號開始監聽客戶端的連接
2.一切從客戶端發送的第一個數據包開始,里面包含有讀文件還是寫文件的操作碼,需要操作的文件名
3.服務器找到對應的文件,開始分成塊傳送給設備
4.設備收到對應的數據塊后回應個服務器確認包,里面包含確認塊號,告訴服務器這塊我收到了你可以傳下塊了
5.直到最后一個分包傳給設備,設備根據包的大小和規定分片的大小做對比,如果小于規定的分片則表示是最后一個數據包,向服務器發送一個ACK確認包,然后關閉連接,服務器收到最后一個確認的ACK后也關掉自己的Socket,本次傳輸完成;如果最后一個包正好也是分片的大小,服務器接下來還得傳輸一個操作碼后面數據長度為0的數據包過去,這樣客戶端才知道沒有數據了
4.代碼實現部分
話不多說,先看Demo效果(效果圖為GIF動態圖,動畫只執行一次,看不到效果可以刷新下頁面重新播放)
(1)服務器
① 首先初始化套接字,并綁定到指定端口(我這里用傳進來的一個端口號,并沒有寫成固定的69),同時檢查創建的套接字的讀寫能力(下面要用這個套接字監聽設備數據返回和向設備發送數據)
//套接字初始化(Create Socket)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd <= 0) {
[self throwErrorWithCode:errno reason:@"Failed to create socket"];
return;
}
//綁定監聽地址
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;
}
②監聽客戶端的連接,直到有數據請求,同時設置等待超時時間為30s(30s內沒有連接則關閉套接字) ->如果有連接則進入步驟③,數據解析
//申明一個接受客戶端連接套接字的地址
struct sockaddr_in addr_clict;
socklen_t addr_clict_len = sizeof(struct sockaddr_in);
addr_clict.sin_len = addr_clict_len;
//設置接收請求連接超時時間為30s
struct timeval timeout = {30,0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
printf("開始設置Socket服務器接收連接超時失敗: %s\n",strerror(errno));
}
while (1) {
if (_isOpen == NO) return; //服務器關閉直接退出
char recv_buffer[1024]; //接收數據緩沖區
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; //數據包長度必須大于或等于4,否則不是我們想要的數據
//有接收到數據,進入下面的數據解析部分 ->③
}
③有接收到數據,開始解析數據是否是客戶端的連接請求,如果是客戶端的連接請求則‘連接’該套接字,實際上叫注冊更準確,這里的connect()
并不創建實質意義上的連接,只是向套接字中注冊目的地址信息,方便后面數據的收發(不用每次發送和接收數據都注冊地址信息),接著解析出請求文件名并將該文件加載到緩存,開始下面的數據傳輸
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. 初始化一些數據
_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;
}
//一切準備就緒,開始傳輸數據 ->④
[self beganToTransportData];
return;
}
④開始向客戶端地址傳送數據,發送第一個數據包,記錄下發送文件的長度并且判斷是否是最后一個數據包(數據包拼接部分很簡單,按照上面的協議來拼接數據,這里就不貼代碼了,具體的自己可以下載下面的demo看),并且設置套接字接收數據超時時間為6s,防止中間傳輸失敗重傳->進入步驟⑤,循環監聽客戶端數據返回
//1. 局部變量的聲明
char recv_buffer[1024]; //接收數據緩沖區
char send_buffer[1024]; //發送數據緩沖區
NSUInteger sendLen = 0; //發送數據的長度
//2. 初始化一些數據
_blocknum = 1;
_alreadySendLen = 0;
int retry = 0; //同一個包重傳次數
BOOL isLastPacket = false; //記錄是否是最后一個數據包
//3. 第一個數據包的發送
sendLen = [TFTPServerPacket makeDataWithTotalData:self.fileData
sendBuffer:send_buffer
location:_alreadySendLen
length:TFTP_BlockSize
blocknum:_blocknum];
if (sendLen < (TFTP_BlockSize + 4)) isLastPacket = YES; //記錄下是發送的最后一個數據包
if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
[self throwErrorWithCode:errno reason:@"Send data error"];
return;
}
_alreadySendLen = sendLen - 4;
//開始傳輸數據時,定個數據包接收超時時間段為6s
struct timeval timeout = {6,0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
printf("設置Socket通信過程中,接收客戶端數據超時失敗:%s\n",strerror(errno));
}
//開始下面的監聽ACK返回,發送接下來的數據包 ->⑤
⑤循環持續讀取輸入緩沖中的數據,如果讀取超時則重發上次的數據包,連續三次超時則拋出錯誤關閉套接字,若收到客戶端的數據 -> 進入步驟⑥,解析數據包
//4. while循環監聽數據包的返回
while (1) {
if (_isOpen == NO) return; //服務器關閉直接退出監聽
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超時,發送差錯包給客戶端");
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] 接收客戶端確認包超時 -> 重傳上次的包(塊號:%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;
}
}
//數據包長度小于4不要
if (result_recv < 4) continue;
//接下來解析客戶端發送回來的數據和對應數據發送還有服務器端操作 ->⑥
}
⑥解析客戶端發送回來的數據,首先確認操作碼(是否是ACK數據包,此時ACK才是我們需要的,錯誤包做下解析和對應的操作,其他的數據包可以忽略),如果是ACK數據包 ->進行步驟⑦,解析ACK數據包并判斷
//先解析操作碼
char opCode = recv_buffer[1];
if (opCode == TFTP_RRQ || opCode == TFTP_WRQ || opCode == TFTP_DATA) {
NSLog(@"[TFTPServer] 客戶端發錯了數據包(操作碼: %d), 不理",opCode);
}else if (opCode == TFTP_ACK) { //收到ACK數據包
//收到設備端的ACK確認包->進行ACK塊號確認,發送對應數據包 ->⑦
}else if (opCode == TFTP_ERROR) {
//客戶端那邊發送過來了錯誤包
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] 客戶端發錯了數據包(操作碼: %d), 不理",opCode);
}
⑦收到客戶端的ACK確認包,首先判斷是否是最后一個數據包的確認塊號,如果是,則關閉服務器(斷掉Socket),本次傳輸完成,如果不是則進行步驟⑧,解析塊號判斷,并做對應的操作
//①. 解析出確認塊號
uint clict_sureblocknum = ((recv_buffer[2]&0xff)<<8)|((recv_buffer[3]&0xff));
//NSLog(@"[TFTPServer] 收到客戶端的ACK數據包,塊號:%d",clict_sureblocknum);
//②. 判斷是否是最后一個包的確認
if (isLastPacket == YES && _blocknum == clict_sureblocknum) { //是最后一個包了
[self sendComplete];
return;
}else {
//不是最后一個數據包的確認塊號,解析出塊號并判斷 ->⑧
}
⑧解析出塊號,與自己已經發送的塊號做對比,如果是剛發的塊號,則確認進行下一個包的發送,如果塊號是上一個數據包的塊號,則進入步驟⑨(數據包重發),否則則為塊號錯亂,退出重新發送
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; //記錄下是發送的最后一個數據包
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塊號不對,進入重發機制
//上一個數據包客戶端接收有誤, 重傳 ->⑨
}else {
NSLog(@"[TFTPServer] 客戶端返回的確認塊號不對 _blocknum:%u clict_sureblocknum:%u",_blocknum,clict_sureblocknum);
[self throwErrorWithCode:1002 reason:@"Request block number error"];
return;
}
⑨如果收到的是上次發送包的確認塊號(數據丟失可能設備沒有收到),則判斷對上個包的重發次數有沒有達到上限,如果達到上限,則向客戶端發送一個差錯包,告訴客戶端此次傳輸有問題,服務器要斷開連接了,如果沒有達到上限則將上次發送的數據包重新發送
retry ++;
if (retry >= MAX_RETRY) {
NSLog(@"[TFTPServer] 接收ACK錯誤次數達到上限,發送差錯包給客戶端");
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] 客戶端發送ACK塊號有誤(塊號:%u), 重傳上次的包(塊號:%u)",_blocknum,clict_sureblocknum);
if (send(_sockfd, send_buffer, sendLen, 0) < 0 && _isOpen) {
[self throwErrorWithCode:errno reason:@"Send data error"];
return;
}
}
(2)客戶端
①創建Socket套接字,綁定固定端口并檢查套接字的讀寫情況,用來接收服務器數據返回
//初始化套接字
_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;
}
②初始化服務器地址信息,并注冊服務器地址信息,方便后面直接接收數據和發送數據,并且設置接讀取輸入緩沖超時時間為6s,防止傳輸過程中出現異常
//注冊套接字目的地址
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;
}
//設置讀取數據超時
struct timeval timeout = {6, 0};
if (setsockopt(_sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(struct timeval)) < 0) {
printf("[TFTPClient] 設置接收數據超時失敗:%s",strerror(errno));
}
③開始向服務器發送第一個數據包,文件請求數據包
//1. 初始化一些變量
char sendBuffer[1024]; //發送數據緩存區
NSUInteger sendLen; //發送數據長度
char recvBuffer[1024]; //接收數據緩存區
_blocknum = 0; //接收塊號記錄
self.fileData.length = 0; //接收文件緩存區
int retry = 0; //記錄同一個包的請求次數
BOOL isLastPacket = false; //記錄是否是最后一個數據包
//2. 發送文件請求包
sendLen = [TFTPClientPacket makeRRQWithFileName:filename
sendBuffer:sendBuffer];
if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
[self throwErrorWithCode:errno reason:@"Read data error"];
return;
}
//開始監聽服務器數據發送了 -> ④
④循環監聽服務器發送數據,首先判斷是否讀取超時,如果超時則重發上一次的確認塊,連續三次超時則拋出錯誤關閉套接字。如果收到數據則判斷數據長度是否滿足自己的需求,都滿足則進行接下來的解析->步驟⑤,不滿足跳過本地循環讀取,進行下次讀取
//3. 開始監聽數據返回
while (1) {
ssize_t result_recv = recv(_sockfd, recvBuffer, sizeof(recvBuffer), 0);
if (result_recv < 0 && _isOpen) {
if (errno == EAGAIN) { //讀取數據超時
retry++;
if (retry >= MAX_RETRY) {
NSLog(@"[TFTPClient] 請求超時,發送差錯包給服務器");
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 {
//重發上一個ACK確認包
NSLog(@"[TFTPClient] 客戶端請求數據塊超時,重發上個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;
}
}
//數據長度過短或不是我們需要服務器地址發送過來的數據都不是我們想要的數據, 直接丟掉
if (result_recv < 4) continue;
//是自己需要的數據,解析出操作碼,進行相應的操作 ->⑤
}
⑤首先解析出接收到數據包的操作碼,判斷是否是DATA
對應的操作碼,如果不是拋出對應的信息和進行對應的操作,如果是則進行接下來的操作->步驟⑥,塊號的確認
//解析操作碼
char opCode = recvBuffer[1];
if (opCode == TFTP_RRQ || opCode == TFTP_WRQ || opCode == TFTP_ACK) {
NSLog(@"[TFTPClient] 服務器發送了錯誤數據包(操作碼: %d),不理",opCode);
}else if (opCode == TFTP_DATA) {
/* 服務器發送過來數據包 */
//進行接下來的數據解析和拼接,發送給確認包 ->⑥
}else if (opCode == TFTP_ERROR) {
NSString *errStr = [[NSString alloc] initWithBytes:&recvBuffer[4] length:result_recv-4 encoding:NSUTF8StringEncoding];
NSLog(@"[TFTPClient] 服務器傳送過來差錯信息: 錯誤碼 -> %u 錯誤信息 -> %@",(((recvBuffer[2] & 0xff) << 8) | (recvBuffer[3] & 0xff)),errStr);
[self throwErrorWithCode:(((recvBuffer[2] & 0xff) << 8) | (recvBuffer[3] & 0xff)) reason:errStr];
return;
}else {
NSLog(@"[TFTPClient] 服務器傳過來不知名的數據包(操作碼: %d)",opCode);
}
⑥解析服務器發送過來的數據包,拿到塊號和自己這邊記錄的塊號做對比,如果塊號正確則向服務器發送本次的塊號確認ACK包,并把數據拼接到緩存,如果收到數據包塊號不正確,則進行步驟⑦重確認操作
//解析出塊號, 與自己的塊號作比較, 看看服務器有沒有發錯
uint blocknum = (recvBuffer[2]&0xff)<<8 | (recvBuffer[3]&0xff);
//NSLog(@"[TFTPClient] 服務器發送過來數據包,塊號:%u",blocknum);
if (blocknum == (_blocknum + 1)) {
retry = 0;
_blocknum = blocknum;
//解析數據包, 并且判斷是否是最后一個數據包
NSData *data = [NSData dataWithBytes:&recvBuffer[4] length:result_recv-4];
[self.fileData appendData:data];
if (data.length < TFTP_BlockSize) isLastPacket = YES;
//發送ACK確認包
sendLen = [TFTPClientPacket makeACKWithBlockNum:_blocknum sendBuffer:sendBuffer];
if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
[self throwErrorWithCode:errno reason:@"Send data error"];
return;
}
}else {
//塊號不對,進入重發機制
//傳送數據塊號不對,進行重新確認 -> ⑦
}
⑦服務器發送的數據塊號和自己將要接收的塊號對不上,拿到自己最后一個接收到的數據包塊號,先判斷對該數據包的確認次數有沒有達到上限,達到上限則向服務器發送差錯包,表示本次傳輸出錯,并關掉套接字,如果沒有達到上限,那么重發這個確認塊號,向服務器確認得到正確的數據塊
retry++;
if (retry >= MAX_RETRY) {
NSLog(@"[TFTPClient] 接收數據包錯誤次數達到上限,發送差錯包給客戶端");
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] 服務器發送塊號不對(塊號:%u), 重發送上個ACK確認包(塊號:%u)",blocknum,_blocknum);
if (send(_sockfd, sendBuffer, sendLen, 0) < 0 && _isOpen) {
[self throwErrorWithCode:errno reason:@"Send data error"];
return;
}
}
⑧確認包發送完或者確認包重發完,判斷是否是最后一個數據包的確認,如果是則關閉Socket,本次傳輸完成,不是則跳過接著監聽
if (isLastPacket) { //就收完成
[self recevComplete];
return;
}
5.結語
上面的代碼部分只是一個大概的思路講解,主要為了方便理解demo里面的代碼邏輯,具體的還是要看demo。