X
首页
技术
模拟电子
单片机
半导体
电源管理
嵌入式
传感器
最能打国产芯
应用
汽车电子
工业控制
家用电子
手机便携
安防电子
医疗电子
网络通信
测试测量
物联网
最能打国产芯
大学堂
首页
直播
专题
TI 培训
论坛
汽车电子
国产芯片
电机驱动控制
电源技术
单片机
模拟电子
PCB设计
电子竞赛
DIY/开源
嵌入式系统
医疗电子
颁奖专区
【厂商专区】
【电子技术】
【创意与实践】
【行业应用】
【休息一下】
最能打国产芯
活动中心
直播
发现活动
颁奖区
电子头条
参考设计
下载中心
分类资源
文集
排行榜
电路图
Datasheet
最能打国产芯
DigiKey得捷技术专区
[作品提交] 【得捷电子Follow me第4期】终极任务:FTP服务器技术实现及分析总结
alanlan86
2024-2-25 12:22
楼主
# 任务目标 - 使用主控板内存储和以太网接口,实现轻量ftp文件服务器,可以上传、下载、删除文件 ![](https://micropython.org/resources/micropython-media/boards/W5500_EVB_PICO/W5500-EVB-Pico.jpg) # 理论 - 根据百度百科可以查得: FTP服务器(File Transfer Protocol Server)是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务。 FTP是File Transfer Protocol(文件传输协议)。顾名思义,就是专门用来传输文件的协议。简单地说,支持FTP协议的服务器就是FTP服务器。 ## FTP协议 查阅FTP的协议如下图。可知FTP协议是基于TCP连接而非UDP,它分为控制连接和数据连接。稍后软件部分,可以详细找到对应的代码调用。 ![](https://bkimg.cdn.bcebos.com/pic/dc54564e9258d1091af37250df58ccbf6d814dc4?x-bce-process=image/format,f_auto/watermark,image_d2F0ZXIvYmFpa2UyNzI,g_7,xp_5,yp_5,P_20/resize,m_lfit,limit_1,h_1080) ## 命令 - 可以被发送到FTP服务器的FTP命令列表,包含由IETF在RFC 959中标准化的所有命令。需要注意的是,大多数命令行FTP客户端都给用户提供了额外的命令集。例如,GET是一个常见的用来下载文件的用户命令,用来替代原始的RETR命令。 ## 其他 - 为了保证FTP文件访问的安全性,FTP协议中还有关于用户登入和权限管控的部分。 - 更详细的FTP协议相关内容可以参考链接:[FTP协议详解](https://baike.baidu.com/item/ftp/13839?fromtitle=%E6%96%87%E4%BB%B6%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE&fromid=1874113 "FTP协议详解") # 硬件 - 采用Pico W5500 EVB及网线接入带路由器或交换器的网络,然后采用相同局域网网段的PC电脑对主控开发板进行FTP访问; - 需要在PC端安装对应的测试软件:Xftp 7(当然使用其他支持FTP协议的PC上位机同样可以) - Pico开发板需要刷入microPython的固件: [Pico W5500 EVB micropython固件](https://micropython.org/download/W5500_EVB_PICO/ "Pico W5500 EVB micropython固件") ```c Vendor: Wiznet Features: Dual-core, Ethernet, External Flash, USB Source on GitHub: rp2/W5500_EVB_PICO More info: Website Installation instructions Flashing via UF2 bootloader To get the board in bootloader mode ready for the firmware update, execute machine.bootloader() at the MicroPython REPL. Alternatively, hold down the BOOTSEL button while plugging the board into USB. The uf2 file below should then be copied to the USB mass storage device that appears. Once programming of the new firmware is complete the device will automatically reset and be ready for use. Firmware Releases v1.22.2 (2024-02-22) .uf2 / [Release notes] (latest) ``` # 软件 ## 初始化W5500 - 采用DHCP的方式初始化W5500的网络控制器,同时打印出接入网络成功之后的IP地址 DNS等信息。 ```python """ W5500 初始化 """ def w5x00_init(): global nic # 网口初始化 nic = network.WIZNET5K(spi, Pin(_CS_PIN), Pin(_RST_PIN)) # spi,cs,reset pin nic.active(True) # 配置网络 # If you use the Dynamic IP(DHCP), you must use the "nic.ifconfig('dhcp')". nic.ifconfig("dhcp") # If you use the Static IP, you must use the "nic.ifconfig("IP","subnet","Gateway","DNS")". # nic.ifconfig(('192.168.0.106','255.255.255.0','192.168.0.1','114.114.114.114')) while not nic.isconnected(): time.sleep(1) print(nic.regs()) print("IP地址: %s" % nic.ifconfig()[0]) print("子网掩码: %s" % nic.ifconfig()[1]) print("网关: %s" % nic.ifconfig()[2]) print("DNS: %s" % nic.ifconfig()[3]) ``` ## 创建TCP套接字 - FTP Server的创建,需要依赖2个socket,分别是ftpsocket和datasocket,它们用于做网络控制和文件数据传送使用。然后端口进行bind绑定和侦听。 ```python """ 启动FTP服务 """ def ftpserver(): DATA_PORT = 13333 ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4]) datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) ftpsocket.listen(1) datasocket.listen(1) datasocket.settimeout(10) print("FTP服务启动成功!监听端口:21") ``` ## 网络命令及文件操作 - 在进行网络套接字 accept连接后,采用非阻塞方式,读出网络数据包,并解释出CWM+参数+路径,相对应的内容,进而根据对应的CMD命令执行响应回复处理。 ```python while True: cl, remote_addr = ftpsocket.accept() cl.settimeout(300) cwd = FTP_ROOT_PATH try: print("新的FTP连接来自: %s:%s" % (remote_addr[0], remote_addr[1])) cl.sendall("220 Welcome! This is the W5500_EVB_PICO!\r\n") while True: gc.collect() data = cl.readline().decode("utf-8").rstrip("\r\n") if len(data) <= 0: print("Client disappeared") break command = data.split(" ")[0].upper() payload = data[len(command) :].lstrip() path = get_absolute_path(cwd, payload) print("命令={}, 参数={}, 路径={}".format(command, payload, path)) if command == "USER": cl.sendall("230 Logged in.\r\n") elif command == "SYST": cl.sendall("215 UNIX Type: L8\r\n") elif command == "NOOP": cl.sendall("200 OK\r\n") elif command == "FEAT": cl.sendall("211 no-features\r\n") elif command == "PWD": cl.sendall('257 "{}"\r\n'.format(cwd)) elif command == "CWD": try: files = uos.listdir(path) cwd = path cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "CDUP": cwd = get_absolute_path(cwd, "..") cl.sendall(msg_250_OK) elif command == "TYPE": # probably should switch between binary and not cl.sendall("200 Transfer mode set\r\n") elif command == "SIZE": try: size = uos.stat(path)[6] cl.sendall("213 {}\r\n".format(size)) except: cl.sendall(msg_550_fail) elif command == "QUIT": cl.sendall("221 Bye.\r\n") break elif command == "PASV": addr = nic.ifconfig()[0] cl.sendall( "227 Entering Passive Mode ({},{},{}).\r\n".format( addr.replace(".", ","), DATA_PORT >> 8, DATA_PORT % 256 ) ) dataclient, data_addr = datasocket.accept() print("新的FTP数据连接来自: %s:%s" % (data_addr[0], data_addr[1])) elif command == "LIST" or command == "NLST": if not payload.startswith("-"): place = path else: place = cwd try: send_list_data( place, dataclient, command == "LIST" or payload == "-l" ) cl.sendall("150 Here comes the directory listing.\r\n") cl.sendall("226 Listed.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "RETR": try: send_file_data(path, dataclient) cl.sendall("150 Opening data connection.\r\n") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "STOR": try: cl.sendall("150 Ok to send data.\r\n") save_file_data(path, dataclient, "wb") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "APPE": try: cl.sendall("150 Ok to send data.\r\n") save_file_data(path, dataclient, "a") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "DELE": try: uos.remove(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "RMD": try: uos.rmdir(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "MKD": try: uos.mkdir(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "RNFR": fromname = path cl.sendall("350 Rename from\r\n") elif command == "RNTO": if fromname is not None: try: uos.rename(fromname, path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) else: cl.sendall(msg_550_fail) fromname = None else: cl.sendall("502 Unsupported command.\r\n") # print("Unsupported command {} with payload {}".format(command, payload)) except Exception as err: print(err) finally: cl.close() cl = None ``` ## 文件操作 - 对应CMD的命令情况,需要操作micropython内部的文件存储,例如,文件列表请求、文件目录信息列举、发送文件、保存文件等等操作。 ```python """ 响应文件列表请求 """ def send_list_data(path, dataclient, full): try: # whether path is a directory name for fname in uos.listdir(path): dataclient.sendall(make_description(path, fname, full)) except: # path may be a file name or pattern pattern = path.split("/")[-1] path = path[: -(len(pattern) + 1)] if path == "": path = "/" for fname in uos.listdir(path): if fncmp(fname, pattern) == True: dataclient.sendall(make_description(path, fname, full)) """ 列出目录详情 """ def make_description(path, fname, full): if full: stat = uos.stat(get_absolute_path(path, fname)) file_permissions = ( "drwxr-xr-x" if (stat[0] & 0o170000 == 0o040000) else "-rw-r--r--" ) file_size = stat[6] tm = localtime(stat[7]) if tm[0] != localtime()[0]: description = "{} 1 owner group {:>10} {} {:2} {:>5} {}\r\n".format( file_permissions, file_size, month_name[tm[1]], tm[2], tm[0], fname ) else: description = ( "{} 1 owner group {:>10} {} {:2} {:02}:{:02} {}\r\n".format( file_permissions, file_size, month_name[tm[1]], tm[2], tm[3], tm[4], fname, ) ) else: description = fname + "\r\n" return description """ 发送文件数据 """ def send_file_data(path, dataclient): try: with open(path, "rb") as file: chunk = file.read(512) print("chunk 0: ", len(chunk)) while len(chunk) > 0: print("chunk: ", len(chunk)) dataclient.sendall(chunk) chunk = file.read(512) except Exception as err: print("error: ", err.args, err.value, err.errno) """ 保存文件上传数据 """ def save_file_data(path, dataclient, mode): with open(path, mode) as file: chunk = dataclient.read(512) while len(chunk) > 0: file.write(chunk) chunk = dataclient.read(512) """ 获取文件绝对路径 """ def get_absolute_path(cwd, payload): # Just a few special cases "..", "." and "" # If payload start's with /, set cwd to / # and consider the remainder a relative path if payload.startswith("/"): cwd = "/" for token in payload.split("/"): if token == "..": if cwd != "/": cwd = "/".join(cwd.split("/")[:-1]) if cwd == "": cwd = "/" elif token != "." and token != "": if cwd == "/": cwd += token else: cwd = cwd + "/" + token return cwd """ 文件名比较 """ def fncmp(fname, pattern): pi = 0 si = 0 while pi < len(pattern) and si < len(fname): if (fname[si] == pattern[pi]) or (pattern[pi] == "?"): si += 1 pi += 1 else: if pattern[pi] == "*": # recurse if (pi + 1) == len(pattern): return True while si < len(fname): if fncmp(fname[si:], pattern[pi + 1 :]) == True: return True else: si += 1 return False else: return False if pi == len(pattern.rstrip("*")) and si == len(fname): return True else: return False ``` ## 完整代码 - 整个测试试验用到的完整工程代码如下。 ```python import gc import uos import time import socket import network from time import localtime from machine import Pin, SPI from micropython import const _LED_PIN = const(25) # 绿色 LED 引脚 _SPI_SPEED = const(2_000_000) # SPI 速率 _MOSI_PIN = const(19) # SPI MOSI 引脚 _MISO_PIN = const(16) # SPI MISO 引脚 _SCK_PIN = const(18) # SPI SCK 引脚 _CS_PIN = const(17) # SPI CS 引脚 _RST_PIN = const(20) # SPI RESET 引脚 FTP_ROOT_PATH = const("/") # FTP 根目录 month_name = [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ] # SPI 定义 spi = SPI(0, _SPI_SPEED, mosi=Pin(_MOSI_PIN), miso=Pin(_MISO_PIN), sck=Pin(_SCK_PIN)) nic = None """ W5500 初始化 """ def w5x00_init(): global nic # 网口初始化 nic = network.WIZNET5K(spi, Pin(_CS_PIN), Pin(_RST_PIN)) # spi,cs,reset pin nic.active(True) # 配置网络 # If you use the Dynamic IP(DHCP), you must use the "nic.ifconfig('dhcp')". nic.ifconfig("dhcp") # If you use the Static IP, you must use the "nic.ifconfig("IP","subnet","Gateway","DNS")". # nic.ifconfig(('192.168.0.106','255.255.255.0','192.168.0.1','114.114.114.114')) while not nic.isconnected(): time.sleep(1) print(nic.regs()) print("IP地址: %s" % nic.ifconfig()[0]) print("子网掩码: %s" % nic.ifconfig()[1]) print("网关: %s" % nic.ifconfig()[2]) print("DNS: %s" % nic.ifconfig()[3]) """ 响应文件列表请求 """ def send_list_data(path, dataclient, full): try: # whether path is a directory name for fname in uos.listdir(path): dataclient.sendall(make_description(path, fname, full)) except: # path may be a file name or pattern pattern = path.split("/")[-1] path = path[: -(len(pattern) + 1)] if path == "": path = "/" for fname in uos.listdir(path): if fncmp(fname, pattern) == True: dataclient.sendall(make_description(path, fname, full)) """ 列出目录详情 """ def make_description(path, fname, full): if full: stat = uos.stat(get_absolute_path(path, fname)) file_permissions = ( "drwxr-xr-x" if (stat[0] & 0o170000 == 0o040000) else "-rw-r--r--" ) file_size = stat[6] tm = localtime(stat[7]) if tm[0] != localtime()[0]: description = "{} 1 owner group {:>10} {} {:2} {:>5} {}\r\n".format( file_permissions, file_size, month_name[tm[1]], tm[2], tm[0], fname ) else: description = ( "{} 1 owner group {:>10} {} {:2} {:02}:{:02} {}\r\n".format( file_permissions, file_size, month_name[tm[1]], tm[2], tm[3], tm[4], fname, ) ) else: description = fname + "\r\n" return description """ 发送文件数据 """ def send_file_data(path, dataclient): try: with open(path, "rb") as file: chunk = file.read(512) print("chunk 0: ", len(chunk)) while len(chunk) > 0: print("chunk: ", len(chunk)) dataclient.sendall(chunk) chunk = file.read(512) except Exception as err: print("error: ", err.args, err.value, err.errno) """ 保存文件上传数据 """ def save_file_data(path, dataclient, mode): with open(path, mode) as file: chunk = dataclient.read(512) while len(chunk) > 0: file.write(chunk) chunk = dataclient.read(512) """ 获取文件绝对路径 """ def get_absolute_path(cwd, payload): # Just a few special cases "..", "." and "" # If payload start's with /, set cwd to / # and consider the remainder a relative path if payload.startswith("/"): cwd = "/" for token in payload.split("/"): if token == "..": if cwd != "/": cwd = "/".join(cwd.split("/")[:-1]) if cwd == "": cwd = "/" elif token != "." and token != "": if cwd == "/": cwd += token else: cwd = cwd + "/" + token return cwd """ 文件名比较 """ def fncmp(fname, pattern): pi = 0 si = 0 while pi < len(pattern) and si < len(fname): if (fname[si] == pattern[pi]) or (pattern[pi] == "?"): si += 1 pi += 1 else: if pattern[pi] == "*": # recurse if (pi + 1) == len(pattern): return True while si < len(fname): if fncmp(fname[si:], pattern[pi + 1 :]) == True: return True else: si += 1 return False else: return False if pi == len(pattern.rstrip("*")) and si == len(fname): return True else: return False """ 启动FTP服务 """ def ftpserver(): DATA_PORT = 13333 ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ftpsocket.bind(socket.getaddrinfo("0.0.0.0", 21)[0][4]) datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4]) ftpsocket.listen(1) datasocket.listen(1) datasocket.settimeout(10) print("FTP服务启动成功!监听端口:21") msg_250_OK = "250 OK\r\n" msg_550_fail = "550 Failed\r\n" try: dataclient = None fromname = None while True: cl, remote_addr = ftpsocket.accept() cl.settimeout(300) cwd = FTP_ROOT_PATH try: print("新的FTP连接来自: %s:%s" % (remote_addr[0], remote_addr[1])) cl.sendall("220 Welcome! This is the W5500_EVB_PICO!\r\n") while True: gc.collect() data = cl.readline().decode("utf-8").rstrip("\r\n") if len(data) <= 0: print("Client disappeared") break command = data.split(" ")[0].upper() payload = data[len(command) :].lstrip() path = get_absolute_path(cwd, payload) print("命令={}, 参数={}, 路径={}".format(command, payload, path)) if command == "USER": cl.sendall("230 Logged in.\r\n") elif command == "SYST": cl.sendall("215 UNIX Type: L8\r\n") elif command == "NOOP": cl.sendall("200 OK\r\n") elif command == "FEAT": cl.sendall("211 no-features\r\n") elif command == "PWD": cl.sendall('257 "{}"\r\n'.format(cwd)) elif command == "CWD": try: files = uos.listdir(path) cwd = path cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "CDUP": cwd = get_absolute_path(cwd, "..") cl.sendall(msg_250_OK) elif command == "TYPE": # probably should switch between binary and not cl.sendall("200 Transfer mode set\r\n") elif command == "SIZE": try: size = uos.stat(path)[6] cl.sendall("213 {}\r\n".format(size)) except: cl.sendall(msg_550_fail) elif command == "QUIT": cl.sendall("221 Bye.\r\n") break elif command == "PASV": addr = nic.ifconfig()[0] cl.sendall( "227 Entering Passive Mode ({},{},{}).\r\n".format( addr.replace(".", ","), DATA_PORT >> 8, DATA_PORT % 256 ) ) dataclient, data_addr = datasocket.accept() print("新的FTP数据连接来自: %s:%s" % (data_addr[0], data_addr[1])) elif command == "LIST" or command == "NLST": if not payload.startswith("-"): place = path else: place = cwd try: send_list_data( place, dataclient, command == "LIST" or payload == "-l" ) cl.sendall("150 Here comes the directory listing.\r\n") cl.sendall("226 Listed.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "RETR": try: send_file_data(path, dataclient) cl.sendall("150 Opening data connection.\r\n") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "STOR": try: cl.sendall("150 Ok to send data.\r\n") save_file_data(path, dataclient, "wb") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "APPE": try: cl.sendall("150 Ok to send data.\r\n") save_file_data(path, dataclient, "a") cl.sendall("226 Transfer complete.\r\n") except: cl.sendall(msg_550_fail) if dataclient is not None: dataclient.close() dataclient = None elif command == "DELE": try: uos.remove(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "RMD": try: uos.rmdir(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "MKD": try: uos.mkdir(path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) elif command == "RNFR": fromname = path cl.sendall("350 Rename from\r\n") elif command == "RNTO": if fromname is not None: try: uos.rename(fromname, path) cl.sendall(msg_250_OK) except: cl.sendall(msg_550_fail) else: cl.sendall(msg_550_fail) fromname = None else: cl.sendall("502 Unsupported command.\r\n") # print("Unsupported command {} with payload {}".format(command, payload)) except Exception as err: print(err) finally: cl.close() cl = None finally: datasocket.close() ftpsocket.close() if dataclient is not None: dataclient.close() if __name__ == "__main__": print("run in main") w5x00_init() # 初始化网络 ftpserver() # 运行 FTP Server ``` # 调试 ## 连接
## 上传文件
## 下载文件
## 删除文件
# 演示
演示
# 总结 以太网是互联网时代里面最重要的通信接口和技术之一。本次Follow Me 4通过Pico W5500 EVB这块带以太网的开发板,让我们对于网络通信有了更多的了解和学习。通过抓包和网络协议的学习,扩展了大家的技术认识! 感谢EEWorld和DigiKey提供的活动平台!!!感谢~
本帖最后由 alanlan86 于 2024-2-25 12:43 编辑
点赞
回复评论
暂无评论,赶紧抢沙发吧
最新活动
有奖直播报名中!抢占工业4.1先机 文晔科技日等你来!
2024 瑞萨电子MCU/MPU工业技术研讨会——深圳、上海站, 火热报名中
罗姆有奖直播 | 重点解析双极型晶体管的实用选型方法和使用方法
STM32N6终于要发布了,ST首款带有NPU的MCU到底怎么样,欢迎小伙们来STM32全球线上峰会寻找答案!
免费下载 | 安森美电动汽车充电白皮书,看碳化硅如何缓解“里程焦虑”!
是德科技有奖直播 | 应对未来高速算力芯片的设计与测试挑战
随便看看
STM32学习第七贴,函数变参篇,欢迎高手来喷,来指导attach_img
pcb工程师分级依据
EEWORLD网站系统升级公告
PIC单片机学习交流进行时
ZIGBEE AD 转换 片内温度学习总结
Cortex-M3权威指南Cn
DSP5509A EMIF_SDRAM中调试出现的问题
【学习心得电源】锂离子电池
项目需要申请LM3S8962评估套件
两头都是TYPEC口的线
来点题外的!~
找共同创业者做TWITTER
求助电容传感器的调理电路
关于晶振的选择?
新的开始
我这样修改cpld的管脚对不对
一点不会的新手,求一个串口转以太网的程序,很简单的就行,万分感谢。
这段标红的程序什么意思啊
TI 用于 C2000 实时控制开发套件的 F28379D controlCARD
除了Kinetis SDK v.2.0 API Reference Manual,有没有编程手册?
电子工程世界版权所有
京B2-20211791
京ICP备10001474号-1
京公网安备 11010802033920号
回复
写回复
收藏
回复