作业需求
要求:
- 用户加密认证 1
- 允许同时多用户登录 1
- 每个用户有自己的家目录 ,且只能访问自己的家目录 1
- 对用户进行磁盘配额,每个用户的可用空间不同
- 允许用户在ftp server上随意切换目录 1
- 允许用户查看当前目录下文件 1
- 允许上传和下载文件,保证文件一致性 1
- 文件传输过程中显示进度条 1
- 附加功能:支持文件的断点续传---------------------未完成
作业分析
作业结构:
client:
server:
小说明:
在这个作业中呢,用到了很多知识点,比如用户加密认证、加密传送文件都用到了hashlib模块;保存用户信息,传递一些字典用到了json模块;多用户同时在线用到了socketserver模块;还有一些命令的操作,用到了os.popen()函数;还有一些有关路径的函数,什么的。。。诶,好像也没用到好多哈。。。对了还有反射,这个非常重要。其他就没什么的了,,具体的分析看下面的一个网站:
精彩内容:client端代码
import socket,hashlib,json,os ,sysbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))print(base_dir)sys.path.append(base_dir) # 添加conf文件的路径\\from conf import configfrom core import put, auth, ls, cd, mkdirhttpCode = """"200" : "服务器成功返回网页""404" : "请求的网页不存在""403" : "出现错误""503" : "服务不可用" """class FtpClient(object): def __init__(self): self.client = socket.socket() # 创建一个socket客户端 self.userMessDirt = {} def connect(self,host,port): self.client.connect((host, port)) # 客户端和服务器端进行连接 def help(self): msg = ''' ls pwd cd ../.. get filename put filename ''' print(msg) def interactive(self): self.userMessDirt = json.loads(self.client.recv(config.buffer).decode()) self.localDir = os.path.basename(self.userMessDirt["homeDir"]) # 初始地址 while True: cmd_str = input("[%s@user %s] #" %(self.userMessDirt["userName"], self.localDir)) if len(cmd_str) ==0:continue cmd = cmd_str.split()[0] if hasattr(self,"cmd_%s" % cmd): func = getattr(self, "cmd_%s" % cmd) func(cmd_str) else: self.help() def cmd_ls(self, *args): '''客户端实现打印出该目录下的所有文件''' ls.ls(self, *args) def cmd_pwd(self, *args): '''客户端实现打印出当前目录功能''' pass def cmd_cd(self, *args): '''客户端实现切换目录功能''' cd.cd(self, *args) def cmd_get(self, *args): '''客户端下载文件''' pass def cmd_put(self, *args): '''客户端上传文件''' put.cmd_put(self,*args) def cmd_mkdir(self, *args): '''创建一个空文件夹''' mkdir.mkdir(self, *args) def auth_login(self): auth.auth_login(self) def auth_register(self): auth.auth_register(self)def run(): info = """ 1.登录 2.注册 >>:""" auth_choice = input(info).strip() ftp = FtpClient() ftp.connect("localhost",9090) if auth_choice == "1": ftp.auth_login() else: ftp.auth_register() ftp.interactive()
import hashlib, os, sysbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))from conf import configdef auth_login(self): self.client.send("login".encode("utf-8")) # 通知对面我要登录了 loginErrorCount = 3 while True: userName_input = self.client.recv(config.buffer) userName = input(userName_input.decode()).strip() self.client.send(userName.encode("utf-8")) # 注意这里的编码问题 httpCode = self.client.recv(config.buffer).decode() if httpCode == "403": print("用户名错误,请重新输入") elif httpCode == "200": break while loginErrorCount: has = hashlib.md5() passWord_input = self.client.recv(config.buffer) passWord = input(passWord_input.decode()).strip() has.update(passWord.encode("utf-8")) # 使用hashlib编码必须要使用byte类型 self.client.send(has.hexdigest().encode("utf-8")) # print("has:",has.hexdigest()) # 测试是否生成了md5 # self.client.send(self.passWord.encode("utf-8")) http_code = self.client.recv(config.buffer).decode() if http_code == "200": break elif http_code == "403": loginErrorCount -= 1 print("密码错误,剩余\033[31;1m%s\033[0m机会" % loginErrorCount) if loginErrorCount == 0: sys.exit(0)def auth_register(self): self.client.send("register".encode("utf-8")) # 通知对面我要注册了 has = hashlib.md5() has_1 = hashlib.md5() userName_input = self.client.recv(config.buffer) userName = input(userName_input.decode()).strip() self.client.send(userName.encode("utf-8")) while True: passWord_input = self.client.recv(config.buffer) passWord = input(passWord_input.decode()).strip() has.update(passWord.encode("utf-8")) # 对第一次输入的密码加密 self.client.send(has.hexdigest().encode("utf-8")) passWord_input_again = self.client.recv(config.buffer) passWord_again = input(passWord_input_again.decode()).strip() has_1.update(passWord_again.encode("utf-8")) # 对第一次输入的密码加密 self.client.send(has_1.hexdigest().encode("utf-8")) http_code = self.client.recv(config.buffer).decode() if http_code == "200": break elif http_code == "403": print("密码错误。。。")
import json, osbase_dir = os.path.dirname(os.path.abspath(__file__))from conf import configdef cd(self, *args): '''cd 中.. 是返回上层目录,/ ''' cmd_str = args[0].split() if len(cmd_str) > 1: cmd_path = cmd_str[1] cmd_dirt = { "action" : "cd", "path" : cmd_path } self.client.send(json.dumps(cmd_dirt).encode("utf-8")) self.localDir = os.path.basename(self.client.recv(config.buffer).decode())
ps:在这里说一下,貌似真的是各种命令的实行大多一样,只要做出来了一个,其他的就能做了
import sysdef progress_bar(num_cur, total): ratio = float(num_cur) / float(total) percentage = int(ratio * 100) r = '\r\n[%s%s]%d%%' % (">"*percentage, " "*(100-percentage), percentage ) sys.stdout.write(r) sys.stdout.flush()
import os, json, hashlib, sysbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))from core import progress_barfrom conf import configdef cmd_put(self, *args): cmd_str = args[0].split() # 提取出args中的数据,并将其按空格的形式转换成列表 if len(cmd_str) > 1: fileName = cmd_str[1] if os.path.isfile(fileName): # 判断这是不是个文件 fileSize = os.stat(fileName).st_size # 获得文件的大小 cmd_dirt = { "action": "put", "fileName": fileName, "fileSize": fileSize, "overridden": True } self.client.send(json.dumps(cmd_dirt).encode("utf-8")) # 用json将整个字典传过去 self.client.recv(config.buffer) # 等待服务器端的确认, 不用做任何处理 with open(fileName, "rb") as fr: has = hashlib.md5() sum_size = 0 for line in fr: line_size = len(line) sum_size += line_size has.update(line) self.client.send(has.hexdigest().encode("utf-8")) # line 本身就是字节类型 return_md5 = self.client.recv(config.buffer) if has.hexdigest() == return_md5.decode(): self.client.send(b'200') self.client.recv(config.buffer) self.client.send(line) progress_bar.progress_bar(sum_size, cmd_dirt["fileSize"]) else: self.client.send(b'403') print("传输错误。。。") break else: print("\nfile upload success...") else: print(fileName, "不存在。。。")
# lsimport json, osbase_dir = os.path.dirname(os.path.abspath(__file__))from conf import configdef ls(self, *args): cmd_str = args[0].split() if len(cmd_str) == 1 and cmd_str[0] == "ls": cmd_dirt = { "action" : "ls", } self.client.send(json.dumps(cmd_dirt).encode("utf-8")) return_mess = self.client.recv(config.buffer) print(return_mess.decode())# mkdirimport json, osbase_dir = os.path.dirname(os.path.abspath(__file__))from conf import configdef mkdir(self, *args): cmd_str = args[0].split() if len(cmd_str) > 1: fileName = cmd_str[1] cmd_dirt = { "action" : "mkdir", "fileName" : fileName, } self.client.send(json.dumps(cmd_dirt).encode("utf-8")) self.client.recv(config.buffer)
import os,sys,logging# 最顶端的路径BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))# 存放日志的目录LOG_DIR = BASE_DIR + '/log/log.txt'logging.basicConfig(level=logging.DEBUG, # level=loggin.INFO意思是,把日志纪录级别设置为INFO,也就是说,只有比日志是INFO或比INFO级别更高的日志才会被纪录到文件里 format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', filename=LOG_DIR, filemode='a+')# 存放用户信息的目录USER_DIR = BASE_DIR + '/date/'# 每次发送文件的大小buffer = 2048# 给用户分配的内存大小memory = 100 * 1024 * 1024
server代码
import socketserver,os,sys,jsonbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))print(base_dir)sys.path.append(base_dir ) # 添加conf文件的路径from conf import configfrom core import put, auth, ls, cd, mkdirclass MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): # 所有的请求都是在handle 中执行的 try: self.userMessDirt = {} state = self.request.recv(config.buffer).decode() if state == "login": self.auth_login() elif state == "register": self.auth_register() self.request.send(json.dumps(self.userMessDirt).encode("utf-8")) self.current_path = os.path.join(self.userMessDirt["homeDir"]) os.chdir(self.current_path) # 将当前的目录改到self.current_path下面去,这里是指每个用户的root目录 while True: cmd_dirt = json.loads(self.request.recv(config.buffer).decode()) action = cmd_dirt["action"] if hasattr(self, action): func = getattr(self, action) func(cmd_dirt) except ConnectionResetError as e: print(self.userMessDirt["userName"], "断开。。。") def ls(self, *args): '''查看该目录下的所有文件''' ls.ls(self, *args) def pwd(self, *args): '''打印当前路径''' pass def cd(self, *args): '''切换目录''' cd.cd(self,*args) def get(self, *args): '''服务器端 发送文件给 客户端''' pass def mkdir(self, *args): '''创建一个空的文件夹''' mkdir.mkdir(self, *args) def put(self, *args): '''服务器端 接收 客户端给的文件''' put.put(self, *args) def auth_login(self): auth.auth_login(self) def auth_register(self): auth.auth_register(self)def run(): Host , Port = "localhost" , 9090 # ip地址 server = socketserver.ThreadingTCPServer((Host,Port) , MyTCPHandler) # 每来一个请求,服务端就开启一个新的线程 server.serve_forever()
import os,sys,logging# 最顶端的路径BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))# 存放日志的目录LOG_DIR = BASE_DIR + '/log/log.txt'logging.basicConfig(level=logging.DEBUG, # level=loggin.INFO意思是,把日志纪录级别设置为INFO,也就是说,只有比日志是INFO或比INFO级别更高的日志才会被纪录到文件里 format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', filename=LOG_DIR, filemode='a+')# 存放用户信息的目录USER_DIR = BASE_DIR + '/date/'# 每次发送文件的大小buffer = 2048# 给用户分配的内存大小memory = 100 * 1024 * 1024# 家目录的路径HOME_DIR = BASE_DIR + '\home\\'
import os, sys, jsonbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))from conf import configdef dirt(userName, passWord, memory, homeDir): return { "userName":userName, "passWord":passWord, "memory" : memory, "homeDir" : homeDir }def auth_login(self): while True: self.request.send('请输入用户名:'.encode("utf-8")) userName = self.request.recv(config.buffer) try: with open(config.USER_DIR + userName.decode(), "r", encoding="utf-8") as fr: self.userMessDirt = json.load(fr) # 记住json只能dump&load一次,所以放在循环里的时候注意 self.request.send(b'200') break except FileNotFoundError as e: self.request.send(b'403') loginErrorCount = 3 while loginErrorCount: self.request.send('请输入密码:'.encode("utf-8")) passWord_md5 = self.request.recv(config.buffer) # print("md5:",passWord_md5.decode()) if self.userMessDirt["passWord"] == passWord_md5.decode(): self.request.send(b"200") break else: self.request.send(b"403") loginErrorCount -= 1 if loginErrorCount == 0: sys.exit(0)def auth_register(self): self.request.send('请输入用户名:'.encode("utf-8")) userName = self.request.recv(config.buffer).decode() # 一定要记得解码解码 while True: self.request.send('请输入密码:'.encode("utf-8")) passWord_md5 = self.request.recv(config.buffer).decode() self.request.send('请再次输入密码:'.encode("utf-8")) passWord_again_md5 = self.request.recv(config.buffer).decode() if passWord_md5 == passWord_again_md5: self.request.send(b'200') os.makedirs(config.HOME_DIR + userName + "\\root\\") # 为用户创建家目录 self.userMessDirt = dirt(userName, passWord_md5, config.memory, config.HOME_DIR + userName + "\\root") with open(config.USER_DIR + userName, "w") as fw: json.dump(self.userMessDirt, fw) # 将用户的信息存到文件中 break else: self.request.send(b'403')
# cdimport osdef cd(self, *args): path_list = self.current_path.split("\\") cmd_str = args[0] cmd_path = cmd_str["path"] if cmd_path == "/": path_list_len = len(path_list) while True: if path_list[path_list_len - 1] == self.userMessDirt["userName"]: break else: path_list.pop() path_list_len -= 1 elif cmd_path == "..": path_list.pop() else: cmd_path_list = cmd_path.split("\\") path_list += cmd_path_list cmd_current_path = "\\".join(path_list) if os.path.exists(cmd_current_path): self.current_path = cmd_current_path os.chdir(self.current_path) print(self.current_path) self.request.send(self.current_path.encode("utf-8"))# lsimport osdef ls(self, *args): cmd_str = args[0] mess = os.popen("dir" ).read() self.request.send(mess.encode("utf-8"))# mkdirimport osdef mkdir(self, *args): cmd_str = args[0] fileName = cmd_str["fileName"] # 需要创建的文件名字 os.popen("mkdir %s" %fileName) self.request.send(b'200')
import os, jsonbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))from conf import configdef put(self, *args): cmd_dirt = args[0] fileName = cmd_dirt["fileName"] fileSize = cmd_dirt["fileSize"] overridden = cmd_dirt["overridden"] if overridden: fw = open(fileName, "wb") else: fw = open(fileName + '.new', 'wb') self.request.send(b"200") # 确认准备好接收文件了 fileReceive = 0 while fileSize > fileReceive: date_md5 = self.request.recv(config.buffer) self.request.send(date_md5) http_code = self.request.recv(config.buffer).decode() if http_code == "200": self.request.send(b"ok") if (fileSize - fileReceive) > config.buffer: date = self.request.recv(config.buffer) else: date = self.request.recv(fileSize - fileReceive) fw.write(date) fileReceive += len(date) elif http_code == "403": print("文件错误。。。") break else: fw.close() print(fileName, "接收完毕。。。")
其他的也没什么可说的了,这个作业真心做了好几天,多亏了上面的博主发的博客,才有点头绪能够写下去