簡單的單進程FTP服務器的實現

  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)
whw_client.py
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()
ftp_client.py
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_server.py
[whw]
name = WangHongWei
password = e99a18c428cb38d5f260853678922e03
expire = 2019-01-01
accounts.ini
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')
settings.py
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))
ftp_server.py
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()
logger.py
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()
management.py
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】到服務器
whw.log

  4、簡單演示:ide

相關文章
相關標籤/搜索