前面我們已經實現了UDP的迴環客戶端和迴環服務器的簡單應用,接下來我們實現一個基於UDP的簡單文件傳輸協議TFTP。
1、TFTP協議簡介
TFTP是TCP/IP協議族中的一個用來在客戶機與服務器之間進行簡單文件傳輸的協議,提供不復雜、開銷不大的文件傳輸服務。端口號爲69
TFTP是一種簡單的文件傳輸協議。目標是在UDP之上上建立一個類似於FTP的但僅支持文件上傳和下載功能的傳輸協議,所以它不包含FTP協議中的目錄操作和用戶權限等內容。
TFTP報文的頭兩個字節表示操作碼,共有5中操作碼,如下表:
讀請求和寫請求功能碼的數據報文格式是一樣的,所以TFTP報文又可表述爲4種形式。對於讀請求或者寫請求,文件名字段說明客戶要讀或寫的位於服務器的上的文件並以0字節作爲結束,模式字段是一個ASCII碼串,同樣以0字節結束。讀請求和寫請求的報文格式:
其次是數據包,起包括2個字節的塊編號以及0-512個字節的數據信息。數據包相對比較簡單,其報文格式:
再者爲確認包。確認包也有2個字節的塊編號。其數據格式:
最後一種TFTP報文類型是差錯報文,它的操作碼爲5.它用於服務器不能處理讀請求或者寫請求的情況。在文件傳輸的過程中的讀和寫也會導致傳送這種報文,接着停止傳輸。錯誤包的報文格式:
TFTP的工作過程很像停止等待協議,發送完一個文件塊後就等待對方的確認,確認時應指明所確認的塊號。發送完數據後在規定時間內收不到確認就要重發數據PDU,發送確認PDU的一方若在規定時間內收不到下一個文件塊,也要重發確認PDU。這樣保證文件的傳送不致因某一個數據報的丟失而告失敗。
2、TFTP協議棧設計
前面我們簡單的介紹了TFTP協議,接下來我們看看該如何實現其編程。它有5種操作碼,我們要做的就是實現對這5種操作碼的響應。
2.1、讀請求實現
所謂讀請求,就是客戶端請求從服務器獲取文件,那麼服務器需要做的自然是響應客戶端的請求。但我們並沒有文件,所以不管它請求什麼文件,我們均給它返回內容和大小相同的測試文件。
/* TFTP讀請求處理*/
int TftpReadProcess(struct udp_pcb *upcb, const ip_addr_t *to, int to_port, char* FileName)
{
tftp_connection_args *args = NULL;
/* 這個函數在回調函數中被調用,因此中斷被禁用,因此我們可以使用常規的malloc */
args = mem_malloc(sizeof(tftp_connection_args));
if (!args)
{
/* 內存分配失敗 */
SendTftpErrorMessage(upcb, to, to_port, TFTP_ERR_NOTDEFINED);
CleanTftpConnection(upcb, args);
return 0;
}
/* i初始化連接結構體 */
args->op = TFTP_RRQ;
args->remote_port = to_port;
args->block = 1;
/* 塊號從1開始 */
args->tot_bytes = 10*1024*1024;
/* 註冊回調函數 */
udp_recv(upcb, RrqReceiveCallback, args);
/* 通過發送第一個塊來建立連接,後續塊在收到ACK後發送*/
SendNextBlock(upcb, args, to, to_port);
return 1;
}
2.2、寫請求實現
寫請求就是客戶端希望向服務器傳送文件,在這裏我們只是實現TFTP服務器的功能,沒必要將收到的文件真正保存到一個地方,所以只是做接收文件的過程並不將其寫到存儲器,簡單的說就是隻在內存中而不會寫入Flash等。
/* TFTP寫請求處理 */
int TftpWriteProcess(struct udp_pcb *upcb, const ip_addr_t *to, int to_port, char *FileName)
{
tftp_connection_args *args = NULL;
/* 這個函數在回調函數中被調用,因此中斷被禁用,因此我們可以使用常規的malloc */
args = mem_malloc(sizeof(tftp_connection_args));
if (!args)
{
SendTftpErrorMessage(upcb, to, to_port, TFTP_ERR_NOTDEFINED);
CleanTftpConnection(upcb, args);
return 0;
}
args->op = TFTP_WRQ;
args->remote_port = to_port;
args->block = 0; //WRQ響應的塊號爲0
args->tot_bytes = 0;
/* 爲控制塊註冊回調函數 */
udp_recv(upcb, WrqReceiveCallback, args);
/* 通過發送第一個ack來發起寫事務 */
SendTftpAckPacket(upcb, to, to_port, args->block); return 0;
}
2.3、數據包操作
無論是讀請求還是寫請求,最終的目的無非是要傳送數據,所以數據包自然也是我們需要構造和傳送的。其對應的就是數據包操作碼,我們設計程序如下:
/* 構造並且傳送數據包 */
static int SendTftpDataPacket(struct udp_pcb *upcb, const ip_addr_t *to, int to_port, int block,char *buf, int buflen)
{
/* 將開始的2個字節設置爲功能碼 */
SetTftpOpCode(buf, TFTP_DATA);
/* 將後續2個字節設置爲塊號 */
SetTftpBlockNumber(buf, block);
/* 在後續設置n各字節的數據 */
/* 發送數據包 */
return SendTftpMessage(upcb, to, to_port, buf, buflen + 4);
}
2.4、確認包操作
在傳送數據包後,收到沒收到,發送方是不知道的,怎麼辦呢?這時候接受方接收到後,會給出一個確認包。其對應的就是確認操作碼,那麼我們還需實現確認包的構造和發送。
/*構造併發送確認包*/
int SendTftpAckPacket(struct udp_pcb *upcb,const ip_addr_t *to, int to_port, int block)
{
/* 創建一個TFTP ACK包 */
char packet[TFTP_ACK_PKT_LEN];
/* 將開始的2個字節設置爲功能碼 */
SetTftpOpCode(packet, TFTP_ACK);
/* 制定ACK的塊號 */
SetTftpBlockNumber(packet, block);
return SendTftpMessage(upcb, to, to_port, packet, TFTP_ACK_PKT_LEN);
}
2.5、錯誤包操作
在包傳送的過程中,有沒有可能出現錯誤呢?當然是有的,這就需要所謂的錯誤包操作碼。在服務器不能處理讀請求或者寫請求的情況下。在文件傳輸的過程中的讀和寫也會導致傳送這種報文,接着停止傳輸。我們也需要開發構造和傳送錯誤包的函數。
/* 構造並向客戶端發送一條錯誤消息 */
static int SendTftpErrorMessage(struct udp_pcb *upcb, const ip_addr_t *to, int to_port, tftp_errorcode err)
{
char buf[512];
int error_len;
error_len = ConstructTftpErrorMessage(buf, err);
return SendTftpMessage(upcb, to, to_port, buf, error_len);
}
3、TFTP服務器實現
我們已經實現了UDP服務器,而且也實現了簡單的TFTP協議棧,接下來的工作就是在UDP基礎上實現TFTP服務器功能。前面我們已經提到過,複雜的服務器應用只是回到函數的功能不一樣,所以開發的過程並無區別。
首先我們來實現初始化部分。創建新的UDP控制塊。綁定到制定的服務器端口,我們要實現TFTP服務器,而TFTP協議的端口號爲69,所以我們將其綁定到該端口。最後註冊TFTP服務器的回調函數。
/* 初始化TFTP服務器 */
void Tftp_Server_Initialization(void)
{
err_t err;
struct udp_pcb *tftp_server_pcb = NULL;
/* 生成新的 UDP PCB控制塊 */
tftp_server_pcb = udp_new();
/* 判斷UDP控制塊是否正確生成 */
if (NULL == tftp_server_pcb)
{
return;
}
/* 綁定PCB控制塊到指定端口 */
err = udp_bind(tftp_server_pcb, IP_ADDR_ANY, UDP_TFTP_SERVER_PORT);
if (err != ERR_OK)
{
udp_remove(tftp_server_pcb);
return;
}
/* 註冊TFTP服務器處理函數 */
udp_recv(tftp_server_pcb, TftpServerCallback, NULL);
}
在初始化中註冊了回調函數,所以我們還要實現TFTP服務器的回調函數。這部分出於結構清晰的考慮,我們分成兩個函數來寫。
/* TFTP服務器回調函數 */
static void TftpServerCallback(void *arg, struct udp_pcb *upcb, struct pbuf *p,const ip_addr_t *addr, u16_t port)
{
/* 處理新的連接請求 */
ProcessTftpRequest(p, addr, port);
pbuf_free(p);
}
/* 從每一個來自addr:port的新請求創建一個新的端口來服務響應,並啓動響應過程 */
static void ProcessTftpRequest(struct pbuf *pkt_buf, const ip_addr_t *addr, u16_t port)
{
tftp_opcode op = ExtractTftpOpcode(pkt_buf->payload);
char FileName[50] = {0};
struct udp_pcb *upcb = NULL;
err_t err;
/* 生成新的UDP PCB控制塊 */
upcb = udp_new();
if (!upcb)
{
return;
}
/* 連接 */
err = udp_connect(upcb, addr, port);
if (err != ERR_OK)
{
return;
}
ExtractTftpFilename(FileName, pkt_buf->payload);
switch (op)
{
case TFTP_RRQ:
{
TftpReadProcess(upcb, addr, port, FileName);
break;
}
case TFTP_WRQ:
{
/* 啓動TFTP寫模式 */
TftpWriteProcess(upcb, addr, port, FileName);
break;
}
default:
{
/* 異常,發送錯誤消息 */
SendTftpErrorMessage(upcb, addr, port, TFTP_ERR_ACCESS_VIOLATION);
udp_remove(upcb);
break;
}
}
}
在回調函數中,我們實現了對TFTP讀請求和寫請求的響應,但這足以驗證我們想要實現的TFTP服務器的功能。
4、結論
本篇我們基於LwIP的UDP實現了一個簡單的FTP服務器。這個FTP服務器只是實現FTP協議的功能,具體的應用可根據需要添加。我們使用了TFTP客戶端工具對這一服務器進行了基本測試,最終結果符合我們的預期。