用 Python实现一个ftp+CRT(不用ftplib)
转载请注明出处http://www.cnblogs.com/Wxtrkbc/p/5590004.html
本来最初的想法是实现一个ftp服务器,用来实现用户的登陆注册和文件的断点上传下载等,结果做着做着就连CRT也顺带着跟着完成了,然后就变成了这样一个'不伦不类'的工具。用到的知识有hashlib加密密码传输,面向对象,sockeserver支持多客户访问,os.subprocess来处理系统自带的命令,然后自定义上传下载命令,以及如何实现断点续传,传输过程中的粘包问题,以及如何反射处理用户的输入。这里仅仅是为了娱乐,下面先来看一下效果图
一、客户端
首先定义以个客户端类,大致在类里面定义了这些方法,以后有需要的话,可以对其进行扩充,下面的类一初始化就会去执行start方法,如果在服务端注册登录成功的话,就会接下来执行internet方法,等待用户的输入,
class Client: def __init__(self, address): self.address = Client.get_ip_port(address) self.help_message = [ '目前自定义的命令仅支持以下操作:\n' '\tput|filename', '\tget|filename', ] self.start() self.cwd = '' @staticmethod def get_ip_port(address): ip, port = address.split(':') return (ip, int(port)) def register(self): try_counts = 0 while try_counts < 3: user = input('请输入用户名:') if len(user) == 0: continue passwd = input('请输入用密码:') if len(passwd) == 0: continue pd = hashlib.sha256() pd.update(passwd.encode()) self.socket.sendall('register|{}:{}'.format(user, pd.hexdigest()).encode()) # 发送加密后的账户信息 ret = self.socket.recv(1024).decode() if ret == '202': print('注册成功请登录') os.mkdir(os.path.join(settings.USER_HOME_DIR, user)) # 在客户端也创建一个用户家目录 os.mkdir(os.path.join(settings.USER_HOME_DIR, user, 'download_file')) os.mkdir(os.path.join(settings.USER_HOME_DIR, user, 'upload_file')) return True else: try_counts += 1 sys.exit("Too many attemps") def login(self): try_counts = 0 while try_counts < 3: user = input('请输入用户名:') self.user = user if len(user) == 0: continue passwd = input('请输入用密码:') if len(passwd) == 0: continue pd = hashlib.sha256() pd.update(passwd.encode()) self.socket.sendall('login|{}:{}'.format(user, pd.hexdigest()).encode()) # 发送加密后的账户信息 ret = self.socket.recv(1024).decode() if ret == '200': print('登陆成功!') self.cwd = self.socket.recv(1024).decode() return True else: print('用户或密码错误,请从新登陆:') try_counts += 1 sys.exit("Too many attemps") def internet(self): pass<br> def process(self, cmd, argv): # 处理自定义的命令 pass<br> def help(self, argv=None): pass def put(self, argv=None): pass def get(self, argv=None): pass def start(self): self.socket = socket.socket() try: self.socket.connect(self.address) except Exception as e: sys.exit("Failed to connect server:%s" % e) print(self.socket.recv(1024).decode()) inp = input('1、注册,2、登录,3、离开: ') if inp == '1': if self.register(): if self.login(): # 登陆成功后进行交互操作 self.internet() elif inp == '2': if self.login(): self.internet() else: sys.exit() if __name__ == '__main__': # address = input('请输入FTP服务端地址(ip:port):') address = '127.0.0.1:9999' client = Client(address)
二、服务端
服务端是用socketserver来写的,以便出来多用户请求,每当用户来来请求的时候,先让其注册或登陆,注册完后,以用户的名字为其创建一个家目录,并将用户名和密码保存起来,将来用户登录的时候从db中取出数据和用户输入的密码进行对比,正确后让其进行下一步操作,用户密码输入三次以后,退出程序。服务端为了响应客户端的每一个操作,定义了一个函数专门接受客户端传来的每一次命令,然后对其分解,反射到具体的服务端具体的函数中去,这里需要先定义客户端传过来的数据的格式是 cmd|args。大家可以看到我上面客户端登陆认证的代码传输的数据格式 ('login|{}:{}'.format(user, pd.hexdigest())) ,这么做的道理就是为了服务端好统一进行处理。下面来一下代码具体怎么做的,
def handle(self): self.request.sendall('欢迎来到FTP服务器!'.encode()) while True: data = self.request.recv(1024).decode() if '|' in data: cmd, argv = data.split('|') else: cmd = data argv = None self.process(cmd, argv) # 将接受道德数据出来后在经过 process def process(self, cmd, argv=None): # 使用反射处理客户端传过来的命令(自定义的命令) if hasattr(self, cmd): func = getattr(self, cmd) func(argv) else: pass
这么一写后,就只需在服务端写上相应的函数,比如,注册的话,我就只需写一个函数名为register的函数,专门来处理注册,同理登陆的话,我也只需写一个login函数即可,当初我第一次写的时候,都是用if,else来判断,当时的代码写下来,自己看着都恐怖,写着写着,自己都不知道判断到哪里去了,想想就泪奔。而用反射的话,就不需要这些繁琐的步骤了,而且以后要添加功能的话,只需要写一个简单的函数即可。客户端的反射也这样做的,就不再重复了。
三、断点上传
如果只是文件上传的话,比较好写,但是如果要断点上传的话,就有些麻烦了,这里提供一种思路,那就是服务端纪录以上传文件的大小,下次上传的时候,如果要断点续传的话,先将已上传的文件大小发给客户端,然后客户端从断点的位置在上传。下面来看一下代码的实现,
# 客户端 def put(self, argv=None): if len(argv) == None: print("Please add the file path that you want to upload") return print('上传之前请确保的文件在用户upload文件夹下') file_path = os.path.join(settings.USER_HOME_DIR, self.user, 'upload_file', argv) if os.path.exists(file_path): #判断文件存不存在 file_size = os.stat(file_path).st_size file_info = { 'file_name': argv, 'file_size': file_size, } has_sent = 0 self.socket.sendall(('put|{}'.format(json.dumps(file_info))).encode()) # 将上传的文件信息作为参数发给服务端 ret = self.socket.recv(1024).decode() if ret == '204': inp = input("文件存在,是否续传?Y/N:").strip() if inp.upper() == "Y": self.socket.sendall('205'.encode()) has_sent = int(self.socket.recv(1024).decode()) else: self.socket.sendall('207'.encode()) with open(file_path, 'rb') as f: f.seek(has_sent) # 如果要续传的话,has_set为已经上传的大小,否则 has_set为0,从头开始上传 for line in f: self.socket.sendall(line) has_sent += len(line) k = int((has_sent / file_size * 100)) # 下面的代码是用来显示进度条 table_space = (100 - k) * ' ' flag = k * '*' time.sleep(0.05) sys.stdout.write('\r{} {:.0%}'.format((flag + table_space), (has_sent / file_size))) print() # 显示换行的作用
下面来看一下服务端的代码,服务端和客户端都是一收一发,注意要保持recv要收到信息,否则会阻塞,此外传送的时候可能会发生粘包,解决的办法是在发送文件文件前,先发送一条标志信息,当服务端收到该标志信息,就可以通知客户端发送文件了。
# 服务端 def put(self, argv=None): file_info = json.loads(argv) # 获取客户端传来的消息 file_name = file_info['file_name'] file_size = int(file_info['file_size']) file_path = os.path.join(settings.USER_HOME_DIR, 'kobe', 'upload_file', file_name) have_send = 0 # 已经上传的位置 if os.path.exists(file_path): self.request.sendall('204'.encode()) ret = self.request.recv(1024).decode() if ret == '205': # 续传 have_send = os.stat(file_path).st_size # 获取已经上传文件的大小 self.request.sendall(bytes(str(have_send), encoding='utf-8')) f = open(file_path, 'ab') # 续传的话,以a模式打开文件, else: f = open(file_path, 'wb') # 不续传的话,以w模式打开, else: self.request.sendall('206'.encode()) # 直接上传 f = open(file_path, 'wb') while True: if have_send == file_size: # 一旦接受到的内容等于文件大小,直接退出循环 break try: ret = self.request.recv(1024) except Exception as e: break f.write(ret) have_send += len(ret)
解决了断点续传,那么断点下载也是同样的道理,就不再重复讲了。
4、CRT
最后来讲一下怎么实现远程操作服务端主机,这里存粹是为了娱乐。其实实现起来也比较简单,主要使用了subprocess.getoutput获取来处理客户端输入的命令,然后将结果返回给客户端就可以,但是有一点致命的缺陷,那就是不支持cd命令,如果不支持cd命令的话,那还谈什么远程操作了,所以这里对于cd命令就需要特殊对待了,解决的办法,当然是找一个支持cd的命令,下面来看下服务端大代码
def process(self, cmd, argv=None): # 使用反射处理客户端传过来的命令(自定义的命令) if hasattr(self, cmd): func = getattr(self, cmd) func(argv) else: if cmd.startswith('cd'): #(处理cd命令,subprocess不支持cd命令) os.chdir(cmd.split(' ')[1]) # 获取cd 命令的参数,交给os.chdir来切换目录 pass else: data = subprocess.getoutput(cmd) # 其他命令,交给subprocess处理, data_length = len(data) if data_length != 0: pass else: pass
处理完相关的命令,将结果返会给客户端显示就可以了。最后吗客户端保存一个变量,用来保存当前的执行路径显示出来,就像[C:\Users\Tab\PycharmProjects\myftp\New_ftp\New_Server (help)]:这样。
五、总结
到这里,就基本将关键性的东西讲完了,其他的都是一些简单的操作,只要自己稍微注意一下,你就可以写出一个类似的东西。