目录
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 ('登录', <function login at 0x7ff7cf171a60>)
2 ('注册', <function register at 0x7ff7cf17e620>)
3 ('退出', <function myexit at 0x7ff7cf171ae8>)
""" 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]) ('退出', <function myexit at 0x7f801e34aa60>)
# print(operate_lst1[num-1][1]) <function myexit at 0x7f801e34aa60>
# 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()