FTP Server
本篇文章,我们将详细介绍如何在W5500芯片上实现FTP协议。并通过实战例程,为大家讲解使用W5500作为FTP服务器、PC端作为FTP客户端进行文件传输、目录操作等多种功能。
该例程用到的其他网络协议,例如DHCP,请参考相关章节。有关初始化过程,请参考Network Install章节,这里将不再赘述。
FTP协议简介
FTP(File Transfer Protocol,文件传输协议)是遵循文件传输协议(FTP),在网络中提供文件传输服务的重要组件。它采用客户端 - 服务器架构,工作模式分为主动和被动。主动模式下,服务器主动发起数据连接;被动模式中,由客户端发起数据连接。用户访问时,需进行身份验证,常见方式是输入用户名和密码,部分服务器也支持匿名登录。
FTP协议特点
- 基于 TCP 传输:
- 分离控制与数据:
- 控制连接用于发送命令和接收响应。
- 数据连接用于文件内容或目录信息的传输。
- 支持多种传输模式:
- 主动模式(Active Mode):服务器主动连接客户端的数据端口。
- 被动模式(Passive Mode):客户端主动连接服务器提供的数据端口,解决 NAT 防火墙限制。
- 支持多种操作方式:
- 文件上传(STOR)、下载(RETR)、删除(DELE)。
- 目录操作(MKD、RMD、CWD、PWD)。
- 获取文件列表(LIST、NLST)。
- 明文传输(传统 FTP):
- 灵活的用户认证机制:
- 支持匿名登录。
- 支持认证用户名和密码。
FTP 使用两个 TCP 连接:控制连接(端口 21)和数据连接(端口 20 或 PASV 模式动态分配的端口),确保可靠的数据传输。
用户名、密码及数据以明文形式传输,不安全。安全改进:FTPS(FTP Secure,基于 SSL/TLS)和 SFTP(Secure File Transfer Protocol,基于 SSH)。
FTP协议应用场景
接下来,我们了解下在W5500上,可以使用FTP协议完成哪些操作及应用呢?
- 固件升级:嵌入式设备通过 FTP 下载新固件或软件更新包进行系统升级。适用于需要定期更新功能的设备,如路由器、工业控制设备等。
- 数据采集与传输:嵌入式设备(如传感器节点或数据记录器)将采集的数据上传至远程服务器进行存储和分析。例如:环境监测设备将温湿度数据上传到服务器。
- 远程配置与日志管理:设备通过 FTP 下载配置文件或上传日志信息供管理员分析和排错。适用于工业自动化设备和嵌入式监控系统。
- 嵌入式 Web 服务器的文件管理:许多嵌入式设备内置简易 Web 服务器,用于文件存储或内容分发,通过 FTP 管理这些文件资源。
FTP协议基本工作流程
- 建立控制连接
- 客户端初始化:客户端启动 FTP 客户端程序,指定要连接的 FTP 服务器的地址和端口号(端口号为 21)。
- TCP 连接建立:客户端通过 TCP 协议向服务器的 21 端口发起连接请求。服务器监听该端口,接收到请求后,与客户端建立起一条 TCP 连接,这个连接被称为控制连接,主要用于传输 FTP 命令和服务器的响应信息。
- 身份验证:连接建立后,服务器会提示客户端输入用户名和密码进行身份验证。客户端发送相应的用户名和密码信息到服务器,服务器验证通过后,才允许客户端进行后续操作。也有一些匿名 FTP 服务器,允许用户以 “anonymous” 作为用户名,以电子邮件地址作为密码进行登录,提供公开的文件访问服务 。
- 传输模式选择
- 主动模式(PORT 模式):客户端通过控制连接告诉服务器自己的数据端口(客户端随机开放的一个端口),服务器使用 20 端口主动连接客户端的数据端口来传输数据。
- 被动模式(PASV 模式):客户端发送 PASV 命令给服务器,服务器在控制连接上告知客户端自己开放的一个临时数据端口(通常是 1024 以上的端口),然后客户端使用自己的一个随机端口连接服务器的这个临时数据端口来传输数据。
- 数据传输
- 上传文件:客户端向服务器发送 STOR(存储)命令,然后通过数据连接将本地文件数据发送到服务器。服务器接收到数据后,将其存储在指定的目录下。
- 下载文件:客户端向服务器发送 RETR(检索)命令,请求下载服务器上的文件。服务器通过数据连接将文件数据发送给客户端,客户端接收数据并将其保存到本地指定位置。
- 目录操作:客户端还可以发送诸如LIST(列出目录内容)、CWD(更改工作目录)、MKD(创建目录)、RMD(删除目录)等命令,服务器执行相应操作,并通过控制连接返回操作结果。执行这些命令时,若需要传输目录列表等数据,也会通过数据连接进行传输。
- 关闭连接
- 数据连接关闭:在完成文件传输或其他操作后,数据连接会被关闭。如果还有其他操作需要进行,客户端和服务器可以根据需要重新建立数据连接。
- 控制连接关闭:当客户端完成所有操作后,会向服务器发送 QUIT 命令,服务器接收到该命令后,会关闭控制连接。至此,客户端与服务器之间的 FTP 会话结束。
客户端和服务器在控制连接上协商数据传输模式,主要有两种模式:
根据用户的操作需求,通过数据连接进行文件或目录相关操作:
主动模式与被动模式详解
主动模式(Active Mode):
- 客户端打开一个端口并监听。
- 客户端通过控制连接告诉服务器自己的 IP 和端口。
- 服务器主动连接到客户端指定的端口传输数据。
被动模式(Passive Mode):
- 客户端通过控制连接请求被动模式。
- 服务器打开一个随机端口并通过控制连接告知客户端。
- 客户端主动连接到服务器指定的端口传输数据。
优缺点对比:
- 客户端通过控制连接请求被动模式。
- 主动模式更适合在服务器端网络无防火墙限制的环境。
- 被动模式更适合客户端在 NAT 或防火墙后的情况。
FTP报文解析
FTP 报文分为命令和响应报文,命令报文用于发送操作请求,响应报文用于返回结果。
命令报文格式为“<命令><参数>\r\n”,字段解释如下:
- <命令>:FTP命令(如 USER、PASS)。
- <参数>:命令的附加信息(如用户名、文件名)。
例如“USER username\r\n”。常见的命令包括登录 (USER, PASS)、文件操作 (RETR, STOR)、目录操作 (LIST, CWD) 等。每个 FTP 报文由命令或响应代码、状态码及附加数据组成,状态码用于指示操作结果。
以下是 FTP 常见命令:
- USER:提供用户名进行身份验证。
- PASS:提供密码进行身份验证。
- CWD:更改当前工作目录。
- PWD:显示当前工作目录。
- LIST:列出目录下的文件和子目录。
- RETR: 从服务器下载文件。
- STOR:上传文件到服务器。
- DELE:删除指定文件。
- MKD:创建新目录。
- RMD:删除目录。
- QUIT:终止会话并退出。
- TYPE:设置文件传输类型(ASCII 或 Binary)。
- PORT: 指定数据连接的端口。
- PASV: 启用被动模式,服务器指定端口供客户端连接。
响应报文格式为“<状态码><说明文字>\r\n”,字段解释如下:
- <状态码>:三位数字表示状态。
- <说明文字>:状态的文字描述。
例如“230 User logged in, proceed.\r\n”。以下是FTP常见的响应码:
- 1xx(信息性响应): 主要是提供一些初步的信息,通常表示服务器正在处理请求,还没有完成操作。
- 2xx(成功响应): 表示命令成功执行。这是客户端最希望看到的响应类型之一,说明请求的操作(如登录、文件传输等)顺利完成。
- 3xx(补充信息响应): 表示服务器需要一些额外的信息才能完成操作。通常是在身份验证或者文件定位等过程中出现。
- 4xx(暂时错误响应): 表示客户端的请求有问题,但错误是暂时的,可能通过一些调整(如重新发送请求等)可以解决。
- 5xx(永久性错误响应): 表示客户端的请求存在错误,并且这个错误是比较严重的,很难通过简单的调整来纠正。
接着我们来看看FTP获取目录的报文示例:
- 客户端建立TCP连接到服务器的21端口
- 服务器返回:220 Welcome to FTP Server\r\n
- 客户端发送:USER wiznet\r\n
- 服务器返回:331 User wiznet OK.Password required\r\n
- 客户端发送:PASS wiznet\r\n
- 服务器返回:230 User logged in\r\n
- 客户端发送PORT 192,168,1,5,20,100\r\n(主动模式,192,168,1,5是客户端的地址,20,100是客户端期望的端口号20*256+100=5260)
- 服务器返回:200 PORT command successful\r\n
- 客户端发送:LIST\r\n(DIR命令,获取当前目录的文件信息)。
- 服务器回复:150 Opening ASCII mode data connection for file list\r\n
- 服务器向客户端期望的端口号发起TCP连接,并传输目录信息,传输完成后关闭TCP连接。
- 客户端发送:QUIT\r\n(退出FTP会话)
- 服务器回复:221 Goodbye\r\n
实现过程
接下来,我们看看如何在W5500上实现FTP协议的Server模式。
步骤1:获取网络配置信息和FTP初始化
wizchip_getnetinfo(&net_info);
ftpd_init(net_info.ip);
ftpd_init()函数内容如下:
/**
* @brief Initialize the FTP server
*
* Initialize the FTP server, set the status, commands, modes and other parameters of the FTP server, and initialize the FTP login username and password.
*
* @param src_ip: Source IP address
*/
void ftpd_init(uint8_t *src_ip)
{
ftp.state = FTPS_NOT_LOGIN;
ftp.current_cmd = NO_CMD;
ftp.dsock_mode = ACTIVE_MODE;
ftp.ID_Enable = STATUS_USED;
ftp.PW_Enable = STATUS_USED;
if (ftp.ID_Enable == STATUS_USED)
{
strcpy(ftp.username, ftp_ID);
printf(" FTP ID[%d]:%s \r\n", strlen(ftp.username), ftp.username);
}
if (ftp.PW_Enable == STATUS_USED)
{
strcpy(ftp.userpassword, ftp_PW);
printf(" FTP PW[%d]:%s \r\n", strlen(ftp.userpassword), ftp.userpassword);
}
local_ip.cVal[0] = src_ip[0];
local_ip.cVal[1] = src_ip[1];
local_ip.cVal[2] = src_ip[2];
local_ip.cVal[3] = src_ip[3];
local_port = 35000;
strcpy(ftp.workingdir, "/");
socket(CTRL_SOCK, Sn_MR_TCP, IPPORT_FTP, 0x0);
socket(CTRL_SOCK1, Sn_MR_TCP, IPPORT_FTP, 0x0);
}
ftpd_init()函数的主要作用是对 FTP 服务器的各种参数进行初始化设置,包括服务器状态、用户认证信息、网络地址和端口,以及创建TCP socket,为后续的 FTP 服务运行做好准备
步骤2:实现服务器和客户端之间的持续交互
ftpd_run()函数在主循环中不断被调用,作用是让 FTP 服务器持续运行,不断处理客户端的各种请求,实现服务器与客户端之间的持续交互,以提供稳定的 FTP 服务 。
while (1)
{
ftpd_run(ethernet_buf);
}
ftpd_run()函数内容如下:
/**
* Runs the FTP daemon, processing incoming data.
*
* @param dbuf: Pointer to data buffer for FTP processing.
*
* @return Status/error code (implementation-specific).
*
* Note: Main entry point for FTP daemon.
*/
uint8_t ftpd_run(uint8_t *dbuf)
{
uint16_t size = 0;
long ret = 0;
uint32_t blocklen, recv_byte;
uint32_t remain_filesize;
int32_t remain_datasize;
#if defined(F_FILESYSTEM)
FILINFO fno;
#endif
// FTP Control 1
#if 1
switch (getSn_SR(CTRL_SOCK))
{
case SOCK_ESTABLISHED:
if (!connect_state_control)
{
#if defined(_FTP_DEBUG_)
printf("%d:FTP Connected\r\n", CTRL_SOCK);
#endif
// fsprintf(CTRL_SOCK, banner, HOSTNAME, VERSION);
strcpy(ftp.workingdir, "/");
sprintf((char *)dbuf, "220 %s FTP version %s ready.\r\n", HOSTNAME, VERSION);
ret = send(CTRL_SOCK, (uint8_t *)dbuf, strlen((const char *)dbuf));
#if defined(_FTP_DEBUG_)
printf("%d:send() [%s]\r\n", CTRL_SOCK, dbuf);
#endif
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:send() error:%ld\r\n", CTRL_SOCK, ret);
#endif
close(CTRL_SOCK);
return ret;
}
connect_state_control = 1;
}
#if connect_timeout_en
else
{
if (con_remain_cnt1 > remain_time)
{
if ((ret = disconnect(CTRL_SOCK)) != SOCK_OK)
return ret;
#if defined(_FTP_DEBUG_)
printf("%d:Timeout Closed\r\n", CTRL_SOCK);
#endif
}
#if defined(_FTP_DEBUG_)
else if (((con_remain_cnt1 % 10000) == 0) && (con_remain_cnt1 != 0))
{
// printf("%d:Timeout Count:%ld\r\n", CTRL_SOCK, con_remain_cnt1);
}
#endif
con_remain_cnt1++;
}
#endif
#if defined(_FTP_DEBUG_)
// printf("ftp socket %d\r\n", CTRL_SOCK);
#endif
if ((size = getSn_RX_RSR(CTRL_SOCK)) > 0) // Don't need to check SOCKERR_BUSY because it doesn't not occur.
{
#if defined(_FTP_DEBUG_)
printf("%d:size: %d\r\n", CTRL_SOCK, size);
#endif
memset(dbuf, 0, _MAX_SS);
if (size > _MAX_SS)
size = _MAX_SS - 1;
ret = recv(CTRL_SOCK, dbuf, size);
dbuf[ret] = '\0';
if (ret != size)
{
if (ret == SOCK_BUSY)
return 0;
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:recv() error:%ld\r\n", CTRL_SOCK, ret);
#endif
close(CTRL_SOCK);
return ret;
}
}
#if defined(_FTP_DEBUG_)
printf("%d:Rcvd Command: %s", CTRL_SOCK, dbuf);
#endif
proc_ftpd(CTRL_SOCK, (char *)dbuf);
con_remain_cnt1 = 0;
}
break;
case SOCK_CLOSE_WAIT:
#if defined(_FTP_DEBUG_)
printf("%d:CloseWait\r\n", CTRL_SOCK);
#endif
if ((ret = disconnect(CTRL_SOCK)) != SOCK_OK)
return ret;
#if defined(_FTP_DEBUG_)
printf("%d:Closed\r\n", CTRL_SOCK);
#endif
break;
case SOCK_CLOSED:
#if defined(_FTP_DEBUG_)
printf("%d:FTPStart\r\n", CTRL_SOCK);
#endif
if ((ret = socket(CTRL_SOCK, Sn_MR_TCP, IPPORT_FTP, 0x0)) != CTRL_SOCK)
{
#if defined(_FTP_DEBUG_)
printf("%d:socket() error:%ld\r\n", CTRL_SOCK, ret);
#endif
close(CTRL_SOCK);
return ret;
}
break;
case SOCK_INIT:
#if defined(_FTP_DEBUG_)
printf("%d:Opened\r\n", CTRL_SOCK);
#endif
strcpy(ftp.workingdir, "/");
if ((ret = listen(CTRL_SOCK)) != SOCK_OK)
{
#if defined(_FTP_DEBUG_)
printf("%d:Listen error\r\n", CTRL_SOCK);
#endif
return ret;
}
connect_state_control = 0;
con_remain_cnt1 = 0;
#if defined(_FTP_DEBUG_)
printf("%d:Listen ok\r\n", CTRL_SOCK);
#endif
break;
default:
break;
}
// FTP Control 2
switch (getSn_SR(CTRL_SOCK1))
{
case SOCK_ESTABLISHED:
if (!connect_state_control1)
{
#if defined(_FTP_DEBUG_)
printf("%d:FTP Connected\r\n", CTRL_SOCK1);
#endif
strcpy(ftp.workingdir, "/");
sprintf((char *)dbuf, "220 %s FTP version %s ready.\r\n", HOSTNAME, VERSION);
ret = send(CTRL_SOCK1, (uint8_t *)dbuf, strlen((const char *)dbuf));
#if defined(_FTP_DEBUG_)
printf("%d:send() [%s]\r\n", CTRL_SOCK1, dbuf);
#endif
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:send() error:%ld\r\n", CTRL_SOCK1, ret);
#endif
close(CTRL_SOCK1);
return ret;
}
connect_state_control1 = 1;
}
#if connect_timeout_en
else
{
if (con_remain_cnt2 > remain_time)
{
if ((ret = disconnect(CTRL_SOCK1)) != SOCK_OK)
return ret;
#if defined(_FTP_DEBUG_)
printf("%d:Timeout Closed\r\n", CTRL_SOCK1);
#endif
}
#if defined(_FTP_DEBUG_)
else if (((con_remain_cnt2 % 10000) == 0) && (con_remain_cnt2 != 0))
{
printf("%d:Timeout Count:%ld\r\n", CTRL_SOCK1, con_remain_cnt2);
}
#endif
con_remain_cnt2++;
}
#endif
#if defined(_FTP_DEBUG_)
// printf("ftp socket %d\r\n", CTRL_SOCK);
#endif
if ((size = getSn_RX_RSR(CTRL_SOCK1)) > 0) // Don't need to check SOCKERR_BUSY because it doesn't not occur.
{
#if defined(_FTP_DEBUG_)
printf("%d: size: %d\r\n", CTRL_SOCK1, size);
#endif
memset(dbuf, 0, _MAX_SS);
if (size > _MAX_SS)
size = _MAX_SS - 1;
ret = recv(CTRL_SOCK1, dbuf, size);
dbuf[ret] = '\0';
if (ret != size)
{
if (ret == SOCK_BUSY)
return 0;
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:recv() error:%ld\r\n", CTRL_SOCK1, ret);
#endif
close(CTRL_SOCK1);
return ret;
}
}
#if defined(_FTP_DEBUG_)
printf("%d: Rcvd Command: %s", CTRL_SOCK1, dbuf);
#endif
proc_ftpd(CTRL_SOCK1, (char *)dbuf);
con_remain_cnt2 = 0;
}
break;
case SOCK_CLOSE_WAIT:
#if defined(_FTP_DEBUG_)
printf("%d:CloseWait\r\n", CTRL_SOCK1);
#endif
if ((ret = disconnect(CTRL_SOCK1)) != SOCK_OK)
return ret;
connect_count--;
#if defined(_FTP_DEBUG_)
printf("%d:Closed\r\n", CTRL_SOCK1);
#endif
break;
case SOCK_CLOSED:
#if defined(_FTP_DEBUG_)
printf("%d:FTPStart\r\n", CTRL_SOCK1);
#endif
if ((ret = socket(CTRL_SOCK1, Sn_MR_TCP, IPPORT_FTP, 0x0)) != CTRL_SOCK1)
{
#if defined(_FTP_DEBUG_)
printf("%d:socket() error:%ld\r\n", CTRL_SOCK1, ret);
#endif
close(CTRL_SOCK1);
return ret;
}
break;
case SOCK_INIT:
#if defined(_FTP_DEBUG_)
printf("%d:Opened\r\n", CTRL_SOCK1);
#endif
strcpy(ftp.workingdir, "/");
if ((ret = listen(CTRL_SOCK1)) != SOCK_OK)
{
#if defined(_FTP_DEBUG_)
printf("%d:Listen error\r\n", CTRL_SOCK1);
#endif
return ret;
}
connect_state_control1 = 0;
con_remain_cnt2 = 0;
#if defined(_FTP_DEBUG_)
printf("%d:Listen ok\r\n", CTRL_SOCK1);
#endif
break;
default:
break;
}
#endif
/////////////////////////////////// ftp data part
#if 1
switch (getSn_SR(DATA_SOCK))
{
case SOCK_ESTABLISHED:
if (!connect_state_data)
{
#if defined(_FTP_DEBUG_)
printf("%d:FTP Data socket Connected\r\n", DATA_SOCK);
#endif
connect_state_data = 1;
}
switch (ftp.current_cmd)
{
case LIST_CMD:
case MLSD_CMD:
#if defined(_FTP_DEBUG_)
printf("previous size: %d\r\n", size);
#endif
#if defined(F_FILESYSTEM)
scan_files(ftp.workingdir, dbuf, (int *)&size);
#endif
#if defined(_FTP_DEBUG_)
printf("returned size: %d\r\n", size);
printf("%s\r\n", dbuf);
#endif
#if !defined(F_FILESYSTEM)
if (strncmp(ftp.workingdir, "/$Recycle.Bin", sizeof("/$Recycle.Bin")) != 0)
size = sprintf((char *)dbuf, "drwxr-xr-x 1 ftp ftp 0 Dec 31 2014 $Recycle.Bin\r\n-rwxr-xr-x 1 ftp ftp 512 Dec 31 2014 test.txt\r\n");
#endif
size = strlen((char *)dbuf);
send(DATA_SOCK, dbuf, size);
ftp.current_cmd = NO_CMD;
disconnect(DATA_SOCK);
size = sprintf((char *)dbuf, "226 Successfully transferred \"%s\"\r\n", ftp.workingdir);
send(cur_sn, dbuf, size);
break;
case RETR_CMD:
#if defined(_FTP_DEBUG_)
printf("filename to retrieve : %s %d\r\n", ftp.filename, strlen(ftp.filename));
#endif
#if defined(F_FILESYSTEM)
ftp.fr = f_open(&(ftp.fil), (const char *)ftp.filename, FA_READ);
// print_filedsc(&(ftp.fil));
if (ftp.fr == FR_OK)
{
remain_filesize = ftp.fil.fsize;
#if defined(_FTP_DEBUG_)
printf("f_open return FR_OK\r\n");
#endif
do
{
#if defined(_FTP_DEBUG_)
printf("remained file size: %d\r\n", ftp.fil.fsize);
#endif
memset(dbuf, 0, _MAX_SS);
if (remain_filesize > _MAX_SS)
send_byte = _MAX_SS;
else
send_byte = remain_filesize;
ftp.fr = f_read(&(ftp.fil), dbuf, send_byte, &blocklen);
if (ftp.fr != FR_OK)
break;
#if defined(_FTP_DEBUG_)
printf("#");
printf("----->fsize:%d recv:%d len:%d \r\n", remain_filesize, send_byte, blocklen);
printf("----->fn:%s data:%s \r\n", ftp.filename, dbuf);
#endif
send(DATA_SOCK, dbuf, blocklen);
remain_filesize -= blocklen;
} while (remain_filesize != 0);
#if defined(_FTP_DEBUG_)
printf("\r\nFile read finished\r\n");
#endif
ftp.fr = f_close(&(ftp.fil));
}
else
{
#if defined(_FTP_DEBUG_)
printf("File Open Error: %d\r\n", ftp.fr);
#endif
}
#else
remain_filesize = strlen(ftp.filename);
#if defined(_FTP_DEBUG_)
printf("<<<<<1recv data[%d]\r\n", remain_datasize);
#endif
do
{
memset(dbuf, 0, _MAX_SS);
blocklen = sprintf((char *)dbuf, "%s", ftp.filename);
printf("####1[%d] dbuf:%s\r\n", remain_filesize, dbuf);
send(DATA_SOCK, dbuf, blocklen);
remain_filesize -= blocklen;
} while (remain_filesize != 0);
#endif
ftp.current_cmd = NO_CMD;
disconnect(DATA_SOCK);
size = sprintf((char *)dbuf, "226 Successfully transferred \"%s\"\r\n", ftp.filename);
send(cur_sn, dbuf, size);
break;
case STOR_CMD:
#if defined(_FTP_DEBUG_)
printf("filename to store : %s %d\r\n", ftp.filename, strlen(ftp.filename));
#endif
#if defined(F_FILESYSTEM)
ftp.fr = f_open(&(ftp.fil), (const char *)ftp.filename, FA_CREATE_ALWAYS | FA_WRITE);
// print_filedsc(&(ftp.fil));
if (ftp.fr == FR_OK)
{
#if defined(_FTP_DEBUG_)
printf("f_open return FR_OK\r\n");
#endif
while (1)
{
if ((remain_datasize = getSn_RX_RSR(DATA_SOCK)) > 0)
{
while (1)
{
memset(dbuf, 0, _MAX_SS);
if (remain_datasize > _MAX_SS)
recv_byte = _MAX_SS;
else
recv_byte = remain_datasize;
ret = recv(DATA_SOCK, dbuf, recv_byte);
#if defined(_FTP_DEBUG_)
printf("----->fn:%s data:%s \r\n", ftp.filename, dbuf);
#endif
ftp.fr = f_write(&(ftp.fil), dbuf, (UINT)ret, &blocklen);
#if defined(_FTP_DEBUG_)
printf("----->dsize:%d recv:%d len:%d \r\n", remain_datasize, ret, blocklen);
#endif
remain_datasize -= blocklen;
if (ftp.fr != FR_OK)
{
#if defined(_FTP_DEBUG_)
printf("f_write failed\r\n");
#endif
break;
}
if (remain_datasize <= 0)
break;
}
if (ftp.fr != FR_OK)
{
#if defined(_FTP_DEBUG_)
printf("f_write failed\r\n");
#endif
break;
}
#if defined(_FTP_DEBUG_)
printf("#");
#endif
}
else
{
if (getSn_SR(DATA_SOCK) != SOCK_ESTABLISHED)
break;
}
}
#if defined(_FTP_DEBUG_)
printf("\r\nFile write finished\r\n");
#endif
ftp.fr = f_close(&(ftp.fil));
}
else
{
#if defined(_FTP_DEBUG_)
printf("File Open Error: %d\r\n", ftp.fr);
#endif
}
// fno.fdate = (WORD)(((current_year - 1980) << 9) | (current_month << 5) | current_day);
// fno.ftime = (WORD)((current_hour << 11) | (current_min << 5) | (current_sec >> 1));
// f_utime((const char *)ftp.filename, &fno);
#else
while (1)
{
if ((remain_datasize = getSn_RX_RSR(DATA_SOCK)) > 0)
{
#if defined(_FTP_DEBUG_)
printf("<<<<<2recv data[%ld]\r\n", remain_datasize);
#endif
while (1)
{
memset(dbuf, 0, _MAX_SS);
if (remain_datasize > _MAX_SS)
recv_byte = _MAX_SS;
else
recv_byte = remain_datasize;
ret = recv(DATA_SOCK, dbuf, recv_byte);
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("finish ret[%d\r\n", ret);
#endif
break;
}
#if defined(_FTP_DEBUG_)
printf("#####2[%ld] dbuf:%s\r\n", remain_datasize, dbuf);
#endif
remain_datasize -= ret;
#if defined(_FTP_DEBUG_)
printf("###ret:%ld,remain:%d\r\n", ret, remain_datasize);
#endif
if (remain_datasize <= 0)
break;
}
}
else
{
if (getSn_SR(DATA_SOCK) != SOCK_ESTABLISHED)
break;
}
}
#endif
ftp.current_cmd = NO_CMD;
disconnect(DATA_SOCK);
size = sprintf((char *)dbuf, "226 Successfully transferred \"%s\"\r\n", ftp.filename);
send(cur_sn, dbuf, size);
break;
case NO_CMD:
default:
break;
}
break;
case SOCK_CLOSE_WAIT:
#if defined(_FTP_DEBUG_)
printf("%d:CloseWait\r\n", DATA_SOCK);
#endif
if ((ret = disconnect(DATA_SOCK)) != SOCK_OK)
return ret;
#if defined(_FTP_DEBUG_)
printf("%d:Closed\r\n", DATA_SOCK);
#endif
break;
case SOCK_CLOSED:
if (ftp.dsock_state == DATASOCK_READY)
{
if (ftp.dsock_mode == PASSIVE_MODE)
{
#if defined(_FTP_DEBUG_)
printf("%d:FTPDataStart, port : %d\r\n", DATA_SOCK, local_port);
#endif
if ((ret = socket(DATA_SOCK, Sn_MR_TCP, local_port, 0x0)) != DATA_SOCK)
{
#if defined(_FTP_DEBUG_)
printf("%d:socket() error:%ld\r\n", DATA_SOCK, ret);
#endif
close(DATA_SOCK);
return ret;
}
local_port++;
if (local_port > 50000)
local_port = 35000;
}
else
{
#if defined(_FTP_DEBUG_)
printf("%d:FTPDataStart, port : %d\r\n", DATA_SOCK, IPPORT_FTPD);
#endif
if ((ret = socket(DATA_SOCK, Sn_MR_TCP, IPPORT_FTPD, 0x0)) != DATA_SOCK)
{
#if defined(_FTP_DEBUG_)
printf("%d:socket() error:%ld\r\n", DATA_SOCK, ret);
#endif
close(DATA_SOCK);
return ret;
}
}
ftp.dsock_state = DATASOCK_START;
}
break;
case SOCK_INIT:
#if defined(_FTP_DEBUG_)
printf("%d:Opened\r\n", DATA_SOCK);
#endif
if (ftp.dsock_mode == PASSIVE_MODE)
{
if ((ret = listen(DATA_SOCK)) != SOCK_OK)
{
#if defined(_FTP_DEBUG_)
printf("%d:Listen error\r\n", DATA_SOCK);
#endif
return ret;
}
#if defined(_FTP_DEBUG_)
printf("%d:Listen ok\r\n", DATA_SOCK);
#endif
}
else
{
if ((ret = connect(DATA_SOCK, remote_ip.cVal, remote_port)) != SOCK_OK)
{
#if defined(_FTP_DEBUG_)
printf("%d:Connect error\r\n", DATA_SOCK);
#endif
return ret;
}
}
connect_state_data = 0;
break;
default:
break;
}
#endif
return 0;
}
进入ftpd_run()函数后,程序会执行一个TCP Server模式的状态机(具体可参考TCP Server章节),当socket处于 SOCK_ESTABLISHED 状态时,如果是首次进入SOCK_ESTABLISHED 状态,则会向客户端发送欢迎消息。后续则是监听客户端指令,当收到客户端指令后,会进入proc_ftpd()进行处理。
proc_ftpd()函数处理 FTP 服务的命令,根据不同命令及参数进行相应操作,包括用户认证、文件操作、数据传输、状态处理及错误响应。
请注意:
当宏定义connect_timeout_en的值设置为 1 时,会启用连接超时功能。如果在超过宏定义remain_time所设定的时长后,仍然没有进行任何操作,系统将自动断开连接。其中,connect_timeout_en是一个控制连接超时功能开启或关闭的宏,其值为 1 表示开启该功能,为 0 表示关闭;而remain_time是一个宏,它定义了在触发自动断开连接操作前的最大允许无操作时长。
proc_ftpd()函数如下:
/**
* Processes FTP-related data or request.
*
* @param sn Identifier for distinguishing requests/sessions.
* @param buf Buffer to store result or status information.
*
* @return Character indicating operation status ('S' for success, 'E' for error, etc.).
*
* Note: Refer to documentation for exact behavior and meaning of return value.
*/
char proc_ftpd(uint8_t sn, char *buf)
{
char **cmdp, *cp, *arg, *tmpstr;
char sendbuf[200];
int slen;
long ret;
// Translate first word to lower case
for (cp = buf; *cp != ' ' && *cp != '\0'; cp++)
*cp = tolower(*cp);
// Find command in table; if not present, return syntax error
for (cmdp = commands; *cmdp != NULL; cmdp++)
if (strncmp(*cmdp, buf, strlen(*cmdp)) == 0)
break;
if (*cmdp == NULL)
{
// fsprintf(CTRL_SOCK, badcmd, buf);
slen = sprintf(sendbuf, "500 Unknown command '%s'\r\n", buf);
send(sn, (uint8_t *)sendbuf, slen);
return 0;
}
// Allow only USER, PASS and QUIT before logging in
if (ftp.state == FTPS_NOT_LOGIN)
{
switch (cmdp - commands)
{
case USER_CMD:
case PASS_CMD:
case QUIT_CMD:
break;
default:
// fsprintf(CTRL_SOCK, notlog);
slen = sprintf(sendbuf, "530 Please log in with USER and PASS\r\n");
send(sn, (uint8_t *)sendbuf, slen);
return 0;
}
}
arg = &buf[strlen(*cmdp)];
while (*arg == ' ')
arg++;
/* Execute specific command */
switch (cmdp - commands)
{
case USER_CMD:
#if defined(_FTP_DEBUG_)
printf("USER_CMD : %s", arg);
#endif
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
if (ftp.ID_Enable == STATUS_USED)
{
if (strcmp(ftp.username, arg) != 0)
{
slen = sprintf(sendbuf, "430 Invalid username\r\n");
ret = send(sn, (uint8_t *)sendbuf, slen);
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:send() error:%ld\r\n", sn, ret);
#endif
close(sn);
return ret;
}
break;
}
}
else
{
strcpy(ftp.username, arg);
}
// fsprintf(CTRL_SOCK, givepass);
slen = sprintf(sendbuf, "331 Enter PASS command\r\n");
ret = send(sn, (uint8_t *)sendbuf, slen);
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:send() error:%ld\r\n", sn, ret);
#endif
close(sn);
return ret;
}
break;
case PASS_CMD:
#if defined(_FTP_DEBUG_)
printf("PASS_CMD : %s", arg);
#endif
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
if (ftp.PW_Enable == STATUS_USED)
{
if (strcmp(ftp.userpassword, arg) != 0)
{
slen = sprintf(sendbuf, "430 Invalid password\r\n");
ret = send(sn, (uint8_t *)sendbuf, slen);
if (ret < 0)
{
#if defined(_FTP_DEBUG_)
printf("%d:send() error:%ld\r\n", sn, ret);
#endif
close(sn);
return ret;
}
break;
}
}
ftplogin(sn, arg);
break;
case TYPE_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
switch (arg[0])
{
case 'A':
case 'a': // Ascii
ftp.type = ASCII_TYPE;
// fsprintf(CTRL_SOCK, typeok, arg);
slen = sprintf(sendbuf, "200 Type set to %s\r\n", arg);
send(sn, (uint8_t *)sendbuf, slen);
break;
case 'B':
case 'b': // Binary
case 'I':
case 'i': // Image
ftp.type = IMAGE_TYPE;
// fsprintf(CTRL_SOCK, typeok, arg);
slen = sprintf(sendbuf, "200 Type set to %s\r\n", arg);
send(sn, (uint8_t *)sendbuf, slen);
break;
default: /* Invalid */
// fsprintf(CTRL_SOCK, badtype, arg);
slen = sprintf(sendbuf, "501 Unknown type \"%s\"\r\n", arg);
send(sn, (uint8_t *)sendbuf, slen);
break;
}
break;
case FEAT_CMD:
slen = sprintf(sendbuf, "211-Features:\r\n MDTM\r\n REST STREAM\r\n SIZE\r\n MLST size*;type*;create*;modify*;\r\n MLSD\r\n UTF8\r\n CLNT\r\n MFMT\r\n211 END\r\n");
send(sn, (uint8_t *)sendbuf, slen);
break;
case QUIT_CMD:
#if defined(_FTP_DEBUG_)
printf("QUIT_CMD\r\n");
#endif
// fsprintf(CTRL_SOCK, bye);
slen = sprintf(sendbuf, "221 Goodbye!\r\n");
send(sn, (uint8_t *)sendbuf, slen);
disconnect(sn);
break;
case RETR_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
#if defined(_FTP_DEBUG_)
printf("RETR_CMD\r\n");
#endif
if (strlen(ftp.workingdir) == 1)
sprintf(ftp.filename, "/%s", arg);
else
sprintf(ftp.filename, "%s/%s", ftp.workingdir, arg);
slen = sprintf(sendbuf, "150 Opening data channel for file downloand from server of \"%s\"\r\n", ftp.filename);
send(sn, (uint8_t *)sendbuf, slen);
ftp.current_cmd = RETR_CMD;
break;
case APPE_CMD:
case STOR_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
#if defined(_FTP_DEBUG_)
printf("STOR_CMD\r\n");
#endif
if (strlen(ftp.workingdir) == 1)
sprintf(ftp.filename, "/%s", arg);
else
sprintf(ftp.filename, "%s/%s", ftp.workingdir, arg);
slen = sprintf(sendbuf, "150 Opening data channel for file upload to server of \"%s\"\r\n", ftp.filename);
send(sn, (uint8_t *)sendbuf, slen);
ftp.current_cmd = STOR_CMD;
if (ftp.dsock_mode == ACTIVE_MODE)
{
if ((ret = connect(DATA_SOCK, remote_ip.cVal, remote_port)) != SOCK_OK)
{
#if defined(_FTP_DEBUG_)
printf("%d:Connect error\r\n", DATA_SOCK);
#endif
return ret;
}
}
connect_state_data = 0;
break;
case PORT_CMD:
#if defined(_FTP_DEBUG_)
printf("PORT_CMD\r\n");
#endif
if (pport(arg) == -1)
{
// fsprintf(CTRL_SOCK, badport);
slen = sprintf(sendbuf, "501 Bad port syntax\r\n");
send(sn, (uint8_t *)sendbuf, slen);
}
else
{
// fsprintf(CTRL_SOCK, portok);
ftp.dsock_mode = ACTIVE_MODE;
ftp.dsock_state = DATASOCK_READY;
slen = sprintf(sendbuf, "200 PORT command successful.\r\n");
send(sn, (uint8_t *)sendbuf, slen);
}
break;
case MLSD_CMD:
#if defined(_FTP_DEBUG_)
printf("MLSD_CMD\r\n");
#endif
slen = sprintf(sendbuf, "150 Opening data channel for directory listing of \"%s\"\r\n", ftp.workingdir);
send(sn, (uint8_t *)sendbuf, slen);
ftp.current_cmd = MLSD_CMD;
break;
case LIST_CMD:
#if defined(_FTP_DEBUG_)
printf("LIST_CMD\r\n");
#endif
slen = sprintf(sendbuf, "150 Opening data channel for directory listing of \"%s\"\r\n", ftp.workingdir);
send(sn, (uint8_t *)sendbuf, slen);
ftp.current_cmd = LIST_CMD;
break;
case NLST_CMD:
#if defined(_FTP_DEBUG_)
printf("NLST_CMD\r\n");
#endif
break;
case SYST_CMD:
slen = sprintf(sendbuf, "215 UNIX emulated by WIZnet\r\n");
send(sn, (uint8_t *)sendbuf, slen);
break;
case PWD_CMD:
case XPWD_CMD:
slen = sprintf(sendbuf, "257 \"%s\" is current directory.\r\n", ftp.workingdir);
send(sn, (uint8_t *)sendbuf, slen);
break;
case PASV_CMD:
slen = sprintf(sendbuf, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)\r\n", local_ip.cVal[0], local_ip.cVal[1], local_ip.cVal[2], local_ip.cVal[3], local_port >> 8, local_port & 0x00ff);
send(sn, (uint8_t *)sendbuf, slen);
if (getSn_SR(DATA_SOCK) == SOCK_ESTABLISHED)
{
#if defined(_FTP_DEBUG_)
printf("data disconnect: %d\r\n", DATA_SOCK);
#endif
disconnect(DATA_SOCK);
}
ftp.dsock_mode = PASSIVE_MODE;
ftp.dsock_state = DATASOCK_READY;
cur_sn = sn;
#if defined(_FTP_DEBUG_)
printf("PASV port: %d\r\n", local_port);
#endif
break;
case SIZE_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
if (slen > 3)
{
tmpstr = strrchr(arg, '/');
*tmpstr = 0;
#if defined(F_FILESYSTEM)
slen = get_filesize(arg, tmpstr + 1);
#else
slen = _MAX_SS;
#endif
if (slen > 0)
slen = sprintf(sendbuf, "213 %d\r\n", slen);
else
slen = sprintf(sendbuf, "550 File not Found\r\n");
}
else
{
slen = sprintf(sendbuf, "550 File not Found\r\n");
}
send(sn, (uint8_t *)sendbuf, slen);
break;
case CWD_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
if (slen > 3)
{
arg[slen - 3] = 0x00;
tmpstr = strrchr(arg, '/');
*tmpstr = 0;
#if defined(F_FILESYSTEM)
slen = get_filesize(arg, tmpstr + 1);
#else
slen = 0;
#endif
*tmpstr = '/';
if (slen == 0)
{
slen = sprintf(sendbuf, "213 %d\r\n", slen);
strcpy(ftp.workingdir, arg);
slen = sprintf(sendbuf, "250 CWD successful. \"%s\" is current directory.\r\n", ftp.workingdir);
}
else
{
slen = sprintf(sendbuf, "550 CWD failed. \"%s\"\r\n", arg);
}
}
else
{
strcpy(ftp.workingdir, arg);
slen = sprintf(sendbuf, "250 CWD successful. \"%s\" is current directory.\r\n", ftp.workingdir);
}
send(sn, (uint8_t *)sendbuf, slen);
break;
case MKD_CMD:
case XMKD_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
#if defined(F_FILESYSTEM)
if (f_mkdir(arg) != 0)
{
slen = sprintf(sendbuf, "550 Can't create directory. \"%s\"\r\n", arg);
}
else
{
slen = sprintf(sendbuf, "257 MKD command successful. \"%s\"\r\n", arg);
// strcpy(ftp.workingdir, arg);
}
#else
slen = sprintf(sendbuf, "550 Can't create directory. Permission denied\r\n");
#endif
send(sn, (uint8_t *)sendbuf, slen);
break;
case DELE_CMD:
slen = strlen(arg);
arg[slen - 1] = 0x00;
arg[slen - 2] = 0x00;
#if defined(F_FILESYSTEM)
if (f_unlink(arg) != 0)
{
slen = sprintf(sendbuf, "550 Could not delete. \"%s\"\r\n", arg);
}
else
{
slen = sprintf(sendbuf, "250 Deleted. \"%s\"\r\n", arg);
}
#else
slen = sprintf(sendbuf, "550 Could not delete. Permission denied\r\n");
#endif
send(sn, (uint8_t *)sendbuf, slen);
break;
case XCWD_CMD:
case ACCT_CMD:
case XRMD_CMD:
case RMD_CMD:
case STRU_CMD:
case MODE_CMD:
case XMD5_CMD:
// fsprintf(CTRL_SOCK, unimp);
slen = sprintf(sendbuf, "502 Command does not implemented yet.\r\n");
send(sn, (uint8_t *)sendbuf, slen);
break;
default: // Invalid
// fsprintf(CTRL_SOCK, badcmd, arg);
slen = sprintf(sendbuf, "500 Unknown command \'%s\'\r\n", arg);
send(sn, (uint8_t *)sendbuf, slen);
break;
}
return 1;
}
进入 proc_ftpd()函数后,程序会执行一个状态机,首先将接收的命令转换为小写并在命令表中查找,未找到时发送错误信息。登录前仅允许 USER、PASS、QUIT 命令,其余报错。对于不同命令,如 USER_CMD 处理用户名验证和后续操作,PASS_CMD 进行密码验证和登录,TYPE_CMD 处理传输类型设置,FEAT_CMD 发送特性信息,QUIT_CMD 断开连接,还有 RETR_CMD 等文件操作命令,以及 PORT_CMD、PASV_CMD 等数据连接模式相关命令,根据不同情况执行相应的操作和错误处理,同时发送相应的状态信息。
运行结果
请注意:
测试实例需要PC端和W5500处于同一网段
烧录例程运行后,首先进行了PHY链路检测,然后打印设置网络信息,接着则是运行FTP服务器。

然后打开CMD,输入ftp 192.168.1.127访问FTP服务器,输入用户名和密码wiznet,接着执行读取目录和上传文件操作。

总结
本文讲解了如何在 W5500 芯片上实现 FTP 协议的服务器模式,通过实战例程展示了使用 W5500 作为 FTP 服务器与 PC 端进行文件传输、目录操作等功能的过程,涵盖获取网络配置信息和 FTP 初始化、实现服务器和客户端之间的持续交互等关键步骤。文章详细介绍了 FTP 协议的概念、特点、应用场景、基本工作流程、主动与被动模式、报文解析,帮助读者理解其在文件传输中的实际应用价值。
下一篇文章将聚焦 FTP 协议客户端模式,解析其核心原理及在文件传输中的应用,同时讲解如何在W5500上实现 FTP 客户端功能,敬请期待!