socketserver、struct模塊練習,簡易的FTP ...
一、開發環境
server端:centos 7 python-3.6.2
客戶端:Windows 7 python-3.6.2 pycharm-2018
程式目的:1、學習使用socketserver實現併發處理多個客戶端。
2、瞭解使用struct解決TCP粘包。
二、程式設計
(本人菜鳥一枚,對於開發規範,介面設計完全不懂,完全是隨心所欲,自娛自樂。寫博客主要是記錄自己學習的點點滴滴,如有不足之處還請見諒。)
1、server端
1.1 目錄結構如下:
1.2 目錄簡介:
FTP_SERVER:程式主目錄
app:程式主邏輯目錄,目錄下有四個模塊:
FTPserver.py:FTP Server端啟動入口。
login.py:認證註冊模塊,用於處理用戶註冊,登錄認證。
dataAnalysis.py:命令解析模塊,負責解析,執行客戶端命令。
FileOpertion.py:負責文件讀,寫。數據發送,數據接收。
db:存放user_pwd.db文件,用於存放用戶信息(用戶名,密碼,FTP目錄總空間,已使用空間等)
lib:存放公共數據。
1.3 模塊中類的繼承關係
1.4 執行流程
1.4.1 程式啟動文件FTPserver.py,程式啟動後進入監聽狀態。核心代碼如下:
class MyFtpServer(socketserver.BaseRequestHandler): def handle(self): # 重寫handle方法,處理socket請求 print(f"連接來自{self.client_address}的客戶端") commom_obj = Commom() data_analy = DataAnalysis() login_obj = Login() while 1: # 執行用戶選項:1、登陸系統 2、註冊賬號。並返回一個結果 status_id = login_obj.run_client_choice(self.request, commom_obj) if status_id == "01": # 登陸成功 if not self.run_ftp_server(data_analy,commom_obj): # 執行ftpserver主功能 break elif int(status_id) == -1: # client斷開連接了 break print(f"客戶端{self.client_address}斷開了連接") def run_ftp_server(self,data_analy,commom_obj): """" 登陸成功後,接收客戶端發來的命令,併進行處理 :param data_analy:負責解析,執行客戶端命令的對象 :param commom_obj:程式執行時所需的數據對象 :return 返回false代表客戶端斷開連接了 """ while True: try: cmd_len_pack = self.request.recv(4) cmd_len = struct.unpack('i',cmd_len_pack)[0] # 獲取命令長度,防止粘包 except Exception: break recv_data = self.request.recv(cmd_len).decode('utf-8') # 接收客戶端數據 if recv_data.upper() == "Q": # 客戶端提出斷開連接了 break # 解析,處理客戶端的命令 data_analy.syntax_analysis(recv_data, self.request, commom_obj) return False if __name__ == '__main__': print('運行FTP服務') ip_port = ('192.168.10.10',9000) # 創建併發服務端對象 server = socketserver.ThreadingTCPServer(ip_port, MyFtpServer) # 開啟服務 server.serve_forever()
1.4.2 服務端進入監聽狀態後,客戶端發起連接請求,服務端接收連接請求後會等待客戶單發來狀態碼,1表示請求登錄FTP伺服器,2表示客戶端要註冊用戶,註冊用戶需要服務端手動反饋狀態碼1才可註冊。處理用戶登錄,註冊模塊login.py核心代碼如下:
class Login(FileOperation): """ 登陸註冊類。主要負責用戶的登陸認證,和用戶註冊。 """ def run_client_choice(self,socket_obj,commom): """ 獲取客戶端的請求,1是登陸,2是註冊用戶 :param socket_obj: socket對象 :param commom: ftpserver運行時所需要的數據對象 :return: """ recv_choice = socket_obj.recv(1).decode("utf-8") # 獲取用戶選項:1是登陸,2是註冊用戶 if recv_choice == "1": # client請求登陸 return self.login_authen(socket_obj,commom) elif recv_choice == "2": # client請求註冊賬號 return self.register_user(socket_obj,commom) else: return -1 # client斷開連接了 # 用戶登陸認證 def login_authen(self,socket_obj,commom): """ 客戶端登陸認證 :param socket_obj: socket對象 :param commom: ftpserver運行時需要的數據對象 :return:返回1代表登陸成功 """ # 接收client發來的用戶名,密碼 recv_userPwd = self.recv_data(socket_obj).decode("utf-8").split("|") # 效驗用戶名密碼 check_ret = self.check_user_pwd(recv_userPwd, socket_obj,commom) if check_ret: # 用戶名密碼正確 self.check_user_home_dir(commom,recv_userPwd[0]) # 檢測用戶家目錄 return commom.status_info["login_success"] else: return commom.status_info["login_fail"] ... # 註冊用戶 def register_user(self,socket_obj,commom): """ :param socket_obj: :param commom: :return: 返回是否允許註冊的結果,1允許客戶端註冊,2拒絕客戶端註冊 """ while True: choice_id = input("請輸入回應碼:1是允許註冊,2是不允許註冊:") if choice_id.isdigit() and 3 > int(choice_id) > 0: socket_obj.send(choice_id.encode("utf-8")) # 發通知告知客戶端,處理結果 if choice_id == "1": # 註冊用戶 return self.client_register(socket_obj, commom) return choice_id else: print("您輸入的信息有誤,請重新輸入。") ...
1.4.3 客戶端登錄成功後,服務端會等待接收客戶端發來的命令,命令的解析,執行由dataAnalysis.py模塊執行,核心代碼如下:
class DataAnalysis(FileOperation): """ 數據分析處理類,主要負責解析client發送過來的指令。 """ def syntax_analysis(self,recv_data, socket_obj, commom): """ 負責解析客戶端傳來的數據。 :param recv_data:接收到的客戶端用戶數據 :param socket_obj:socket對象 :param commom:數據對象 :return: """ clientData = recv_data.split(" ") if hasattr(self,clientData[0]): # 判斷對象方法是否存在 get_fun = getattr(self,clientData[0])#獲取對象方法 get_fun(clientData,socket_obj,commom) # 運行對象方法 else: pass ...
執行客戶端命令後,繼續等待接收客戶端發來的命令,如此迴圈...。
2、客戶端
2.1 目錄結構如下:
2.2 目錄簡介:
client:程式主目錄。
bin:程式入口,程式啟動文件main.py用於建立socket連接,然後調用FTPclient.py模塊下的run_ftp_client方法運行程式。
app:程式主邏輯,目錄下有四個模塊如下:
FTPclient.py:FTP客戶端,根據用戶選項,執行用戶指令。
login.py:認證註冊模塊,用於處理用戶註冊,登錄認證。
dataAnalysis.py:命令解析模塊,解析用戶輸入的命令,發給服務端獲取結果。
FileOpertion.py:負責文件讀,寫。
lib:存放公共數據,有兩個文件:
commom.py:主要存放的是公共變數。
help.txt:存放的是幫助文檔,當用戶執行help命令時會調用該文件。
2.3 模塊中類的繼承關係
2.4 執行流程
2.4.1 程式入口main.py,啟動後會與FTP服務端建立連接,與服務端連接成功後會調用FTPclient.py模塊下的run_ftp_client方法,執行用戶功能。核心代碼如下:
socket_obj = socket.socket(socket.AF_INET,socket.SOCK_STREAM) socket_obj.connect(("192.168.10.10",9000)) client_obj = Client() client_obj.run_ftp_client(socket_obj) # 接收用戶輸入的選項,執行對應的功能
2.4.2 FTPclient.py模塊下的run_ftp_client方法會列印菜單,並等待用戶輸入選項,執行相應功能,核心代碼如下:
class Client(Login,DataAnalysis): def run_ftp_client(self,socket_obj): """ 運行用戶輸入的選項:1、是登陸 2、是註冊賬號 :return: """ while True: self.login_menu() # 列印系統菜單 choice_id = self.get_user_choice() # 獲取用戶輸入的選項 if choice_id: if self.run_user_choice(choice_id,socket_obj): break else: print("您輸入的有誤") def get_user_choice(self): """ 獲取用戶輸入的選項 :return: """ choice_id = input("請輸入選項:") if choice_id.isdigit() and 4 > int(choice_id) > 0 or choice_id.upper() == "Q": return choice_id return False def run_user_choice(self,choice_id,socket_obj): if choice_id == "1": # 登陸系統 socket_obj.send(choice_id.encode("utf-8")) # 發通知告知伺服器準備登陸 if self.run_login(socket_obj) == True: # 執行登陸 return True elif choice_id == "2": # 註冊用戶 socket_obj.send(choice_id.encode("utf-8")) # 請求伺服器,註冊用戶 self.register_user(socket_obj) # 執行註冊 elif choice_id.upper() == "Q": # 退出程式 socket_obj.send(choice_id.encode("utf-8")) # 通知伺服器,準備退出程式 socket_obj.close() print("程式正常退出") return True def run_login(self,socket_obj,): """ 運行登陸認證模塊,如果登陸成功執行程式主邏輯,否則重新登陸。 :param socket_obj: :return: """ if self.login_authention(socket_obj): while True: send_data = input(">>>").strip(" ") # 獲取發送數據(用戶執行的命令) if send_data.upper() == "Q": # 正常退出程式 socket_obj.send(send_data.encode("utf-8")) # 通知服務區斷開連接 socket_obj.close() print("程式正常退出") return True if self.syntax_analysis(send_data, socket_obj): # 解析用戶數據並處理數據 print("異常退出") return True return False def login_menu(self): print("-"*41) print(" 歡迎登陸迷你FTPv1.0") print("-"*41) print("1、登陸系統") print("2、用戶註冊") print("Q、退出程式")
2.4.3 login.py模塊主要用於處理註冊和登錄的功能,核心代碼如下:
class Login(Commom): def login_authention(self,socket_obj): """ 登陸認證 :param socket_obj:socket 對象 :return: """ user_pwd = self.get_user_pwd() # 獲取用戶名密碼 self.send_data(socket_obj,user_pwd) # 將用戶名和密碼發給伺服器 recv_status = socket_obj.recv(2).decode("utf-8") # 等待接收狀態碼 print(self.status_info[recv_status]) # 列印狀態碼對應的結果 if self.status_info[recv_status] == '登錄成功': return True return False ... def register_user(self,socket_obj): """ 等待服務端反饋是否允許註冊用戶。 :param socket_obj: :return: """ print("請等待服務端回應.....") recv_status = socket_obj.recv(1).decode("utf-8") if recv_status == "1": # 服務端同意申請賬號 user_pwd = self.get_regist_user_pwd() # 獲取註冊用戶名和密碼 if user_pwd: self.send_data(socket_obj,user_pwd) result = socket_obj.recv(2).decode("utf-8") print(self.status_info[result]) else: print("用戶名密碼有誤") else: # 客戶端拒絕申請賬號的請求 print("服務端拒絕了您申請賬號的請求,請與管理員取得聯繫。") return False ...
2.4.4 用戶登錄成功後,會等待接收用戶輸入命令,由dataAnalysis.py模塊負責解析用戶輸入的命令,並將命令發給FTP伺服器,然後接收伺服器的反饋。核心代碼如下:
class DataAnalysis(FileOperation): def syntax_analysis(self,cmd,socket_obj): """ 解析用戶輸入的命令。 :param cmd:用戶執行的命令,如:put 上傳的文件 :param socket_obj:socket對象發送和接收數據 :return: """ cmd_split = cmd.split(" ") # 將字元串命令分割成列表,用於驗證命令是否存在 if hasattr(self,cmd_split[0]): run_fun = getattr(self,cmd_split[0]) run_fun(cmd_split,socket_obj) else: print("無效的命令") ...