本文已参与「新人创作礼」活动,一起开启掘金创作之路
项目需求梳理
1. 用户登录认证(目前只实现一次连接一个客户端,即单线程)
1.1 用户密码 md5 加密
2. 查看目录
2.1 ls 命令 表示查看用户在服务端的目录,默认是家目录
2.2 dir 命令 表示用户查看在客户端的目录
3. 切换目录
3.1 用户使用 cd 命令切换服务端目录
3.1.1 实际上在服务端并没有真正切换目录
3.1.2 而且服务端代码编写时要注意,对用户来说,家目录就是最高一级,即对用户而言,home/username
是可以达到的路径,但 server/home 目录对用户来说是不存在路径
3.1.3 在切换路径后,客户端要记录用户此时切换到的路径,为接下来的 断点续存 功能做准备,不然会出现客
户端重新连接继续下载时找不到要下载文件在服务端路径的情况
4. 下载/上传/断点续存 进度条 创建
5. 下载功能
5.1 下载完成前,文件命名为 “filename.download”
5.1.1 在创建 “filename.download” 文件前判断该文件是否存在,如果存在,就在 download 后面再
加时间戳后缀
5.1.2 若 FTP 在文件下载完成前崩溃导致文件未下载完成,创建 shelvel 对象,记录 文件名,未下载完
成文件在服务端的路径,已下载的大小,为 断点续存 功能编写做准备
5.1.3 文件下载完成后,将后缀部分去除,如果去除后缀后文件已存在,就增加时间戳后缀,即“filename.时
间戳”
6. 上传功能(基本和下载功能一致)
6.1 下载完成前,文件命名为 “filename.upload”
6.1.1 在创建 “filename.upload” 文件前判断该文件是否存在,如果存在,就在 upload 后面再
加时间戳后缀
6.1.2 若 FTP 在文件上传完成前崩溃导致文件未上传完成,创建 shelvel 对象,记录 文件名,未上传完
成文件在服务端的路径,已下载的大小,为 断点续存 功能编写做准备
6.1.3 文件下载完成后,将后缀部分去除,如果去除后缀后文件已存在,就增加时间戳后缀,即“filename.时
间戳”
7. 下载/上传 断点续存功能
7.1 在用户登陆成功后,打印未下载完成、未上传完成的文件名,供用户选择是否要继续上传或下载
7.2 在用户选择继续下载或上传后,仍出现进度条并且从上次断的百分比开始增加
7.3 文件命名仍和下载/上传功能中文件命名一致`
项目基本框架搭建
项目文件说明
-- FTP (项目名称)
-- client 客户端
-- server 服务端
-- bin (存放可执行的二进制文件)
-- conf (存放配置文件)
-- accounts.ini (包括用户名称和密码,在此文件中的密码使用 md5 加密,非明文显示)
-- setting.py (包括服务端接口、用户家目录等内容)
-- home(存放用户家目录,比如 home\aoteman 是 用户 aoteman 的家目录)
-- lib (存放的是函数调用的信息,在windows操作系统中起到链接程序和函数的作用。其意义在于代码重用,程
序员将常用的功能写成函数,保存为lib文件,在以后编程要实现这些功能的时候,就不需要再重新编写代
码,而是直接调用写好的lib文件,这很大程度上减轻程序员的负担。)
-- log (项目日志)
bin 文件夹下 FTPServer.py 文件
# coding=gbk
import os,sys
# 因为要调用 server 文件夹下的其他 python 文件,所以程序。路径应为 \..\..\server
# 先返回 FTPServer.py 文件的绝对路径,在往上两级得到 server 文件夹的绝对路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# print(BASE_DIR)
sys.path.append(BASE_DIR)
if __name__ == '__main__':
# 从 lib 文件夹下 导入 management.py 文件
from lib import management
# sys.argv 可以简单的理解为 将 终端命令 作为参数传入, sys.argv 的结果是列表
# 比如说在终端输入 python server\bin\FTPServer.py start 运行,那么此时的 sys.argv 就是 ['server\\bin\\FTPServer.py', 'start']
FTP = management.FTPManagement(sys.argv)
FTP.execute()
lib 文件夹下 management.py
# coding=gbk
from lib import main
class FTPManagement(object):
def __init__(self, sys_argv):
self.sys_argv = sys_argv # 终端命令运行时传入的参数
# print(self.sys_argv)
def help_msg(self):
"""
帮助信息,为用户打印提示信息
"""
help_msg = """
start start up server
"""
# 退出并打印
exit(help_msg)
def parameter_verification(self):
"""
验证参数合法性
1. 项目启动时的命令设置为 python 文件路径 命令(如 start)
1.1 所以如果参数长度小于 2,应该向用户提示服务端可以接受的,命令
1.2 只需要判断长度是否小于2,如果大于2,程序只把 sys.argv 列表中的第二位参数视为命令
"""
if len(self.sys_argv) < 2:
self.help_msg()
def execute(self):
"""解析指令"""
self.parameter_verification() # 如果未通过参数验证,退出并打印 帮助信息,不会再运行下面的代码
parameter = self.sys_argv[1] # 取出代表命令的参数
# print(parameter)
# 判断 FTPManagement类 中是否有与命令参数相同的函数
# 即如果命令是 start, 那么需要判断 FTPManagement类 中是否有 self.start 函数
# 如果有此函数,调用函数
# 如果没有此函数,打印 提示信息
if hasattr(self, parameter): # FTPManagement类 中有 self.start 函数
func = getattr(self, parameter) # func 赋值为 self.start 函数
func() # 运行 self.start 函数
else:
print(" command does not exist !")
print(" start start up server !")
def start(self):
# print("------------")
Server = main.Server(self)
Server.run_forever()
lib 文件夹下 main.py
# coding=gbk
import socket
from conf import setting
class Server(object):
MSG_SIZE = 8192 # 一次性接收的信息长度
# 状态码
STATUS_CODE = {
}
def __init__(self, management):
self.management = management
self.user_home = setting.USER_HOME_DIR # 记录用户家目录
self.current_dir = self.user_home # 记录用户当前目录,用户切换目录时使用
self.shelve_obj = None # 记录下载/上传未完成的文件信息
# 建立服务端
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
self.sock.bind((setting.HOST, setting.PORT)) # 装上手机卡
self.sock.listen(5) # 开机,最大监听数为 5
def run_forever(self):
"""正式启动服务端"""
print(("FTP Server start %s %s " % (setting.HOST, setting.PORT)).center(50, "-"))
while True:
self.conn, self.addr = self.sock.accept() # 等待电话
print(self.addr)
print("Connection from %s" % (self.addr, )) # self.addr 是元组,所以选择 (self.addr, )的形式
try: # 防止报错而服务端退出程序
self.handle() # 处理客户端和服务端和交互问题
except Exception as e:
print(e)
def handle(self):
pass
conf 文件夹下 setting.py 文件
# coding=gbk
import os.path
# 服务器端口
HOST = "0.0.0.0"
PORT = 9999
# 用户家目录
# 先切换到 server 目录,在切换到 home 目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER_HOME_DIR = os.path.join(BASE_DIR, "home")
client 文件夹下 FTPClient.py 文件
# coding=gbk
import optparse
import shelve
import socket
class Client(object):
MSG_SIZE = 8192 # 一次性接收的信息长度
def __init__(self):
self.show_to_client = None # 输入命令时显示给用户的信息,如 【\username】>>:
self.username = None # 记录用户名称
self.current_dir = None # 记录用户下载文件时,文件在客户端的路径
self.shelve_obj = shelve.open("FTP_Download") # 记录未下载完成的文件信息
self.shelve_obj = shelve.open("FTP_Upload") # 记录未上传完成的文件信息
# 终端命令参数
# 输入 python client/FTPClient.py -h 得到提示信息
# 比如输入 python client/FTPClient.py -H 123 -P 12456 返回的结果是 self.option = {'serverHost': '123', 'port': 12456}, self.args = []
# 比如输入 python client/FTPClient.py 123 456 789, 返回的结果是 self.option = {'serverHost': None, 'port': None}, self.args = ['123', '456', '789']
parser = optparse.OptionParser()
parser.add_option("-H", "--serverHost", dest="serverHost", help="ftp server ip_addr")
parser.add_option("-P", "--port", type="int", dest="port", help="ftp server port")
self.options, self.args = parser.parse_args() # 取出参数
# print(self.options, self.args)
self.parameter_test()
self.make_connection()
def parameter_test(self):
"""参数检验,保证用户输入了 -H 和 -P 内容,不然客户端无法连接服务端端口"""
if not self.options.serverHost or not self.options.port: # 其中一项为空
exit("Error: must supply serverHost and port !")
def make_connection(self):
"""客户端连接"""
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
self.client.connect((self.options.serverHost, self.options.port)) # 打电话
print("Connection successful !")
if __name__ == '__main__':
client = Client()