目录
1.FTP程序所需要的知识点
2.FTP程序具体实现过程
2.1 FTP程序之注册功能
2.2 FTP程序之登录功能
2.3 FTP程序之下载功能
3.FTP程序源代码
FTP程序所需要的知识点
1.socketserver并发编程
2.连续send,recv黏包现象:struct
3.hashlib模块的md5加密
4.静态方法staticmethod和类方法classmethod
5.json序列化
6.反射:hasattr,setattr
7.os模块相关方法
FTP程序具体实现过程
FTP程序之注册功能
1.要明确,FTP程序是要实现服务端的并发的,所以需要引入socketserver模块来实现并发
2.写服务端下socketserver的基本语法[day31:socketserver的基本语法]
# 服务端 import socketserver class FTPServer(socketserver.BaseRequestHandler): def handle(self): pass myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer) myserver.serve_forever() # 客户端 import socket sk = socket.socket() sk.connect(("127.0.0.1",9000)) sk.close()
3.用户需要自己输入账号和密码,所以在客户端需要写输入用户名和密码的方法(输入用户名和密码后,发送给服务端)
4.在客户端定义auth方法,先写两个input输入用户名和密码
5.输入完用户名密码之后,怎样将用户信息传给服务端呢?
将用户名和密码以及操作做成一个字典,并用json序列化成字符串,并encode后,使用sk.send()发送给服务端
这部分的具体代码如下所示:
# 客户端 def auth(opt): usr = input("username:").strip() pwd = input("password:").strip() dic = {"user":usr,"passwd":pwd,"operate":opt} str_dic = json.dumps(dic) # 将字典序列化成字符串 sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去
auth("register")
6.服务端已经将用户名密码和操作发过去了,所以现在服务端需要接收一下,服务端的整体逻辑写在类中的handle方法中
再定义一个专门用来接收的方法myrecv,并使用handle方法去调用myrecv方法
这部分的具体代码如下所示:
# 服务端 class FTPServer(socketserver.BaseRequestHandler): def handle(self): opt_dic = self.myrecv() print(opt_dic) def myrecv(self): info = self.request.recv(1024) opt_str = info.decode() opt_dic = json.loads(opt_str) return opt_dic
通过以上步骤,我们实现了一収一发
7.接收到了客户端发来的数据,我们就可以在服务端写一些关于注册的逻辑了
在服务端定义Auth类,专门用来实现注册登录,在handler方法也可以去调用类中的成员
那么Auth类中应该写什么呢?
1.首先在当前目录创建db文件夹,并在db问文件夹中创建userinfo.txt用来存放用户名和密码
2.对密码使用md5加密
8.在Auth类中定义md5方法,用来对密码进行一个加密操作
# 服务端 class Auth(): def md5(usr,pwd): md5_obj = hashlib.md5(usr.encode()) md5_obj.update(pwd.encode()) return md5_obj.hexdigest()
我们先加密一份数据存放到userinfo.txt中
9.现在已经对每个用户名的密码加密了,但是还有一个问题需要考虑,在注册的时候,不能注册已经存在的用户名,所以需要对用户名进行判断
10.定义register方法,并使用classmethod装饰器,当其他类调用register方法时,会自动传递类参数.
11.拼接出一个userinfo所在文件的完整路径
1.首先获取当前文件(server.py)所在的位置
两种方法:
方法一:os.getcwd()
方法二:os.path.dirname(__file__)
print(os.getcwd()) # F:\OldBoyPython\week6\day36 print(__file__) # F:/OldBoyPython/week6/day36/ceshi.py print(os.path.dirname(__file__)) # F:/OldBoyPython/week6/day36
2.使用os.path.join进行路径拼接
base_path = os.getcwd() userinfo = os.path.join(base_path,"db","userinfo.txt") print(userinfo) # F:\OldBoyPython\week6\day36\db\userinfo.txt
这样,我们就获取到了userinfo.txt的绝对路径了
12.当有了userinfo.txt的绝对路径后,我们就可以开始文件操作了
在第9步,我们说到要检测用户名是否存在,现在我们就可以实现了
当用户名存在时,返回一个状态False和一个用户名已存在信息提示
@classmethod def register(cls, opt_dic): with open(userinfo, mode='r', encoding='utf-8') as fp: for line in fp: username = line.split(":")[0] if username == opt_dic["user"]: return {"result": False, "info": "用户名存在了"}
13.用户名存在的逻辑已经写完,接下来就是用户名可以使用的逻辑
要注意:密码需要加密后再写入
with open(userinfo, mode='a+', encoding='utf-8') as fp: # 账号就是字典的账号,密码使用md5加密处理后再写入文件 strvar = "%s:%s\n" % (opt_dic["user"], cls.md5(opt_dic["user"], opt_dic["passwd"])) fp.write(strvar)
如果登录成功了,返回一个状态True和一个注册成功信息提示
到此,注册部分的逻辑就已经写完了,具体代码如下所示:
@classmethod def register(cls, opt_dic): # 1.检测注册的用户是否存在 with open(userinfo, mode='r', encoding='utf-8') as fp: for line in fp: username = line.split(":")[0] if username == opt_dic["user"]: return {"result": False, "info": "用户名存在了"} # 2.当前用户可以注册 with open(userinfo, mode='a+', encoding='utf-8') as fp: # 账号就是字典的账号,密码使用md5加密处理后再写入文件 strvar = "%s:%s\n" % (opt_dic["user"], cls.md5(opt_dic["user"], opt_dic["passwd"])) fp.write(strvar) # 3.返回一个注册成功的状态 return {"result": True, "info": "注册成功"}
14.注册的register方法已经写完,但是现在我们需要将register方法和下面的FTPServer类建立联系,这个时候就需要使用反射来实现了
换句话来说:就是想在FTPServer的handle方法中使用Auth中的register方法
15.构建出反射,代码如下所示
到目前为止,基本的代码已经实现,现进行测试,代码如下所示
# 服务端 import socketserver import json import hashlib import os # 找当前数据库文件所在的绝对路径 base_path = os.getcwd() # F:\OldBoyPython\week6\day36\db\userinfo.txt userinfo = os.path.join(base_path,"db","userinfo.txt") class Auth(): @ staticmethod def md5(usr,pwd): md5_obj = hashlib.md5(usr.encode()) md5_obj.update(pwd.encode()) return md5_obj.hexdigest() @ classmethod def register(cls,opt_dic): # 1.检测注册的用户是否存在 with open(userinfo,mode='r',encoding='utf-8') as fp: for line in fp: username = line.split(":")[0] if username == opt_dic["user"]: return {"result":False,"info":"用户名存在了"} # 2.当前用户可以注册 with open(userinfo, mode='a+', encoding='utf-8') as fp: # 账号就是字典的账号,密码使用md5加密处理后再写入文件 strvar = "%s:%s\n" % (opt_dic["user"],cls.md5(opt_dic["user"],opt_dic["passwd"])) fp.write(strvar) # 3.返回一个注册成功的状态 return {"result":True,"info":"注册成功"} class FTPServer(socketserver.BaseRequestHandler): def handle(self): opt_dic = self.myrecv() print(opt_dic) if hasattr(Auth,"register"): res = getattr(Auth,"register")(opt_dic) print(res) def myrecv(self): info = self.request.recv(1024) opt_str = info.decode() opt_dic = json.loads(opt_str) return opt_dic myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer) myserver.serve_forever()
# 客户端 import socket import json sk = socket.socket() sk.connect(("127.0.0.1",9000)) # 处理収发数据的逻辑 def auth(opt): usr = input("username:").strip() pwd = input("password:").strip() dic = {"user":usr,"passwd":pwd,"operate":opt} str_dic = json.dumps(dic) # 将字典序列化成字符串 sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去 auth("register") sk.close()
运行结果如下图所示
客户端输入用户名和密码
服务端接收到客户端发来的数据
并且userinfo.txt也已经写入了你刚才在客户端输入的用户名和密码
16.在服务端我们可以看到注册成功/注册失败的信息了,现在我们想把这个信息发回给客户端,在客户端也能显示出来
和服务端的myrecv方法一样,我们需要自定义一个接収方法mysend
既然在服务端发数据,当然要在客户端接收数据
好的,到此第一部分注册功能就全部完成了。让我们看一下运行结果
所有的信息都应该是显示在客户端上的
FTP程序之登录功能
1.现在添加了登录功能,所以反射的时候就要动态起来。
2.Auth类中只有注册和登录两个方法,如果用户在客户端传入其他方法,必须要给予错误的提示
下面,我们来测试一下结果
3.现在就可以开始写登录函数的逻辑了。。。
登录嘛,肯定是要验证用户名和密码的,所以肯定需要从userinfo.txt中取出用户名和密码进行比对
所以先进行文件操作,将用户名和密码取出来,在进行验证
@ classmethod def login(cls,opt_dic): with open(userinfo,mode='r',encoding='utf-8') as fp: for line in fp: username,password = line.strip().split(":") if username == opt_dic["user"] and password == cls.md5(opt_dic["user"],opt_dic["passwd"]): return {"result":True,"info":"登陆成功"} return {"result":False,"info":"登录失败"}
其他的部分都不用改,定义了login函数,FTPServer就会自己识别是什么操作,并且通过反射获取到对应方法的返回值,将返回值发送给客户端,然后客户端接收后,打印出来
运行结果如下图所示
4.到此,登录部分的逻辑也已经完成了!!
但是在客户端调用时,还是非常死板的
这种调用方式非常的lowb,所以需要改进一下。。
我们需要搞一个界面。
5.先在客户端定义login函数和register函数,在函数里进行调用。
6.除了登录和注册函数,还需要搞一个退出的功能
在客户端定义myexit函数,用来实现退出的功能
现在我们在客户端已经定义了退出函数,但是在服务端我们也要让服务端知道退出的状态。
我们在客户端发送了一个opt_dic给服务端,然后服务端接收这个opt_dic
到此,退出功能就已经实现完了。
7.现在我们需要把登录,注册和退出形成一套界面
def main(): # 生成菜单界面 for i,tup in enumerate(operate_lst,start=1): print(i,tup[0]) # 输入相应序号,实现对应操作 num = int(input("请选择您要进行的操作>>>")) res = operate_lst[num-1][1]() return res # 将对应操作的返回值返回出来 while True: res = main() # 调用main获取到对应的返回值 print(res)
在客户端我们可以通过while True实现循环调用main,进而可以进行循环登录注册和退出。
那么在服务端我们也应该是循环进行调用注册登录和退出
所以需要在服务端也加上一个while True
8.到此为止,登录,注册和退出的功能就都已经实现了。
代码如下所示
# 服务端 import socketserver import json import hashlib import os # 找当前数据库文件所在的绝对路径 base_path = os.path.dirname(__file__) # /mnt/hgfs/python31_gx/day36/db/userinfo.txt userinfo = os.path.join(base_path,"db","userinfo.txt") class Auth(): @staticmethod def md5(usr,pwd): md5_obj = hashlib.md5(usr.encode()) md5_obj.update(pwd.encode()) return md5_obj.hexdigest() @classmethod def register(cls,opt_dic): # 1.检测注册的用户是否存在 with open(userinfo,mode="r",encoding="utf-8") as fp: for line in fp: username = line.split(":")[0] if username == opt_dic["user"]: return {"result":False,"info":"用户名存在了"} # 2.当前用户可以注册 with open(userinfo,mode="a+",encoding="utf-8") as fp: strvar = "%s:%s\n" % ( opt_dic["user"] , cls.md5( opt_dic["user"],opt_dic["passwd"] ) ) fp.write(strvar) """ 当用户上传的时候,给他创建一个专属文件夹,存放数据 """ # 3.返回状态 return {"result":True,"info":"注册成功"} @classmethod def login(cls,opt_dic): with open(userinfo , mode="r" , encoding="utf-8") as fp: for line in fp: username,password = line.strip().split(":") if username == opt_dic["user"] and password == cls.md5( opt_dic["user"] , opt_dic["passwd"] ) : return {"result":True,"info":"登录成功"} return {"result":False,"info":"登录失败"} @classmethod def myexit(cls,opt_dic): return {"result":"myexit"} class FTPServer(socketserver.BaseRequestHandler): def handle(self): while True: opt_dic = self.myrecv() print(opt_dic) # {'user': 'wangwen', 'passwd': '111', 'operate': 'register'} if hasattr(Auth,opt_dic["operate"]): # print( getattr(Auth,"register") ) res = getattr(Auth,opt_dic["operate"])(opt_dic) # login(opt_dic) # 如果接受的操作是myexit,代表退出 if res["result"] == "myexit": return # 把注册的状态发送给客户端 self.mysend(res) else: dic = {"result":False,"info":"没有该操作"} self.mysend(dic) # 接收方法 def myrecv(self): info = self.request.recv(1024) opt_str = info.decode() opt_dic = json.loads(opt_str) return opt_dic # 发送方法 def mysend(self,send_info): send_info = json.dumps(send_info).encode() self.request.send(send_info) # 设置一个端口可以绑定多个程序 # socketserver.TCPServer.allow_reuse_address = True myserver = socketserver.ThreadingTCPServer( ("127.0.0.1",9000) , FTPServer) myserver.serve_forever()
# ### 客户端 import socket import json """""" sk = socket.socket() sk.connect( ("127.0.0.1",9000) ) # 处理收发数据的逻辑 def auth(opt): usr = input("username: ").strip() pwd = input("password: ").strip() dic = {"user":usr,"passwd":pwd,"operate":opt} str_dic = json.dumps(dic) # 发送数据 sk.send(str_dic.encode("utf-8")) # 接受服务端响应的数据 file_info = sk.recv(1024).decode() file_dic = json.loads(file_info) return file_dic # 注册 def register(): res = auth("register") return res # 登录 def login(): res = auth("login") return res # 退出 def myexit(): opt_dic = {"operate":"myexit"} sk.send(json.dumps(opt_dic).encode()) exit("欢迎下次再来") # 第一套操作界面 # 0 1 2 operate_lst1 = [ ("登录",login) ,("注册",register) , ("退出",myexit) ] """ 1.登录 2.注册 3.退出 1 ('登录',) 2 ('注册', """ def main(): for i,tup in enumerate(operate_lst1,start=1): print(i , tup[0]) num = int(input("请选择执行的操作>>> ").strip()) # 1 2 3 # 调用函数 # print(operate_lst1[num-1]) ('退出',) 3 ('退出', ) ) # print(operate_lst1[num-1][1])# operate_lst1[num-1][1]() myexit() res = operate_lst1[num-1][1]() return res while True: # 开启第一套操作界面 res = main() print(res) sk.close()
执行结果如下图所示
FTP注册之下载功能
1.当你登录成功后,要跳转到另一套界面,让用户选择下载上传还是退出
所以我们需要像登录注册退出那套界面逻辑一样,再搞一个operate_lst2
只有登录成功的时候,才能出现第二套界面。
2.客户端现在已经发送过去了,那么对应的服务端也应该有所接收
3.download我们后面再说,先把界面2的退出搞定
同理,客户端的myexit有exit()直接终止程序,在服务端也要及时终止程序
直接搞上一个return,连循环加函数全都退出
到此,界面2的退出也已经搞定了,接下来就搞最复杂的download
4.下载,先搞一下这个客户端
在客户端定义一个download方法,定义一个字典,字典里写入操作和下载的文件名
5.客户端定义了下载方法将字典发送过去,服务端也应该定义download下载方法来接收这个字典并进行逻辑操作
# 服务端 def download(self, opt_dic): filename = opt_dic["filename"] # 获取用户在客户端输入的文件名 file_abs = os.path.join(base_path, "video", filename) # 获取到要下载视频的绝对路径 if os.path.exists(file_abs): # 如果文件存在 dic = {"result": True, "info": "文件存在,可以下载"} self.mysend() else: # 如果文件不存在 pass
6.如果文件存在可以下载,那么就可以执行下载的流程了
在下载时,服务端需要将视频发送给客户端,因为视频很大,且需要分段发送,所以可能会存在黏包现象。
所以需要引入struct模块,并改造mysend方法,以解决黏包现象
# 服务端 def mysend(self, send_info, sign=False): send_info = json.dumps(send_info).encode() if sign: # 1.发送数据的长度 res = struct.pack("i", len(send_info)) self.request.send(res) # 2.发送真实的数据 self.request.send(send_info)
# 客户端 def myrecv(info_len=1024,sign=False): if sign: # 1.接受数据的长度 info_len = sk.recv(4) info_len = struct.unpack("i",info_len)[0] # 2.接受真实的数据 file_info = sk.recv(info_len).decode() file_dic = json.loads(file_info) return file_dic
7.客户端向服务端发送下载操作和要下载的文件名,服务端接收到文件名称,返回一个可以下载的状态给客户端
8.刚才服务端已经将文件存在,可以下载的提示信息发给客户端了,接下来服务端要发送客户端要下载的视频的文件名字和文件大小
9.现在该发的都发了,最后一步就是发送真实的内容了
10.现在几乎是已经大功告成了,还差最后一点小瑕疵
在登录功能的第7步,我们说到,要想进行循环操作(循环选择下载上传和退出),需要在客户端和服务端加while True
11.到此!!所有功能实现完毕
运行结果如下图所示
这个时候,我们去download文件夹,可以查看到下载的视频
FTP程序源代码
客户端
# 客户端 import socket import json import struct import os sk = socket.socket() sk.connect(("127.0.0.1",9000)) def myrecv(info_len=1024,sign=False): if sign: info_len = sk.recv(4) info_len = struct.unpack("i",info_len)[0] file_info = sk.recv(info_len).decode() file_dic = json.loads(file_info) return file_dic # 处理収发数据的逻辑 def auth(opt): usr = input("username:").strip() pwd = input("password:").strip() dic = {"user":usr,"passwd":pwd,"operate":opt} str_dic = json.dumps(dic) # 将字典序列化成字符串 sk.send(str_dic.encode()) # 将字符串转化成字节流并发送出去 return myrecv() def login(): res = auth("login") return res def register(): res = auth("register") return res def myexit(): opt_dic = {"operate":"myexit"} sk.send(json.dumps(opt_dic).encode()) exit("欢迎下次再来") def download(): operate_dict = { "operate":"download", "filename":"ceshi123.mp4" } # 把要下载的文件名称传递给服务端 operate_str = json.dumps(operate_dict) sk.send(operate_str.encode("utf-8")) # 接受服务端发过来的数据(是否可以操作) res = myrecv(sign=True) print(res) # 1.如果收到了服务端的可以下载的提示,就创建一个文件夹用来存放下载的视频 if res["result"]: try: os.mkdir("mydownload") except: pass else: print("没有该文件") # 2.接受文件名字和文件大小 dic = myrecv(sign=True) print(dic) # 3.接収真实的文件 with open("./mydownload/" + dic["filename"],mode='wb') as fp: while dic["filesize"]: content = sk.recv(102400) fp.write(content) dic["filesize"] -= len(content) print("客户端下载完毕") operate_lst1 = [("注册",register), ("登录",login), ("退出",myexit)] operate_lst2 = [("下载",download), ("退出",myexit)] def main(operate_lst): for i,tup in enumerate(operate_lst,start=1): print(i,tup[0]) num = int(input("请选择您要进行的操作>>>")) res = operate_lst[num-1][1]() return res while True: res = main(operate_lst1) if res["result"]: while True: res = main(operate_lst2) sk.close()
服务端
# 服务端 import socketserver import json import hashlib import os import struct # 找当前数据库文件所在的绝对路径 base_path = os.getcwd() # F:\OldBoyPython\week6\day36\db\userinfo.txt userinfo = os.path.join(base_path,"db","userinfo.txt") class Auth(): @ staticmethod def md5(usr,pwd): md5_obj = hashlib.md5(usr.encode()) md5_obj.update(pwd.encode()) return md5_obj.hexdigest() @ classmethod def register(cls,opt_dic): # 1.检测注册的用户是否存在 with open(userinfo,mode='r',encoding='utf-8') as fp: for line in fp: username = line.split(":")[0] if username == opt_dic["user"]: return {"result":False,"info":"用户名存在了"} # 2.当前用户可以注册 with open(userinfo, mode='a+', encoding='utf-8') as fp: # 账号就是字典的账号,密码使用md5加密处理后再写入文件 strvar = "%s:%s\n" % (opt_dic["user"],cls.md5(opt_dic["user"],opt_dic["passwd"])) fp.write(strvar) # 3.返回一个注册成功的状态 return {"result":True,"info":"注册成功"} @ classmethod def login(cls,opt_dic): with open(userinfo,mode='r',encoding='utf-8') as fp: for line in fp: username,password = line.strip().split(":") if username == opt_dic["user"] and password == cls.md5(opt_dic["user"],opt_dic["passwd"]): return {"result":True,"info":"登陆成功"} return {"result":False,"info":"登录失败"} @ classmethod def myexit(cls,opt_dic): return {"result":"myexit"} class FTPServer(socketserver.BaseRequestHandler): def handle(self): while True: opt_dic = self.myrecv() print(opt_dic) # {'user': 'libolun', 'passwd': '111', 'operate': 'register'} if hasattr(Auth,opt_dic["operate"]): res = getattr(Auth,opt_dic["operate"])(opt_dic) if res["result"] == "myexit": return self.mysend(res) if res["result"]: # 接受界面2数据 while True: opt_dic = self.myrecv() print(opt_dic) if opt_dic["operate"] == "myexit": return if hasattr(self,opt_dic["operate"]): getattr(self,opt_dic["operate"])(opt_dic) else: dic = {"result":False,"info":"没有该操作"} self.mysend(dic) def myrecv(self): info = self.request.recv(1024) opt_str = info.decode() opt_dic = json.loads(opt_str) return opt_dic def mysend(self,send_info,sign=False): send_info = json.dumps(send_info).encode() if sign: res = struct.pack("i",len(send_info)) self.request.send(res) self.request.send(send_info) def download(self,opt_dic): filename = opt_dic["filename"] # 获取用户在客户端输入的文件名 file_abs = os.path.join(base_path,"video",filename) # 获取到要下载视频的绝对路径 if os.path.exists(file_abs): # 如果文件存在 # 1.告诉客户端,文件存在,可以下载 dic = {"result":True,"info":"文件存在,可以下载"} self.mysend(dic,sign=True) # 2.发送文件的名字和文件的大小 filesize = os.path.getsize(file_abs) dic = {"filename":filename,"filesize":filesize} self.mysend(dic,sign=True) # 3.真正开始发送数据 with open(file_abs,mode='rb') as fp: while filesize: content = fp.read(102400) self.request.send(content) filesize -= len(content) print("服务器下载完毕") else: dic = {"result":False,"info":"文件不存在"} self.mysend(dic,sign=True) myserver = socketserver.ThreadingTCPServer(("127.0.0.1",9000),FTPServer) myserver.serve_forever()