目錄
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()