1.用戶加密認證 2.容許同時多用戶登陸 3.每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄 4.對用戶進行磁盤配額,每一個用戶的可用空間不一樣 5.容許用戶在ftp server上隨意切換目錄 6.容許用戶查看當前目錄下文件 7.容許上傳和下載文件,保證文件一致性(md5) 8.文件傳輸過程當中顯示進度條 9.附加功能:支持文件的斷點續傳
做業需求: 1.在以前開發的FTP基礎上,開發支持多併發的功能 2.不能使用SocketServer模塊,必須本身實現多線程 3.必須用到隊列Queue模塊,實現線程池 4.容許配置最大併發數,好比容許只有10個併發用戶
基本操做:知足需求1,2,3,4 大神要求:代碼寫的健壯、清晰
這個確定須要用到configparser 和hashlib模塊,用md5進行加密,服務端與用戶端 進行交互前,確定須要進行認證,在服務端進行認證,客戶端須要發送用戶名及密碼,但 是爲了安全起見,服務端數據庫中的密碼應該是加密後的密文,客戶端登錄認證時也應該 發送密文到服務端,服務端接受到密文與數據庫中對應的密文進行比較。
這個只須要寫一個dir就ok 簡單的說,使用configparse模塊就能夠完成
下載的進度條比較好實現,咱們能夠從服務端受到將要下載的文件的大小, 上傳的進度條,咱們能夠利用文件操做的tell()方法,獲取當前指針位置(字節)
併發,是僞並行,,即看起來是同時運行的,單個CPU+多道技術就能夠實現併發 多道技術概念回顧:內存中同時存入多道(多個)程序,cpu從一個進程快速切換 到另一個,使每一個進程各自運行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個cpu 只能執行一個任務,但在1秒內,cpu卻能夠運行多個進程,這就給人產生了並行的錯覺, 即僞併發,以此來區分多處理器操做系統的真正硬件並行(多個cpu共享同一個物理內存)
多線程(即多個控制線程)的概念是,在一個進程中存在多個線程,多個線程共享該進程的地址空間, 至關於一個車間內有多個流水線,都共有一個車間的資源。例如,北京地鐵與上海地鐵是不一樣的進程,而北京 地鐵裏的1號線是一個線程,北京地鐵全部的線路共享該地鐵的全部資源。 爲何使用多線程? 1,同一個進程內能夠共享該進程內的地址資源 2,建立線程的開銷遠小於建立進程的開銷(建立一個進程,就是建立一個車間,涉及到申請空間 ,並且在該空間內至少建立一個流水線,可是建立線程,就只是在一個車間內造一條流水線,無需申請 空間,因此建立開銷小)
使用線程池便可,若是不會能夠參考此博客的練習題:http://www.cnblogs.com/wj-1314/p/9039970.htmlhtml
- 1 對於此項目,最初的想法是寫出上傳,和下載文件的程序,包括客戶端和服務端。 - 2 在此基礎上擴展程序,包括提出開始程序到bin裏面,配置文件在config裏面 - 3 而後把上傳文件和下載文件的程序進行斷點續傳的程序重構 - 4 在此基礎上,對文件進行加密 - 5 增長功能,包括設置進度條,增長查看功能,增長目錄功能,刪除文件功能,切換目錄功能等 - 6 而後再設置磁盤分配功能,完善內容 - 7 而後添加用戶登錄,包括對用戶的密碼加密等功能 - 8 寫完後檢查程序
## 做者:zhanzhengrecheng ## 版本:示例版本 v0.1 ## 程序介紹: - 實現了基於線程開發一個FTP服務器的經常使用功能 - 基本功能所有用python的基礎知識實現,用到了socket\hashlib\configparse\os\sys\pickle\函數\模塊\類知識 - 在保證了支持多併發的功能上,不使用SocketServer模塊,本身實現了多線程,並且使用了隊列 ## 概述 本次做業文件夾一共包含了如下4個文件: - 程序結構圖:整個Thread_based_FTP_homework的程序文件結構 - 程序結構文件:整個Thread_based_FTP_homework的程序文件結構 - 程序文件: Thread_based_FTP_homework - 程序說明文件:README.md ## 程序要求 - 1.用戶加密認證 - 2.容許同時多用戶登陸 - 3.每一個用戶有本身的家目錄 ,且只能訪問本身的家目錄 - 4.對用戶進行磁盤配額,每一個用戶的可用空間不一樣 - 5.容許用戶在ftp server上隨意切換目錄 - 6.容許用戶查看當前目錄下文件 - 7.容許上傳和下載文件,保證文件一致性(md5) - 8.文件傳輸過程當中顯示進度條 - 9.附加功能:支持文件的斷點續傳 - 10.在以前開發的FTP基礎上,開發支持多併發的功能 - 11.不能使用SocketServer模塊,必須本身實現多線程 - 12.必須用到隊列Queue模塊,實現線程池 - 13.容許配置最大併發數,好比容許只有10個併發用戶 ## 本項目思路 - 1 對於這次項目,在上次做業的基礎上完成 - 2 本次首要任務,爲了下降程序的耦合性,將把server端和client端的許多東西分出來,保證一個函數只作一件事情 - 3 發現了上次做業裏面出現的小問題,進行了解決 - 4 使用隊列Queue模塊,實現多線程 - 5 設置配置最大的併發數,此處設置在settings裏面,最大併發用戶設置爲3 ##### 備註(程序結構) > 目前還不會把程序樹放在README.md裏面,因此作出程序結構的txt版本和圖片版本,放在文件外面方便查看 ## 對幾個實例文件的說明 ### 幾個實例文件全是爲了上傳和下載使用,本身隨便找的素材,沒有把視頻,照片上傳 ## 不足及其改進的方面 ### 每次程序從用戶登錄到使用只能完成一次功能,不能重複使用 ## 程序結構 │ Thread_based_FTP_homework │ __init__.py │ ├─client # 客戶端程序入口 │ │ __init__.py │ ├─bin # 可執行程序入口目錄 │ │ run.py │ │ __init__.py │ ├─config # 配置文件目錄 │ │ │ settings.py # 配置文件 │ │ │ __init__.py │ ├─core # 主要邏輯程序目錄 │ │ │ file_func.py # client端文件操做功能模塊 │ │ │ ftp_client.py # client端主程序模塊 │ │ │ md5_func.py # client端對文件加密操做功能模塊 │ │ │ progress_bar_func_func.py # client端文件下載進度條操做功能模塊 │ │ │ __init__.py │ ├─download # 下載內容模塊 │ │ a.txt │ │ b.txt │ │ c.txt │ └─upload # 上傳內容模塊 │ a.txt │ b.txt └─server # 服務端程序入口 ├─bin │ run.py # 可執行程序入口目錄 │ __init__.py ├─config # 配置文件目錄 │ │ accounts.ini # 帳號密碼配置文件 │ │ settings.py # 配置文件 │ │ __init__.py ├─core # 主要邏輯程序目錄 │ │ ftp_server.py # server端主程序模塊 │ │ main.py # 主程序模塊 │ │ user_handle.py # 用戶註冊登陸模塊 │ │ file_func.py # server端文件操做功能模塊 │ │ auth_func.py # server端用戶認證功能模塊 │ │ md5_func.py # server端對文件加密操做功能模塊 └─home # 家目錄 │ __init__.py ├─curry # curry用戶的家目錄 │ │ a.txt │ │ b.txt │ │ c.txt ├─durant # durant用戶的家目錄 │ └─test3 │ └─test4 └─james # james用戶的家目錄 │ a.txt │ b.txt │ c.txt │ test1 │ test2 └─test3
run.pypython
# _*_ coding: utf-8 _*_ import os import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) from core import ftp_server from core import main from config import settings if __name__ == '__main__': a = main.Manager() a.interactive()
settings.pygit
# _*_ coding: utf-8 _*_ import os import sys import socket import logging BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) ACCOUNTS_FILE = os.path.join(BASE_DIR,'config','accounts.ini') address_family = socket.AF_INET socket_type = socket.SOCK_STREAM BIND_HOST = '127.0.0.1' BIND_PORT = 9999 ip_port = (BIND_HOST,BIND_PORT) coding = 'utf-8' listen_count = 5 max_recv_bytes = 8192 allow_reuser_address = False # 容許配置最大併發數,這裏設置爲3 MAX_CONCURRENT_COUNT =3
auth.pyshell
# _*_ coding: utf-8 _*_ import pickle import hashlib import os import struct from config import settings from core.user_handle import UserHandle from core.file_func import File_func class Auth_func(): def auth(self, conn): ''' 處理用戶的認證請求 1,根據username讀取accounts.ini文件,而後查看用戶是否存在 2,將程序運行的目錄從bin.user_auth修改到用戶home/username方便以後查詢 3,把客戶端返回用戶的詳細信息 :return: ''' while True: user_dic = self.get_recv(conn) username = user_dic['username'] password = user_dic['password'] md5_obj = hashlib.md5(password.encode('utf-8')) check_password = md5_obj.hexdigest() user_handle = UserHandle(username) # 判斷用戶是否存在 返回列表, user_data = user_handle.judge_user() if user_data: if user_data[0][1] == check_password: conn.send(struct.pack('i', 1)) # 登陸成功返回 1 self.homedir_path = os.path.join(settings.BASE_DIR, 'home', username) # 將程序運行的目錄名修改到 用戶home目錄下 os.chdir(self.homedir_path) # 將用戶配額的大小從M 改到字節 self.quota_bytes = int(user_data[2][1]) * 1024 * 1024 user_info_dic = { 'username': username, 'homedir': user_data[1][1], 'quota': user_data[2][1] } # 用戶的詳細信息發送到客戶端 conn.send(pickle.dumps(user_info_dic)) return True else: conn.send(struct.pack('i', 0)) # 登陸失敗返回 0 else: conn.send(struct.pack('i', 0)) # 登陸失敗返回 0 def get_recv(self, conn): '''從client端接收發來的數據''' return pickle.loads(conn.recv(settings.max_recv_bytes)) def current_home_size(self): """獲得當前用戶目錄的大小,字節/M""" self.home_bytes_size = 0 File_func().recursion_file(self.homedir_path, self.home_bytes_size) home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)
file_func.py數據庫
# _*_ coding: utf-8 _*_ import os import sys import struct import pickle from config import settings from core import ftp_server from core import md5_func class File_func(object): def readfile(self,file_path): '''讀取文件,獲得文件內容的bytes類型''' with open(file_path, 'rb') as f: filedata = f.read() return filedata def send_filedata(self,file_path,conn,exist_file_size=0): """下載時,將文件打開,send(data)""" with open(file_path, 'rb') as f: f.seek(exist_file_size) while True: data = f.read(1024) if data: conn.send(data) else: break def write_file(self,conn,f,recv_size,file_size): '''上傳文件時,將文件內容寫入到文件中''' while recv_size < file_size: res = conn.recv(settings.max_recv_bytes) f.write(res) recv_size += len(res) conn.send(struct.pack('i', recv_size)) # 爲了進度條的顯示 def recursion_file(self, homedir_path,home_bytes_size): """遞歸查詢用戶目錄下的全部文件,算出文件的大小""" res = os.listdir(homedir_path) for i in res: path = os.path.join(homedir_path,i) if os.path.isdir(path): self.recursion_file(path,home_bytes_size) elif os.path.isfile(path): home_bytes_size += os.path.getsize(path) def current_home_size(self,homedir_path): """獲得當前用戶目錄的大小,字節/M""" self.home_bytes_size =0 self.recursion_file(self.home_bytes_size,homedir_path) home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)
ftp_server.pyjson
# _*_ coding: utf-8 _*_ import socket import struct import json import os import pickle import subprocess import hashlib import queue from threading import Thread from threading import currentThread from config import settings from core.user_handle import UserHandle from core.file_func import File_func from core.md5_func import Md5_func from core.auth_func import Auth_func class FTPServer(): def __init__(self,server_address,bind_and_listen = True): self.server_address = server_address self.socket = socket.socket(settings.address_family,settings.socket_type) self.q = queue.Queue(settings.MAX_CONCURRENT_COUNT) if bind_and_listen: try: self.server_bind() self.server_listen() except Exception: self.server_close() def server_bind(self): allow_reuse_address = False if allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) def server_listen(self): self.socket.listen(settings.listen_count) def server_close(self): self.socket.close() def server_accept(self): return self.socket.accept() def conn_close(self, conn): conn.close() def server_link(self): print("\033[31;1mwaiting client .....\033[0m") while True: # 連接循環 conn,self.client_addr = self.server_accept() print('客戶端地址:', self.client_addr) # while True: # 通訊循環 try: t = Thread(target=self.server_handle,args=(conn,)) self.q.put(t) t.start() except Exception as e: print(e) self.conn_close(conn) self.q.get() def server_handle(self,conn): '''處理與用戶的交互指令''' if Auth_func().auth(conn): print("\033[32;1m-------user authentication successfully-------\033[0m") try: res = conn.recv(settings.max_recv_bytes) if not res: self.conn_close(conn) self.q.get() # 解析命令,提取相應的參數 self.cmds = res.decode(settings.coding).split() if hasattr(self, self.cmds[0]): func = getattr(self, self.cmds[0]) func(conn) except Exception as e: print(e) self.conn_close(conn) self.q.get() def get(self, conn): ''' 下載,首先查看文件是否存在,而後上傳文件的報頭大小,上傳文件,以讀的方式發開文件 找到下載的文件 發送 header_size 發送 header_bytes file_size 讀文件 rb 發送 send(line) 若文件不存在,發送0 client提示:文件不存在 :param cmds: :return: ''' if len(self.cmds) > 1: filename = self.cmds[1] self.file_path = os.path.join(os.getcwd(), filename) if os.path.isfile(self.file_path): file_size = os.path.getsize(self.file_path) obj = conn.recv(4) exist_file_size = struct.unpack('i', obj)[0] header = { 'filename': filename, 'filemd5': Md5_func().getfile_md5(self.file_path), 'file_size': file_size } header_bytes = pickle.dumps(header) conn.send(struct.pack('i', len(header_bytes))) conn.send(header_bytes) if exist_file_size: # 表示以前被下載過 一部分 if exist_file_size != file_size: File_func().send_filedata(self.file_path, exist_file_size) else: print('\033[31;1mbreakpoint and file size are the same\033[0m') else: # 文件第一次下載 File_func().send_filedata(self.file_path, conn) else: print('\033[31;1merror\033[0m') conn.send(struct.pack('i', 0)) else: print("\033[31;1muser does not enter file name\033[0m") def put(self, conn): """從client上傳文件到server當前工做目錄下 """ if len(self.cmds) > 1: obj = conn.recv(4) state_size = struct.unpack('i', obj)[0] if state_size: # 算出了home下已被佔用的大小self.home_bytes_size self.current_home_size() header_bytes = conn.recv(struct.unpack('i', conn.recv(4))[0]) header_dic = pickle.loads(header_bytes) filename = header_dic.get('filename') file_size = header_dic.get('file_size') file_md5 = header_dic.get('file_md5') self.file_path = os.path.join(os.getcwd(), filename) if os.path.exists(self.file_path): conn.send(struct.pack('i', 1)) has_size = os.path.getsize(self.file_path) if has_size == file_size: print("\033[31;1mfile already does exist!\033[0m") conn.send(struct.pack('i', 0)) else: print('\033[31;1mLast time file not finished,this time continue\033[0m') conn.send(struct.pack('i', 1)) if self.home_bytes_size + int(file_size - has_size) > self.quota_bytes: print('\033[31;1mSorry exceeding user quotas\033[0m') conn.send(struct.pack('i', 0)) else: conn.send(struct.pack('i', 1)) conn.send(struct.pack('i', has_size)) with open(self.file_path, 'ab') as f: f.seek(has_size) File_func().write_file(conn, f, has_size, file_size) Md5_func().verification_filemd5(self.file_path, conn, file_md5) else: conn.send(struct.pack('i', 0)) print('\033[31;1mfile does not exist, now first put\033[0m') if self.home_bytes_size + int(file_size) > self.quota_bytes: print('\033[31;1mSorry exceeding user quotas\033[0m') conn.send(struct.pack('i', 0)) else: conn.send(struct.pack('i', 1)) with open(self.file_path, 'wb') as f: recv_size = 0 File_func().write_file(conn, f, recv_size, file_size) Md5_func().verification_filemd5(self.file_path,conn, file_md5) else: print("\033[31;1mfile does not exist!\033[0m") else: print("\033[31;1muser does not enter file name\033[0m") def ls(self, conn): '''查看當前工做目錄下,先返回文件列表的大小,在返回查詢的結果''' print("\033[34;1mview current working directory\033[0m") subpro_obj = subprocess.Popen('dir', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = subpro_obj.stdout.read() stderr = subpro_obj.stderr.read() conn.send(struct.pack('i', len(stdout + stderr))) conn.send(stdout) conn.send(stderr) print('\033[31;1mCongratulations view directory success\033[0m') def mkdir(self, conn): '''增長目錄 在當前目錄下,增長目錄 1.查看目錄名是否已經存在 2.增長目錄成功,返回 1 2.增長目錄失敗,返回 0''' print("\033[34;1madd working directory\033[0m") if len(self.cmds) > 1: mkdir_path = os.path.join(os.getcwd(), self.cmds[1]) if not os.path.exists(mkdir_path): os.mkdir(mkdir_path) print('\033[31;1mCongratulations add directory success\033[0m') conn.send(struct.pack('i', 1)) else: print("\033[31;1muser directory already does exist\033[0m") conn.send(struct.pack('i', 0)) else: print("\033[31;1muser does not enter file name\033[0m") def cd(self, conn): '''切換目錄 1.查看是不是目錄名 2.拿到當前目錄,拿到目標目錄, 3.判斷homedir是否在目標目錄內,防止用戶越過本身的home目錄 eg: ../../.... 4.切換成功,返回 1 5.切換失敗,返回 0''' print("\033[34;1mSwitch working directory\033[0m") if len(self.cmds) > 1: dir_path = os.path.join(os.getcwd(), self.cmds[1]) if os.path.isdir(dir_path): # os.getcwd 獲取當前工做目錄 previous_path = os.getcwd() # os.chdir改變當前腳本目錄 os.chdir(dir_path) target_dir = os.getcwd() if self.homedir_path in target_dir: print('\033[31;1mCongratulations switch directory success\033[0m') conn.send(struct.pack('i', 1)) else: print('\033[31;1mSorry switch directory failed\033[0m') # 切換失敗後,返回到以前的目錄下 os.chdir(previous_path) conn.send(struct.pack('i', 0)) else: print('\033[31;1mSorry switch directory failed,the directory is not current directory\033[0m') conn.send(struct.pack('i', 0)) else: print("\033[31;1muser does not enter file name\033[0m") def remove(self, conn): """刪除指定的文件,或者空文件夾 1.刪除成功,返回 1 2.刪除失敗,返回 0 """ print("\033[34;1mRemove working directory\033[0m") if len(self.cmds) > 1: file_name = self.cmds[1] file_path = os.path.join(os.getcwd(), file_name) if os.path.isfile(file_path): os.remove(file_path) conn.send(struct.pack('i', 1)) elif os.path.isdir(file_path): # 刪除空目錄 if not len(os.listdir(file_path)): os.removedirs(file_path) print('\033[31;1mCongratulations remove success\033[0m') conn.send(struct.pack('i', 1)) else: print('\033[31;1mSorry remove directory failed\033[0m') conn.send(struct.pack('i', 0)) else: print('\033[31;1mSorry remove directory failed\033[0m') conn.send(struct.pack('i', 0)) else: print("\033[31;1muser does not enter file name\033[0m")
main.py安全
# _*_ coding: utf-8 _*_ from core.user_handle import UserHandle from core.ftp_server import FTPServer from config import settings class Manager(): ''' 主程序,包括啓動server,建立用戶,退出 :return: ''' def __init__(self): pass def start_ftp(self): '''啓動server端''' server = FTPServer(settings.ip_port) server.server_link() server.close() def create_user(self): '''建立用戶,執行建立用戶的類''' username = input("\033[32;1mplease input your username>>>\033[0m").strip() UserHandle(username).add_user() def logout(self): ''' 退出登錄 :return: ''' print("\033[32;1m-------Looking forward to your next login-------\033[0m") exit() def interactive(self): '''交互函數''' msg = '''\033[32;1m 1 啓動ftp服務端 2 建立用戶 3 退出 \033[0m''' menu_dic = { "1": 'start_ftp', "2": 'create_user', "3": 'logout', } exit_flag = False while not exit_flag: print(msg) user_choice = input("Please input a command>>>").strip() if user_choice in menu_dic: getattr(self,menu_dic[user_choice])() else: print("\033[31;1myou choice doesn't exist\033[0m")
md5_func.py服務器
# _*_ coding: utf-8 _*_ import hashlib import struct from core import ftp_server from core import file_func class Md5_func(object): def getfile_md5(self,file_path): '''獲取文件的md5''' md5 = hashlib.md5(file_func.File_func().readfile(file_path)) print("md5是:\n",md5.hexdigest()) return md5.hexdigest() def handle_data(self): '''處理接收到的數據,主要是將密碼轉化爲md5的形式''' user_dic = ftp_server.FTPServer().get_recv() username = user_dic['username'] password = user_dic['password'] md5_obj = hashlib.md5() md5_obj.update(password) check_password = md5_obj.hexdigest() def verification_filemd5(self,file_path,conn,filemd5): # 判斷文件內容的md5 if self.getfile_md5(file_path) == filemd5: print('\033[31;1mCongratulations download success\033[0m') conn.send(struct.pack('i', 1)) else: print('\033[31;1mSorry download failed\033[0m') conn.send(struct.pack('i', 0))
user_handle.py多線程
#_*_coding:utf-8_*_ import configparser import hashlib import os from config import settings class UserHandle(): ''' 建立用戶名稱,密碼 若是用戶存在,則返回,若是用戶不存在,則註冊成功 ''' def __init__(self,username): self.username = username self.config = configparser.ConfigParser() self.config.read(settings.ACCOUNTS_FILE) def password(self): '''生成用戶的密碼,而後加密''' password_inp = input("\033[32;1mplease input your password>>>\033[0m").strip() md5_obj = hashlib.md5() md5_obj.update(password_inp.encode('utf-8')) md5_password = md5_obj.hexdigest() return md5_password def disk_quota(self): '''生成每一個用戶的磁盤配額''' quota = input('\033[32;1mplease input Disk quotas>>>:\033[0m').strip() if quota.isdigit(): return quota else: exit('\033[31;1mdisk quotas must be integer\033[0m') def add_user(self): '''建立用戶,存到accounts.ini''' if not self.config.has_section(self.username): print('\033[31;1mcreating username is :%s \033[0m' % self.username) self.config.add_section(self.username) self.config.set(self.username,'password',self.password()) self.config.set(self.username, 'homedir', 'home/' + self.username) self.config.set(self.username, 'quota', self.disk_quota()) self.write_config() self.create_userhome() print('\033[1;32msuccessfully create userdata\033[0m') else: print('\033[1;31musername already existing\033[0m') def create_userhome(self): '''建立用戶的home文件夾''' os.mkdir(os.path.join(settings.BASE_DIR, 'home', self.username)) def write_config(self): '''寫入文檔''' with open(settings.ACCOUNTS_FILE,'w') as f: self.config.write(f) def judge_user(self): '''判斷用戶是否存在''' if self.config.has_section(self.username): return self.config.items(self.username)
run.py併發
# _*_ coding: utf-8 _*_ import os import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) from core import ftp_client from config import settings if __name__ == '__main__': run = ftp_client.FTPClient(settings.ip_port) run.execute()
settings.py
# _*_ coding: utf-8 _*_ import os import sys import socket BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) # 下載的文件存放路徑 down_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'download') # 上傳的文件存放路徑 upload_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'upload') #綁定的IP地址 BIND_HOST = '127.0.0.1' #綁定的端口號 BIND_PORT = 9999 ip_port = (BIND_HOST,BIND_PORT) address_family = socket.AF_INET socket_type = socket.SOCK_STREAM coding = 'utf-8' listen_count = 5 max_recv_bytes = 8192 allow_reuser_address = False
file_func.py
# _*_ coding: utf-8 _*_ import os import sys import struct import pickle from config import settings from core import ftp_client from core import md5_func from core import progress_bar_func class File_func(object): def readfile(self,file_path): '''讀取文件''' with open(file_path,'rb') as f: filedata = f.read() return filedata def appendfile_content(self,socket,file_path,temp_file_size,file_size): '''追加文件內容''' with open(file_path,'ab') as f: f.seek(temp_file_size) get_size = temp_file_size while get_size < file_size: res = socket.recv(settings.max_recv_bytes) f.write(res) get_size += len(res) progress_bar_func.progress_bar(1,get_size,file_size) #1表示下載 def write_file(self,socket,f,get_size,file_size): '''下載文件,將內容寫入文件中''' while get_size < file_size: res = socket.recv(settings.max_recv_bytes) f.write(res) get_size += len(res) progress_bar_func.progress_bar(1,get_size,file_size) #1表示下載 def recv_file_header(self,socket,header_size): """接收文件的header, filename file_size file_md5""" header_types = socket.recv(header_size) header_dic = pickle.loads(header_types) print(header_dic, type(header_dic)) total_size = header_dic['file_size'] filename = header_dic['filename'] filemd5 = header_dic['filemd5'] return (filename,total_size,filemd5) def open_sendfile(self,file_size,file_path,socket,recv_size =0): '''打開要上傳的文件(因爲本程序上傳文件的原理是先讀取本地文件,再寫到上傳地址的文件)''' with open(file_path, 'rb') as f: f.seek(recv_size) while True: data = f.read(1024) if data: socket.send(data) obj = socket.recv(4) recv_size = struct.unpack('i', obj)[0] progress_bar_func.progress_bar(2, recv_size, file_size) else: break success_state = struct.unpack('i', socket.recv(4))[0] if success_state: print('\033[31;1mCongratulations upload success\033[0m') else: print('\033[31;1mSorry upload directory failed\033[0m')
ftp_client.py
# _*_ coding: utf-8 _*_ import socket import struct import json import os import sys import pickle import hashlib from config import settings from core.md5_func import Md5_func from core.file_func import File_func from core import progress_bar_func class FTPClient: def __init__(self,server_address,connect = True): self.server_address = server_address self.socket = socket.socket(settings.address_family,settings.socket_type) if connect: try: self.client_connect() except Exception: self.client_close() def client_connect(self): try: self.socket.connect(self.server_address) except Exception as e: print("\033[31;1merror:%s\033[0m"%e) exit("\033[31;1m\nThe server is not activated \033[0m") def client_close(self): self.socket.close() def get(self,cmds): """從server下載文件到client """ if len(cmds) >1: filename = cmds[1] self.file_path = os.path.join(settings.down_filepath, filename) if os.path.isfile(self.file_path): #若是文件存在,支持斷電續傳 temp_file_size = os.path.getsize(self.file_path) self.socket.send(struct.pack('i',temp_file_size)) header_size = struct.unpack('i',self.socket.recv(4))[0] if header_size: filename,file_size,filemd5 = File_func().recv_file_header(self.socket,header_size) if temp_file_size == file_size: print('\033[34;1mFile already does exist\033[0m') else: print('\033[34;1mFile now is breakpoint continuation\033[0m') File_func().appendfile_content(self.socket,self.file_path,temp_file_size,file_size) Md5_func().verification_filemd5(self.file_path,filemd5) else: print("\033[34;1mFile was downloaded before,but now server's file is not exist\033[0m") else:#若是文件不存在,則是直接下載 self.socket.send(struct.pack('i',0)) obj = self.socket.recv(1024) header_size = struct.unpack('i', obj)[0] if header_size==0: print("\033[31;1mfile does not exist!\033[0m") else: filename, file_size, filemd5 = File_func().recv_file_header(self.socket,header_size) download_filepath = os.path.join(settings.down_filepath, filename) with open(download_filepath, 'wb') as f: get_size = 0 File_func().write_file(self.socket,f, get_size, file_size) Md5_func().verification_filemd5(self.file_path,filemd5) else: print("\033[31;1muser does not enter file name\033[0m") def ls(self,cmds): '''查看當前工做目錄,文件列表''' print("\033[34;1mview current working directory\033[0m") obj = self.socket.recv(4) dir_size = struct.unpack('i',obj)[0] recv_size = 0 recv_bytes = b'' while recv_size <dir_size: temp_bytes = self.socket.recv(settings.max_recv_bytes) recv_bytes +=temp_bytes recv_size += len(temp_bytes) print(recv_bytes.decode('gbk')) print('\033[31;1mCongratulations view directory success\033[0m') def mkdir(self,cmds): '''增長目錄 1,server返回1 增長成功 2,server返回2 增長失敗''' print("\033[34;1madd working directory\033[0m") obj = self.socket.recv(4) res = struct.unpack('i',obj)[0] if res: print('\033[31;1mCongratulations add directory success\033[0m') else: print('\033[31;1mSorry add directory failed\033[0m') def cd(self,cmds): '''切換目錄''' print("\033[34;1mSwitch working directory\033[0m") if len(cmds) >1: obj = self.socket.recv(4) res = struct.unpack('i', obj)[0] if res: print('\033[31;1mCongratulations switch directory success\033[0m') else: print('\033[31;1mSorry switch directory failed\033[0m') else: print("\033[31;1muser does not enter file name\033[0m") def remove(self,cmds): '''表示刪除文件或空文件夾''' print("\033[34;1mRemove working directory\033[0m") obj = self.socket.recv(4) res = struct.unpack('i', obj)[0] if res: print('\033[31;1mCongratulations remove success\033[0m') else: print('\033[31;1mSorry remove directory failed\033[0m') def put_situation(self,file_size,condition=0): '''上傳的時候有兩種狀況,文件已經存在,文件不存在''' quota_state= struct.unpack('i', self.socket.recv(4))[0] if quota_state: if condition: obj = self.socket.recv(4) recv_size = struct.unpack('i', obj)[0] File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0) else: File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0) else: print('\033[31;1mSorry exceeding user quotas\033[0m') def put(self,cmds): """往server端登陸的用戶目錄下上傳文件 """ if len(cmds) > 1: filename = cmds[1] file_path = os.path.join(settings.upload_filepath, filename) if os.path.isfile(file_path): # 若是文件存在,支持斷電續傳 self.socket.send(struct.pack('i', 1)) self.file_path = file_path file_size = os.path.getsize(self.file_path) header_dic = { 'filename': os.path.basename(filename), 'file_md5': Md5_func().getfile_md5(self.file_path), 'file_size': file_size } header_bytes = pickle.dumps(header_dic) self.socket.send(struct.pack('i', len(header_bytes))) self.socket.send(header_bytes) state = struct.unpack('i', self.socket.recv(4))[0] if state: #已經存在 has_state = struct.unpack('i', self.socket.recv(4))[0] if has_state: self.put_situation(file_size, 1) else: # 存在的大小 和文件大小一致 沒必要再傳 print("\033[31;1mfile already does exist!\033[0m") else: # 第一次傳 self.put_situation(file_size) else: # 文件不存在 print("\033[31;1mfile does not exist!\033[0m") self.socket.send(struct.pack('i', 0)) else: print("\033[31;1muser does not enter file name\033[0m") def get_recv(self): '''從client端接受發來的數據''' return pickle.loads(self.socket.recv(settings.max_recv_bytes)) def login(self): ''' 登錄函數,當登錄失敗超過三次,則退出 用戶密碼發送到server短 接受server端返回的信息,若是成功返回1,失敗返回0 :return: 若是用戶帳號密碼正確,則返回用戶數據的字典 ''' retry_count = 0 while retry_count <3: username = input('\033[34;1mplease input Username:\033[0m').strip() if not username: continue password = input('\033[34;1mplease input Password:\033[0m').strip() user_dic = { 'username':username, 'password':password } #將用戶信息發送到客戶端,而後接受客戶端的數據 data = pickle.dumps(user_dic) self.socket.send(pickle.dumps(user_dic)) #爲了防止出現黏包問題,因此先解壓報頭,讀取報頭,再讀數據 obj = self.socket.recv(4) res = struct.unpack('i',obj)[0] #此處,若是返回的是代碼4001,則成功 4002則失敗 if res: print("\033[32;1m-----------------welcome to ftp client-------------------\033[0m") user_info_dic = self.get_recv() recv_username = user_info_dic['username'] return True else: print("\033[31;1mAccount or Passwordoes not correct!\033[0m") retry_count +=1 def execute(self): ''' 執行,或者實施 :return: ''' if self.login(): while True: try: self.help_info() inp = input("Please input a command>>>").strip() if not inp: continue self.socket.send(inp.encode(settings.coding)) cmds = inp.split() if hasattr(self, cmds[0]): func = getattr(self, cmds[0]) func(cmds) break else: print('\033[31;1mNo such command ,please try again\033[0m') except Exception as e: # server關閉了 print('\033[31;1m%s\033[0m'%e) break def help_info(self): print ('''\033[34;1m get + (文件名) 表示下載文件 put + (文件名) 表示上傳文件 ls 表示查詢當前目錄下的文件列表(只能訪問本身的文件列表) mkdir + (文件名) 表示建立文件夾 cd + (文件名) 表示切換目錄(只能在本身的文件列表中切換) remove + (文件名) 表示刪除文件或空文件夾 \033[0m''')
md5_func.py
# _*_ coding: utf-8 _*_ import hashlib from core import ftp_client from core.file_func import File_func class Md5_func(object): def getfile_md5(self,file_path): '''對文件內容進行加密,也就是保持文件的一致性''' md5 = hashlib.md5(File_func().readfile(file_path)) print("md5是:\n",md5.hexdigest()) return md5.hexdigest() def verification_filemd5(self,file_path,filemd5): # 判斷下載下來的文件MD5值和server傳過來的MD5值是否一致 if self.getfile_md5(file_path) == filemd5: print('\033[31;1mCongratulations download success\033[0m') else: print('\033[31;1mSorry download failed,download again support breakpoint continuation\033[0m')
progress_bar_fun.py
# _*_ coding: utf-8 _*_ '''進度條的表示功能''' import sys def progress_bar(num, get_size, file_size): float_rate = float(get_size) / float(file_size) rate_num = round(float_rate * 100, 2) if num == 1: # 1表示下載 sys.stdout.write('\033[31;1m\rfinish downloaded perentage:{0}%\033[0m'.format(rate_num)) elif num == 2: # 2表示上傳 sys.stdout.write('\033[31;1m\rfinish uploaded perentage:{0}%\033[0m'.format(rate_num)) sys.stdout.flush()