1、功能說明:shell
1.本程序基於socket實現客戶端與服務器端的單進程交互json
2.用到的用戶名:whw,密碼abc123——服務器端密碼的驗證利用hashlib模塊進行MD5的編碼以確保通訊安全。windows
3.客戶端登錄成功後能夠查看本身再服務器上的文件夾裏文件的列表;能夠在本身所在的目錄隨意切換;能夠將服務器端本身文件夾中的文件下載到客戶端;能夠將本身本端的文件下載到服務器端本身的文件夾裏去安全
4.客戶端上傳並下載文件有日誌記錄服務器
2、目錄結構併發
WHW_FTP
├── client
│ ├── bin #客戶端入口程序目錄(客戶端文件保存的目錄)
│ │ └── whw_client.py #客戶端的入口程序
│ │
│ └── logics #配置文件目錄
│ └── ftp_client.py #客戶端與服務器端交互的邏輯
│
├── server
│ ├── bin #服務器端入口程序目錄
│ │ └── whw_server.py #服務器端的入口程序
│ │
│ ├── conf #存放的是用戶信息與程序用到的參數
│ │ ├── accounts.ini #客戶信息
│ │ └── settings.py #程序用到的其餘固定參數
│ │
│ └── core #服務器端程序的主邏輯存放地
│ │ ├── ftp_server.py #服務器端與客戶端交互的程序
│ │ └── logger.py #記錄日誌的邏輯
│ │ └── management.py #負責處理客戶端命令行參數的邏輯
│ │
│ └── home #存放的是用戶目錄(用戶在server端的文件存放於此)
│ │ └── whw #客戶文件夾
│ └── log #存放的是日誌信息
│ └── whw.log #日誌文件
└── README.txtapp
3、程序源代碼socket
import os import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) if __name__ == '__main__': from logics import ftp_client client = ftp_client.Ftp_client() client.interactive() print(BASE_DIR)
import optparse import socket import json import os class Ftp_client: '''ftp客戶端''' MSG_SIZE = 1024 #消息最長1024 def __init__(self): #初始化 self.username = None parser = optparse.OptionParser() parser.add_option("-s", "--server", dest="server", help="ftp server ip_addr") parser.add_option("-P", "--port", type="int", dest="port", help="ftp server port") parser.add_option("-u", "--username", dest="username", help="username info") parser.add_option("-p", "--password", dest="password", help="password info") #傳參 self.options, self.args = parser.parse_args() #檢查參數是否合法 self.argv_verification() #創建鏈接 self.make_connection() def argv_verification(self): '''檢查參數合法性''' #客戶端-s跟-P後面不能跟空 if not self.options.server or not self.options.port: exit('Error:must supply server and port parameters!') def interactive(self): """處理與Ftpserver的全部交互""" if self.auth(): while 1: user_input = input('[%s]>>:' % self.username).strip() if not user_input: continue # 將命令分割切片~~~ cmd_list = user_input.split() # 反射~~~ if hasattr(self, '_%s' % cmd_list[0]): func = getattr(self, '_%s' % cmd_list[0]) func(cmd_list[1:]) def auth(self): """用戶認證!!!""" count = 0 while count < 3: username = input("username:").strip() if not username: continue password = input("password:").strip() cmd = { 'action_type': 'auth', 'username': username, 'password': password } #給server發送客戶端用戶的認證信息 self.whw_sock.send(json.dumps(cmd).encode("utf-8")) #而後接收server的反饋 response = self.get_response() if response.get('status_code') == 200: # pass auth self.username = username self.terminal_display = "[%s]>>:" % self.username self.current_dir = "\\" return True else: print(response.get("status_msg")) count += 1 def get_response(self): """獲取服務器端返回的信息""" data = self.whw_sock.recv(self.MSG_SIZE) return json.loads(data.decode('utf-8')) def make_connection(self): '''創建socket連接''' self.whw_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #注意optparse模塊獲得的是兩個對象,而不是字典與列表!要用對象的操做取值 self.whw_sock.connect((self.options.server,self.options.port)) def parameter_check(self,args,min_args=None,max_args=None,exact_args=None): '''命令參數個數合法性檢查''' if min_args: if len(args) < min_args: print('must provide at least %s parameters,but %s received!'%(min_args,len(args))) return False if max_args: if len(args)>max_args: print('must provide at most %s parameters,but %s received!' % (max_args, len(args))) return False if exact_args: if len(args)!=exact_args: print('need exactly %s parameters,but %s received!' % (exact_args, len(args))) return False return True def send_msg(self,action_type,**kwargs ): """打包消息併發送到遠程""" msg_data = { 'action_type': action_type, 'fill':''#作成定長的 } #把兩個字典合成一個,update方法 msg_data.update(kwargs) bytes_msg = json.dumps(msg_data).encode('utf-8') if len(bytes_msg) < self.MSG_SIZE: msg_data['fill'] = msg_data['fill'].zfill( self.MSG_SIZE - len(bytes_msg)) bytes_msg = json.dumps(msg_data).encode() self.whw_sock.send(bytes_msg) def _get(self,cmd_args): '''從ftp server下載''' if self.parameter_check(cmd_args,min_args=1): filename = cmd_args[0] self.send_msg(action_type='get',filename=filename) response = self.get_response() if response.get('status_code') == 301: file_size = response.get('filesize') receive_size = 0 #進度條功能 progress_generator = self.progress_bar(file_size) progress_generator.__next__() #注意打開方式爲wb f = open('%s.download'%filename,'wb') #循環接收 while receive_size < file_size: if file_size - receive_size <8192:#last recv data = self.whw_sock.recv(file_size-receive_size) else: data = self.whw_sock.recv(8192) receive_size += len(data) f.write(data) #打印進度條 progress_generator.send(receive_size) else: print('\n') print('---file [%s] recv done,received size [%s]---'% (filename,file_size)) f.close() os.replace('%s.download'%filename,filename) else: print(response.get('status_msg')) def _ls(self,args): '''顯示當前目錄的文件列表''' self.send_msg(action_type='ls') response = self.get_response() #定長的 1024 if response.get('status_code') == 302: cmd_result_size = response.get('cmd_result_size') received_size = 0 cmd_resule = b'' while received_size < cmd_result_size: #最後一次接收 小於8192 if cmd_result_size - received_size < 8192: data = self.whw_sock.recv(cmd_result_size - received_size) else: data = self.whw_sock.recv(8192) cmd_resule += data received_size += len(data) else: #windows上gbk解碼 print(cmd_resule.decode('gbk')) def _cd(self,cmd_args): """切換目錄""" #只能跟一個參數 if self.parameter_check(cmd_args, exact_args=1): target_dir = cmd_args[0] self.send_msg('cd',target_dir=target_dir) response = self.get_response() if response.get("status_code") == 350:#dir changed self.terminal_display = "[/%s]" % response.get('current_dir') self.current_dir = response.get('current_dir') def progress_bar(self,total_size,current_percent=0,last_percent=0): '''進度條功能''' while 1: received_size = yield current_percent current_percent = int(received_size / total_size *100) if current_percent > last_percent: print("*" * int(current_percent / 2) + "{percent}%".format(percent=current_percent), end='\r', flush=True) last_percent = current_percent # 把本次循環的percent賦值給last def _put(self,cmd_args): """上傳本地文件到服務器""" #先檢查命令的合法性 if self.parameter_check(cmd_args, exact_args=1): local_file = cmd_args[0] if os.path.isfile(local_file): total_size = os.path.getsize(local_file) self.send_msg('put',file_size=total_size,filename=local_file) f = open(local_file,'rb') uploaded_size = 0 progress_generator = self.progress_bar(total_size) progress_generator.__next__() for line in f: self.whw_sock.send(line) uploaded_size += len(line) progress_generator.send(uploaded_size) else: print('\n') print('file upload done'.center(50,'-')) f.close()
import os import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) if __name__ == '__main__': from core import management argv_parser = management.Management_tool(sys.argv) # 解析並執行指令 argv_parser.execute()
[whw] name = WangHongWei password = e99a18c428cb38d5f260853678922e03 expire = 2019-01-01
import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HOST = '0.0.0.0' PORT = 9090 #用戶的家目錄 USER_HOME_DIR = os.path.join(BASE_DIR,'home') #帳戶信息 ACCOUNT_FILE = os.path.join(BASE_DIR,'conf','accounts.ini') MAX_SOCKET_LISTEN = 5 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) #log文件目錄 LOGGING_FILE = os.path.join(BASE_DIR,'log','whw.log')
import socket import json import configparser import hashlib import os import subprocess import time from conf import settings from core import logger class Ftp_server: '''處理與客戶端全部交互的socket server''' #提早定義 交互信息的狀態碼 STATUS_CODE = { 200: "Passed authentication!", 201: "Wrong username or password!", 300: "File does not exist !", 301: "File exist , and this msg include the file size- !", 302: "This msg include the msg size!", 350: "Dir changed !", 351: "Dir doesn't exist !", 401: "File exist ,ready to re-send !", 402: "File exist ,but file size doesn't match!", } #消息最長定義爲1024 大於的話另作處理 # 消息長度最長爲1024 MSG_SIZE = 1024 def __init__(self,management_instance): #能夠調用management的實例對象 self.management_instance = management_instance #server啓動須要的參數——從settings中取 self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.sock.bind((settings.HOST,settings.PORT)) self.sock.listen(settings.MAX_SOCKET_LISTEN) #用戶信息 self.accounts = self.load_accounts() #存放用戶的登錄信息 self.user_obj = None #用戶當前目錄信息 self.user_current_dir = None def run_forever(self): '''啓動socket server''' print('Starting whw_server on %s:%s'.center(50,'*') % (settings.HOST,settings.PORT) ) while 1: #accept()接收client發送的指令信息 self.request,self.addr = self.sock.accept() print('got a new connection from %s...' % (self.addr,)) try: #全部的交互放到handle方法裏 self.handle() except Exception as e: print('Error happened with client,close connection',e) self.request.close() def handle(self): '''處理與用戶的全部指令的交互''' #循環接收數據 while 1: #服務器收到的信息 raw_data = self.request.recv(self.MSG_SIZE) #空信息...斷開連接 if not raw_data: print('connection %s is lost...' % (self.addr,)) #刪除連接信息、 del self.request,self.addr break data = json.loads(raw_data.decode('utf-8'))#str action_type = data.get('action_type') #反射 根據指令類型調用相應的方法 if action_type: if hasattr(self,'_%s'%action_type): func = getattr(self,'_%s'%action_type) func(data) else: print('invalid command!') def load_accounts(self): '''加載全部帳號信息''' config_obj = configparser.ConfigParser() config_obj.read(settings.ACCOUNT_FILE) print('全部用戶名:',config_obj.sections()) return config_obj def _auth(self, data): """處理用戶認證請求""" print("auth ", data) if self.authenticate(data.get('username'), data.get('password')): print('pass auth....') self.send_response(status_code=200) else: self.send_response(status_code=201) def authenticate(self,username,password): '''用戶認證方法''' if username in self.accounts: _password = self.accounts[username]['password'] md5_obj = hashlib.md5() md5_obj.update(password.encode()) md5_password = md5_obj.hexdigest() if md5_password == _password: # 認證成功後 把用戶的信息存下來 self.user_obj = self.accounts[username] #認證成功後 把用戶文件的位置存下來 self.user_obj['home'] = os.path.join(settings.USER_HOME_DIR,username) #ls方法用到,讓用戶以爲切換到用戶的目錄了 self.user_current_dir = self.user_obj['home'] return True else: print('wrong username or password~') return False else: print('wrong username or password~~') return False def send_response(self,status_code,*args,**kwargs): """打包發送消息給客戶端#用戶信息狀態碼與狀態的對應關係""" data = kwargs data['status_code'] = status_code data['status_msg'] = self.STATUS_CODE[status_code] data['fill'] = '' bytes_data = json.dumps(data).encode('utf-8') #製做定長的報頭 if len(bytes_data) < self.MSG_SIZE: #zfill——返回指定長度字符串,原字符串右對齊,前面填充0 data['fill'] = data['fill'].zfill(self.MSG_SIZE - len(bytes_data)) bytes_data = json.dumps(data).encode('utf-8') #將信息發送給客戶端 self.request.send(bytes_data) def _get(self,data): '''客戶端下載文件須要的方法''' file_name = data.get('filename') #這裏須要將用戶的home路徑與文件名拼接,注意這裏home的調用方法############## full_path = os.path.join(self.user_obj['home'],file_name) if os.path.isfile(full_path): file_size = os.stat(full_path).st_size self.send_response(301,filesize=file_size) print('ready to send file') #開始發送文件 f = open(full_path,'rb') for line in f: self.request.send(line) else: print('file 【%s】 send done...'%file_name) f.close() logger.write_logger('客戶<%s>從服務器下載文件【%s】' % (self.user_obj['name'],file_name)) else: self.send_response(300) def _ls(self,data): '''運行ls命令並將結果返回給client''' #運行命令並拿到結果 注意dir命令須要後面加上用戶當前目錄 cmd_obj = subprocess.Popen('dir %s'%self.user_current_dir,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) stdout = cmd_obj.stdout.read() stderr = cmd_obj.stderr.read() cmd_result = stdout + stderr if not cmd_result: cmd_result = b'current dir has no file at all' #與client交互 self.send_response(302,cmd_result_size = len(cmd_result)) self.request.sendall(cmd_result) def _cd(self,data): """根據用戶的target_dir改變self.user_current_dir 的值""" target_dir = data.get('target_dir') # abspath是爲了解決../..的問題 full_path = os.path.abspath(os.path.join(self.user_current_dir,target_dir) ) print("full path:",full_path)##################################################### #檢測要切換的目錄是否存在 if os.path.isdir(full_path): if full_path.startswith(self.user_obj['home']):#has permission self.user_current_dir = full_path relative_current_dir = self.user_current_dir.replace(self.user_obj['home'], '') self.send_response(350, current_dir=relative_current_dir) else: self.send_response(351) else: self.send_response(351) def _put(self,data): """client uploads file to server""" #客戶端發過來——filename local_file = data.get("filename") # 文件目錄 full_path = os.path.join(self.user_current_dir,local_file) # 表明文件已存在,不能覆蓋, if os.path.isfile(full_path): #建立文件名+時間戳 filename = "%s.%s" %(full_path,time.time()) else: filename = full_path f = open(filename,"wb") total_size = data.get('file_size') received_size = 0 while received_size < total_size: if total_size - received_size < 8192: # last recv data = self.request.recv(total_size - received_size) else: data = self.request.recv(8192) received_size += len(data) f.write(data) #print(received_size, total_size) else: print('file %s recv done'% local_file) f.close() logger.write_logger('客戶<%s>上傳文件【%s】到服務器' % (self.user_obj['name'], local_file))
import logging import os from conf import settings def logger_file(): #生成logger對象 whw_logger = logging.getLogger('whw.log') whw_logger.setLevel(logging.INFO) #生成handler對象 whw_fh = logging.FileHandler(settings.LOGGING_FILE) whw_fh.setLevel(logging.INFO) #生成formatter對象 file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') #formatter綁定到handler對象 whw_fh.setFormatter(file_formatter) #handler對象綁定到logger對象中 whw_logger.addHandler(whw_fh) #返回logger對象的內存地址—— return whw_logger def write_logger(msg): log_obj = logger_file() log_obj.info(msg) log_obj.handlers.pop()
from core import ftp_server class Management_tool: '''負責對用戶輸入的指令進行解析並調用相應的模塊去處理''' def __init__(self,sys_argv): self.sys_argv = sys_argv self.verify_argv() def verify_argv(self): '''驗證指令是否合法''' if len(self.sys_argv) < 2: self.help_msg() #sys_argv[0]默認是文件名,因此取第二個纔是指令 cmd = self.sys_argv[1] #用反射判斷指令是否存在 if not hasattr(self,cmd): print('invalid argument!') self.help_msg() def help_msg(self): msg = ''' start start FTP server stop stop FTP server restart restart FTP server createuser username create a FTP user ''' exit(msg) def execute(self): '''解析並執行指令''' cmd = self.sys_argv[1] func = getattr(self,cmd) func() def start(self): ''' start ftp server''' #實例化對象 server = ftp_server.Ftp_server(self) server.run_forever()
2018-05-23 13:36:13,853 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【wanghw.txt】 2018-05-23 13:36:24,069 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【rrr.mp4】 2018-05-23 13:36:39,862 - whw.log - INFO - 客戶<WangHongWei>上傳文件【whw.mp4】到服務器 2018-05-23 16:38:37,041 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【rrr.mp4】 2018-05-23 16:38:50,163 - whw.log - INFO - 客戶<WangHongWei>上傳文件【whw.mp4】到服務器 2018-05-23 16:39:16,313 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【wanghw.txt】 2018-05-23 16:47:21,678 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【rrr.mp4】 2018-05-23 16:47:31,895 - whw.log - INFO - 客戶<WangHongWei>上傳文件【whw.mp4】到服務器 2018-05-23 16:47:51,453 - whw.log - INFO - 客戶<WangHongWei>上傳文件【eee.txt】到服務器 2018-05-23 17:11:58,296 - whw.log - INFO - 客戶<WangHongWei>從服務器下載文件【wanghw.txt】 2018-05-23 17:12:05,091 - whw.log - INFO - 客戶<WangHongWei>上傳文件【eee.txt】到服務器
4、簡單演示:ide