小程式5:FTP程式
- 2020 年 8 月 23 日
- 筆記
- PythonS31-小程式
目錄
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,進而可以進行循環登錄註冊和退出。
那麼在服務端我們也應該是循環進行調用註冊登錄和退出
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()