目录
本篇文章,我们将详细介绍如何在W55MH32芯片上面实现TFTP协议。并通过实战例程,为大家讲解如何使用TFTP客户端模式向服务器获取文本文件。
该例程用到的其他网络协议,例如DHCP请参考相关章节。有关W55MH32的初始化过程,也请参考Network Install章节,这里将不再赘述。
1 TFTP协议简介
TFTP(Trivial File Transfer Protocol)协议是一种轻量级的文件传输协议,它通常用于需要快速、简单的文件交换场景,尤其是在网络设备启动和配置过程中。与FTP(文件传输协议)不同,TFTP设计得非常简单,仅提供基本的文件读写功能,并且使用UDP作为传输层协议,因而不具备TCP的复杂性和重传机制。
2 TFTP协议特点
简单性:TFTP协议设计简单,它的报头格式简洁,操作命令种类少,这使得实现起来相对容易,对资源的需求也较低。
轻量级:TFTP协议不需要复杂的连接建立和管理过程,开销小,因此适合在一些对性能要求不高、资源有限的环境中使用。
基于UDP:TFTP使用UDP作为传输层协议,利用了UDP的快速传输和无连接特性,从而能够快速地传输数据。不过,这也意味着TFTP本身不提供可靠的传输保证,需要在应用层实现可靠性机制。
端口固定:TFTP使用固定的端口69来监听客户端的请求。数据传输使用的端口是动态分配的,每次传输会在此基础上选择一个临时端口。
数据块大小限制:每个数据报文最多只能传输512字节的数据,如果文件较大,会分多次传输,每次发送一个512字节的数据块。最后一个数据块可能小于512字节,表示文件的结束。
3 TFTP协议应用场景
接下来,我们了解下在W55MH32上,可以使用TFTP协议完成哪些操作及应用呢?
- 固件升级:对于路由器、交换机等网络设备,TFTP协议常用于将固件传输到这些设备以进行固件更新。TFTP协议能够确保固件文件快速、准确地传输到目标设备。
- 配置文件传输:TFTP协议也常用于管理网络设备的配置文件。将配置文件传输到网络设备以进行 配置更新,或者从网络设备下载配置文件进行备份或分析。
- IOT设备固件升级:TFTP协议因其简单性和高效性,成为IOT设备固件升级的一种常用协议。
4 TFTP协议基本工作流程
- 请求发送:客户端向服务器发送读请求(RRQ,Read Request)或写请求(WRQ,Write Request)。这些请求包含了要读取或写入的文件名以及传输模式(如二进制或ASCII码)。
- 建立连接:服务器接收到客户端的请求后,根据请求中的文件名和传输模式,打开相应的文件(对于写请求)或准备发送文件数据(对于读请求),并向客户端发送确认信息,从而建立连接。
- 数据传输:在写请求的情况下,客户端开始发送文件数据到服务器,服务器接收并写入文件。数据以数据块的形式发送,每个数据块的大小通常为512字节(但可以根据网络状况调整)。
在读请求的情况下,服务器开始发送文件数据到客户端,客户端接收并保存文件。同样,数据也是以数据块的形式发送的。
- 回应与确认:每当客户端或服务器发送一个数据块后,接收方会发送一个回应包(ACK,Acknowledgment)来确认接收到了该数据块。这个回应包包含了接收到的数据块的编号,以确保数据的顺序和完整性。
- 继续传输或结束:根据回应包,发送方会继续发送下一个数据块,直到整个文件传输完成。如果传输过程中出现错误,服务器会向客户端发送错误信息包(ERROR),中断传输过程。
- 关闭连接:文件传输完成后,客户端和服务器会关闭连接。
5 TFTP协议报文解析
常见的操作码:
1:读请求(RRQ),用于请求读取服务器上的文件。
2:写请求(WRQ),用于请求向服务器上写入文件。
3:数据(DATA),用于传输文件数据。
4:回应(ACK),用于确认接收到的数据块。
5:错误信息(ERROR),用于报告传输过程中发生的错误。
常见操作码的报文格式如下:
报文类型 | 报文格式 | 操作码 | 其他关键字段及说明 |
读请求(RRQ) | 总长可变,由2字节操作码、可变长文件名(以1字节0结尾)、可变长传输模式(以1字节 0 结尾)组成 | 1 | 文件名:明确要读取的文件名称 |
写请求(WRQ) | 总长可变,由2字节操作码、可变长文件名(以1字节0结尾)、可变长传输模式(以1字节 0 结尾)组成 | 2 | 文件名:明确要读取的文件名称 |
数据(DATA) | 由2字节操作码、2字节数据块编号、最多512字节数据组成 | 3 | 数据块编号:从1开始,用于标识数据块顺序 |
确认(ACK) | 由2字节操作码和2字节确认的数据块编号组成 | 4 | 数据块编号:与接收到的数据块编号一致,用于确认接收 |
错误(ERROR) | 由2字节操作码、2字节错误码、可变长错误信息(以 1字节0结尾)组成 | 5 | 错误码:明确错误类型 |
报文示例:
客户端读请求报文:
|报文解析|
Trivial File Transfer Protocol
Opcode: Read Request (1) (操作码为01,读请求报文)
Source File: tftp_test_file.txt (明确要读取的文件名为tftp_test_file.txt)
Type: octet (传输模式为octet)
Option: timeout = 5
|报文原文|
00 01 74 66 74 70 5f 74 65 73 74 5f 66 69 6c 65 2e 74 78 74 00 6f 63 74 65 74 00 74 69 6d 65 6f 75 74 00 35 00
服务器响应报文:
|报文解析|
Trivial File Transfer Protocol
Opcode: Option Acknowledgement (6) (操作码为06,扩展操作码)
[Destination File: tftp_test_file.txt]
[Read Request in frame 125]
Option: timeout = 5
|报文原文|
00 06 74 69 6d 65 6f 75 74 00 35 00
客户端响应报文:
|报文解析|
Trivial File Transfer Protocol
Opcode: Acknowledgement (4) (操作码为04,回应报文)
[Destination File: tftp_test_file.txt]
[Read Request in frame 125]
Block: 0 (数据块标号为00 00)
[Full Block Number: 0]
|报文原文|
00 04 00 00
服务器响应报文:
|报文解析|
Trivial File Transfer Protocol
Opcode: Data Packet (3) (操作码为03,数据报文)
[Destination File: tftp_test_file.txt]
[Read Request in frame 125]
Block: 1 (数据块标号为00 01)
[Full Block Number: 1]
Data (36 bytes)
Data: 736461666173646661736466617364666666666666666641617364666173666166736466 (数据)
[Length: 36]
|报文原文|
00 03 00 01 73 64 61 66 61 73 64 66 61 73 64 66 61 73 64 66 66 66 66 66 66 66 66 41 61 73 64 66 61 73 66 61 66 73 64 66
客户端响应报文:
|报文解析|
Trivial File Transfer Protocol
Opcode: Acknowledgement (4) (操作码为04,回应报文)
[Destination File: tftp_test_file.txt]
[Read Request in frame 125]
Block: 1 (数据块标号为00 01)
[Full Block Number: 1]
|报文原文|
00 04 00 01
6 实现过程
接下来,我们在W55MH32上实现TFTP协议读取文件。
注意:测试实例需要PC端和W55MH32处于同一网段。
在主函数中调用do_tftp_client()函数不断检查和处理 TFTP 客户端的状态,并根据读取的结果(成功或失败)进入相应的处理状态。
do_tftp_client(SOCKET_ID, ethernet_buf);
do_tftp_client()函数如下:
void do_tftp_client(uint8_t sn, uint8_t *buff)
{
uint32_t tftp_server_ip = inet_addr(TFTP_SERVER_IP);
uint8_t tftp_read_file_name[] = TFTP_SERVER_FILE_NAME;
TFTP_init(sn, buff);
while (1)
{
if (tftp_read_flag == 0)
{
printf("tftp server ip: %s, file name: %s\r\n", TFTP_SERVER_IP, TFTP_SERVER_FILE_NAME);
printf("send request\r\n");
TFTP_read_request(tftp_server_ip, TFTP_SERVER_FILE_NAME);
tftp_read_flag = 1;
}
else
{
tftp_state = TFTP_run();
if (tftp_state == TFTP_SUCCESS)
{
printf("tftp read success, file name: %s\r\n", tftp_read_file_name);
while (1)
{
}
}
else if (tftp_state == TFTP_FAIL)
{
printf("tftp read fail, file name: %s\r\n", tftp_read_file_name);
while (1)
{
}
}
}
}
}
进入do_tftp_client()函数后开始进行TFTP客户端处理,步骤如下:
步骤一:TFTP初始化
调用TFTP_init()函数对TFTP客户端进行初始化,参数sn和buff分别是socket号,socket缓存。
1. void TFTP_init(uint8_t socket, uint8_t *buf)
2. {
3. init_tftp();
4.
5. g_tftp_socket = open_tftp_socket(socket);
6. g_tftp_rcv_buf = buf;
7. }
static void init_tftp(void)
{
g_filename[0] = 0;
set_server_ip(0);
set_server_port(0);
set_local_port(0);
set_tftp_state(STATE_NONE);
set_block_number(0);
// timeout flag
g_resend_flag = 0;
tftp_retry_cnt = tftp_time_cnt = 0;
g_progress_state = TFTP_PROGRESS;
}
步骤二:发送 TFTP 读请求
当tftp_read_flag为 0 时,表示尚未发送读取请求。此时,打印 TFTP 服务器的 IP 地址和要读取的文件名,然后调用TFTP_read_request()函数向服务器发送读取请求。发送请求后,将tftp_read_flag设置为 1,表示已发送请求。
void TFTP_read_request(uint32_t server_ip, uint8_t *filename)
{
set_server_ip(server_ip);
#ifdef __TFTP_DEBUG__
DBG_PRINT(INFO_DBG, "[%s] Set Tftp Server : %x\r\n", __func__, server_ip);
#endif
g_progress_state = TFTP_PROGRESS;
send_tftp_rrq(filename, (uint8_t *)TRANS_BINARY, &default_tftp_opt, 1);
}
步骤三:运行 TFTP 协议并处理结果
当 tftp_read_flag 为 1 时调用 TFTP_run()函数处理 TFTP 协议操作,依据其返回的 tftp_state 判断结果:若为 TFTP_SUCCESS 则打印成功信息并进入无限循环,若为 TFTP_FAIL 则打印失败信息并进入无限循环。
TFTP_run()函数如下:
int TFTP_run(void)
{
int len;
uint16_t from_port;
uint32_t from_ip;
/* Timeout Process */
if (g_resend_flag)
{
if (tftp_time_cnt >= g_timeout)
{
switch (get_tftp_state())
{
case STATE_WRQ:
break;
case STATE_RRQ:
send_tftp_rrq(g_filename, (uint8_t *)TRANS_BINARY, &default_tftp_opt, 1);
break;
case STATE_OACK:
case STATE_DATA:
send_tftp_ack(get_block_number());
break;
case STATE_ACK:
break;
default:
break;
}
tftp_time_cnt = 0;
tftp_retry_cnt++;
if (tftp_retry_cnt >= 5)
{
init_tftp();
g_progress_state = TFTP_FAIL;
}
}
}
/* Receive Packet Process */
len = recv_udp_packet(g_tftp_socket, g_tftp_rcv_buf, MAX_MTU_SIZE, &from_ip, &from_port);
if (len < 0)
{
#ifdef __TFTP_DEBUG__
DBG_PRINT(ERROR_DBG, "[%s] recv_udp_packet error\r\n", __func__);
#endif
return g_progress_state;
}
recv_tftp_packet(g_tftp_rcv_buf, len, from_ip, from_port);
return g_progress_state;
}
在处理接收到的TFTP数据包时,首先调用recv_tftp_packet()函数。
recv_tftp_packet()函数如下:
static void recv_tftp_packet(uint8_t *packet, uint32_t packet_len, uint32_t from_ip, uint16_t from_port)
{
uint16_t opcode;
/* Verify Server IP */
if (from_ip != get_server_ip())
{
#ifdef __TFTP_DEBUG__
DBG_PRINT(ERROR_DBG, "[%s] Server IP faults\r\n", __func__);
DBG_PRINT(ERROR_DBG, "from IP : %08x, Server IP : %08x\r\n", from_ip, get_server_ip());
#endif
return;
}
opcode = ntohs(*((uint16_t *)packet));
/* Set Server Port */
if ((get_tftp_state() == STATE_WRQ) || (get_tftp_state() == STATE_RRQ))
{
set_server_port(from_port);
#ifdef __TFTP_DEBUG__
DBG_PRINT(INFO_DBG, "[%s] Set Server Port : %d\r\n", __func__, from_port);
#endif
}
switch (opcode)
{
case TFTP_RRQ: /* When Server */
recv_tftp_rrq(packet, packet_len);
break;
case TFTP_WRQ: /* When Server */
recv_tftp_wrq(packet, packet_len);
break;
case TFTP_DATA:
recv_tftp_data(packet, packet_len);
break;
case TFTP_ACK:
recv_tftp_ack(packet, packet_len);
break;
case TFTP_OACK:
recv_tftp_oack(packet, packet_len);
break;
case TFTP_ERROR:
recv_tftp_error(packet, packet_len);
break;
default:
// Unknown Mesage
break;
}
}
进入该函数后,第一步验证接收到的数据包的源IP地址,只有当它与服务器IP地址一致时才继续处理,若不一致则直接返回。接着,从数据包中获取操作码(opcode)。根据获取到的操作码,调用相应的处理函数:如果是TFTP读请求(RRQ),则调用 recv_tftp_rrq()函数;若是写请求(WRQ),则调用recv_tftp_wrq()函数;对于接收到的数据数据包,调用 recv_tftp_data()函数;确认数据包则由recv_tftp_ack()函数处理;OACK 数据包由recv_tftp_oack()函数处理;若遇到错误数据包,调用recv_tftp_error()函数来解析错误代码和错误信息。最后,返回g_progress_state,以此表示当前TFTP操作的状态。
7 运行结果
烧录例程运行后,首先进行了PHY链路检测,然后是DHCP获取网络地址结果,最后打印服务器IP和文本名称,读取文本内容,如下图所示:
8 总结
本文讲解了如何在 W55MH32 芯片上实现 TFTP 协议,通过实战例程详细展示了使用 TFTP 客户端模式从服务器获取文本文件的过程,涵盖 TFTP 初始化、发送读请求、运行协议并处理结果等核心步骤。文章还对 TFTP 协议的简介、特点、应用场景、基本工作流程和报文解析进行了分析,帮助读者理解其在文件传输中的实际应用价值。
下一篇文章将聚焦 SNMP 协议,解析其核心原理及在网络管理中的应用,同时讲解如何在相关设备上实现 SNMP 功能,敬请期待!