多线程FTP项目(3)—— socketserver版本多线程FTP项目
threading 版
本来是想自己写一个实现多进程的 FTP 项目的,也就是说不使用 socketserver 模块实现多线程 FTP 项目,但是我写到一半调试的时候发现,虽然可以实现多用户同时登录,但是在输入命令之后,客户端很容易被 “远程计算机直接断开连接”。目前还是不清楚出了什么问题,不过看了 socketserver 模块源码后,发现该模块的多线程实现是比较复杂的,所以我觉得出现这个 bug 很大可能是因为我对于多线程的理解使用还不到家,所以还是使用 socketserver 模块实现多线程FTP项目。
项目开发目录
项目可实现功能
- 用户注册
- 多用户登录
- 用户查看家目录
- 用户切换家目录
- 用户在家目录下创建其他文件夹
- 用户从服务端下载文件,并且在下载过程中显示进度条
- 实现断点续存,文件下载过程中中断连接,可以继续下载,并显示进度条
不足之处
- 用户注册在服务端,其实是有问题的,从实际上来看,用户注册是用户做的事情,但是我们不可能在用户端注册用户然后将用户名和密码等信息传入服务端的文件中。目前能想到的解决方法是使用数据库,所以这样来看,FTP项目我大概到时候还要写一个含数据库的版本。
- 断点续存功能写完之后发现每次退出后都会删除未下载完的文件信息,所以这样好像一次只会存储一个未下载完成的文件信息。
- 本来想写一个注销用户的功能,比如启动服务端时可以选择删除操作,在删除前需要输入管理员密码,但最后没有写;
- 还有用户目录空间大小分配的功能,比如用户在上传文件时,判断分配的空间大小是否足够,因为没有写上传功能,所以也没有写判断空间大小的功能。
- 暂时能想到的不足之处只有这么多,但实际上应该还有挺多不足之处的,所以大概率之后还会再写一个优化后的含数据库的FTP项目。
项目代码
FTPclient.py 文件
import socket
import optparse, struct, json, time, os
from optparse import OptionParser
from configparser import ConfigParser
STR_RECV_LENGTH = 8 # 接收信息头字典长度编码后的数据
MAX_RECV_LENGTH = 1024 # 接收文件时一次性接收的数据长度
INI_PATH = os.path.dirname(os.path.dirname(__file__)) + "\\conf\\download.ini" # 因为没有专门写客户端的settings.py 所以ini的路径就直接写在 FTPclient.py 文件内部
class MyClient(object):
def __init__(self):
self.username = None # 记录登录的用户名
self.current_dir = None # 记录用户在服务端的当前目录
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 实例化 socket
# 启动 FTPclient.py 文件时,用于解析命令行选项
opt = OptionParser()
opt.add_option("-H", dest="HOST", help="FTP Server HOST")
opt.add_option("-P", dest="PORT", type="int", help="FTP Serve PORT")
values, args = opt.parse_args()
self.connection(values) # 连接服务端
def connection(self, values:optparse.Values):
"""和服务端连接"""
# 服务器的端口
HOST = values.HOST
PORT = values.PORT
if HOST and PORT: # 输入的 HOST 和 PORT 都不能为空
self.client.connect((HOST, PORT))
print("connect succeed")
self.login() # 连接完成后,进行用户登录
else:
exit("ERROR: should supply HOST and PORT !") # 退出并打印提示
def create_msg_to_send(self, action_type, **kwargs) -> dict:
"""制作信息头"""
# 信息头字典一定要有 action_type 的数据,方便服务端识别命令类型
dic = {
"action_type": action_type
}
# 传入的 **kwargs 是键值形式,update 将 dic 和 **kwargs 整合成一个信息头字典
dic.update(kwargs)
return dic
def send_msg(self, dic:dict) -> None:
"""发送信息"""
# 字典转换成字符串
dic_str = json.dumps(dic)
# 对字符串传毒进行编码,编码后的编码长度恒为 8
dic_str_length = struct.pack("q", len(dic_str))
self.client.sendall(dic_str_length) # 发送编码
self.client.sendall(dic_str.encode("utf-8")) # 发送字符串
def recv_msg(self) -> dict:
"""接收信息"""
# 解码获取字符串长度
str_length = struct.unpack("q", self.client.recv(STR_RECV_LENGTH))[0]
# 根据字符串长度选择不同的方式接收消息
if str_length < MAX_RECV_LENGTH: # 没有超过最大接收长度
dic_str = self.client.recv(str_length)
else: # 字符串长度超过 1024
recv_size = 0
dic_str = ""
while recv_size < str_length:
data = self.client.recv(MAX_RECV_LENGTH)
dic_str += data
recv_size += len(data)
dic = json.loads(dic_str) # 将字符串转化为字典,并返回字典
return dic
def login(self):
"""客户端登录"""
num = 0
while num < 3: # 用户可以尝试三次
username = input("username >>:").strip() # 去除输入后左右两边的空格,防止因为某些原因用户一开始输入时输入多个空格
if not username: # 用户名不能为空
print("username can not be empty")
continue
password = input("password >>:").strip()
if not password: # 密码不能为空
print("password can not be empty")
continue
dic = self.create_msg_to_send(action_type="login", username=username, password=password) # 制作信息头
self.send_msg(dic) # 发送信息头
dic = self.recv_msg() # 接收服务端的反馈
if dic.get("status_code") == 200: # 根据状态码判断用户是否登录成功,200 代表登陆成功
print("{} {} login succeed !!! {}".format("-" * 25, username, "-" * 25))
self.current_dir = dic.get("current_dir") # 记录用户在服务端的当前目录
self.username = username # 登录成功,记录用户登录的用户名
self.re_get() # 登录成功后,首先让用户选择是否需要对未完成的文件进行断点续存
self.handle() # 和服务端进行交互
else:
status_msg = dic.get("status_msg") # 登录失败,打印服务端反馈的状态信息
print("{} {} !!! {}".format("-" * 25, status_msg, "-" * 25))
num += 1 # 尝试次数减少一次
def handle(self):
"""和服务端交互"""
while True:
try:
# 用户输入交互命令,输入时的提示信息是用户目前在客户端的当前目录,一样需要去除空格
cmd = input("[{}]>>:".format(self.current_dir)).strip()
if not cmd: # 输入命令的不能控
print("{} command should not be empty !!! {}".format("-" * 25, "-" * 25))
continue
cmd_list = cmd.split(" ") # 对用户输入的内容进行按空格分割,分割之后的第一个词是交互命令名称
if hasattr(self, cmd_list[0]): # 如果客户端存在该方法
func = getattr(self, cmd_list[0])
func(cmd_list) # 执行该方法,并传入分割后的命令列表作为参数
else: # 用户输入的交互命令不存在
print("{} this command is not existed !!! {}".format("-" * 25, "-" * 25))
except Exception as e:
print(e) # 打印报错
def parameter_num_judgment(self, parameter_length, Min_num = None, Max_num = None, Exact_num = None):
"""命令参数个数判断"""
# 判断参数个数时候符合规范,可以输入最大参数个数,最小参数个数,准确的参数个数
if Min_num and Min_num > parameter_length:
print("{} the least number of parameter is {}, but you supply the {} parameter {}".format("-" * 25, Min_num, parameter_length, "-" * 25))
return False
if Max_num and Max_num < parameter_length:
print("{} the most number of parameter is {}, but you supply the {} parameter {}".format("-" * 25, Max_num, parameter_length, "-" * 25))
return False
if Exact_num and Exact_num != parameter_length: