一、要求:
开发一个支持多用户在线的FTP程序
1、用户md5加密认证;
2、允许同时多用户登录(socketserver);
3、执行命令:
客户端;ipconfig;
服务端:subprocess;
4、上传下载文件:
显示进度条;
断点续传;
二、程序目录结构:
三、程序运行顺序解构
如图,服务端和客户端分别创建各自的类实例对象之后,启动程序用户从注册开始,客户端从start()函数开始按顺序调用register()、auth()、interactive()函数;其中客户端的register()和auth()函数直接和服务端进行通信,其他函数通过interactive()分拆参数之后进行反射执行函数;服务端接收来自客户端的消息之后,通过分发函数反射执行对应的功能函数,如用户注册、登录、上传、下载功能等。如此,通过反射模式,服务端和客户端都可以通过扩展函数实现更多功能。
四、客户端上传代码通信
客户端上传功能,包括和服务端的四次通信应答,如下图;要注意的是,对于判断语句,可能会有两次接收或者发送通信,也有可能在判断之后可以忽略掉,如图中的第三次通信;下载功能原理一样。
1 def file_put(self, user_data): 2 """ 3 上传功能 4 服务端处理客户端命令 5 """ 6 # 第1次通信[接]:先序列化从客户端发送来的数据; 7 json_data = json.loads(user_data) 8 9 # 判断用户存储目录容量; 10 if not self.login_user.used_storage: 11 self.calculate_storage() 12 13 # 第2次通信[发]:发送服务端用户目录容量标志判断码给服务端; 14 if self.login_user.used_storage + json_data['file_size'] > self.login_user.storage_limit: 15 self.request.sendall(bytes(json.dumps({"response": "303"}), encoding="utf-8")) 16 else: 17 self.request.sendall(bytes(json.dumps({"response": "301"}), encoding="utf-8")) 18 19 # 获得当前目录绝对路径和文件大小; 20 file_abs_path = self.get_file_abs_path(json_data['file_name']) 21 total_file_size = int(json_data['file_size']) 22 23 # 定义已接收的值; 24 has_received = 0 25 26 # 如果目录文件存在; 27 if os.path.exists(file_abs_path): 28 29 # 第3.1.Y次通信[发]:若目录文件存在,则向客户端发送2003标志码; 30 self.request.sendall(bytes("2003", encoding="utf8")) 31 32 # 第3.1.1.Y/N次通信[接]:接收客户端回答,是否要继续上传的标志码; 33 is_continue = str(self.request.recv(1024), encoding="utf8") 34 if is_continue == "2004": 35 36 # 第3.1.2次通信[发]:继续上传,先发送已存在文件的大小; 37 has_file_size = os.stat(file_abs_path).st_size 38 self.request.sendall(bytes(str(has_file_size), encoding="utf8")) 39 40 # 此时,已接收的文件大小=已存在文件大小 41 has_received += has_file_size 42 43 # 打开文件,追加; 44 f = open(file_abs_path, "ab") 45 46 # 第3.1.1.N次通信[接]:接收客户端回答,是否要继续上传的标志码; 47 else: 48 49 # 打开文件,新建写入; 50 f = open(file_abs_path, "wb") 51 52 # 第3.1.N次通信[发];如果目录文件不存在,则发送2002,并在本地新建文件写入; 53 else: 54 self.request.sendall(bytes("2002", "utf8")) 55 f = open(file_abs_path, "wb") 56 57 # 第4次通信[接]:接收数据; 58 while has_received < total_file_size: 59 data = self.request.recv(1024) 60 f.write(data) 61 has_received += len(data) 62 print("ending") 63 f.close()
1 def instruction_put(self, instructions): 2 """ 3 客户端上传命令 4 :keyword instructions:用户输入的第二个参数 5 """ 6 print("开始上传!") 7 print(instructions) 8 print(len(instructions)) 9 print(instructions[1]) 10 11 # 判断参数个数; 12 if len(instructions) < 2: 13 print("请添加要上传的文件路径!") 14 return 15 else: 16 local_path = instructions[1] 17 18 # 先判断本地文件路径是否存在; 19 if os.path.exists(local_path): 20 print("即将上传文件: %s" % local_path) 21 file_name = os.path.basename(local_path) 22 file_size = os.path.getsize(local_path) 23 print(file_name, file_size) 24 25 # 第1次通信[发]:将上传命令序列化之后发送给服务端,告诉服务端要上传文件; 26 command_str = "file_put|%s" % (json.dumps({"file_name": file_name, "file_size": file_size})) 27 self.sock.sendall(bytes(command_str, encoding="utf-8")) 28 29 # 第2次通信[接]:接收服务端用户目录容量标志判断码给服务端,301或303; 30 ok = str(self.sock.recv(1024), encoding="utf8") 31 32 # 第3.1.Y/N次通信[接]:接收来自服务端的判断码,如果目录文件存在则标志码为2003; 33 result_exist = str(self.sock.recv(1024), encoding="utf8") 34 print(result_exist) 35 36 has_sent = 0 37 if result_exist == "2003": 38 inp = input("文件存在,是否续传?Y/N").strip() 39 40 # 第3.1.1.Y次通信[发]:客户端回答,2004表示要继续上传; 41 if inp.upper() == "Y": 42 self.sock.sendall(bytes("2004", encoding="utf8")) 43 44 # 第3.1.2次通信[接]:接收服务端已存在的文件大小 45 result_continue_pos = str(self.sock.recv(1024), encoding="utf8") 46 print(result_continue_pos) 47 has_sent = int(result_continue_pos) 48 49 else: 50 # 第3.1.1.N次通信[发]:客户端回答,2005表示不继续上传; 51 self.sock.sendall(bytes("2005", "utf8")) 52 53 # 以二进制打开,并拔到已发送位置 54 file_obj = open(local_path, "rb") 55 file_obj.seek(has_sent) 56 57 # 第4次通信[发]:发送数据; 58 while has_sent < file_size: 59 data = file_obj.read(1024) 60 self.sock.sendall(data) 61 has_sent += len(data) 62 # self.bar(has_sent, file_size) 63 self.progress(has_sent, file_size, 100) 64 65 file_obj.close() 66 print("上传成功") 67 68 else: 69 print("文件不存在!")
四、断点续传
继点续传主要原理是:服务端记录已上传文件的大小,下次上传的时候,如果要断点续传的话,先将已上传的文件大小发给客户端,然后客户端从断点的位置在上传;如图标记所示。
四、显示进度条
传输数据过程中,将已传输数据大小和文件数据总大小,传到进度条函数;代码如下。
1 def progress(self, num, sum, l): 2 """ 3 显示上传进度条 4 num:已上传大小 5 sum:文件总大小 6 l:定义进度条大小 7 """ 8 bar_length = l # 定义进度条大小 9 percent = float(num) / float(sum) 10 hashes = '=' * int(percent * bar_length) # 定义进度显示的数量长度百分比 11 spaces = ' ' * (bar_length - len(hashes)) # 定义空格的数量=总长度-显示长度 12 sys.stdout.write( 13 "\r上传中: %.2fM/%.2fM %d%% [%s] " % (num / 1048576, sum / 1048576, percent * 100, hashes + spaces)) # 输出显示进度条 14 sys.stdout.flush() # 强制刷新到屏幕