• 简单的单进程FTP服务器的实现


      一、功能说明:

    1.本程序基于socket实现客户端与服务器端的单进程交互

    2.用到的用户名:whw,密码abc123——服务器端密码的验证利用hashlib模块进行MD5的编码以确保通信安全。

    3.客户端登陆成功后可以查看自己再服务器上的文件夹里文件的列表;可以在自己所在的目录随意切换;可以将服务器端自己文件夹中的文件下载到客户端;可以将自己本端的文件下载到服务器端自己的文件夹里去

    4.客户端上传并下载文件有日志记录

      二、目录结构

    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.txt

      三、程序源代码

    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('
    ')
                        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='
    ',
                          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('
    ')
                        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

      四、简单演示:

  • 相关阅读:
    存储过程分页,前台应用范例repeater分页
    引用真正分页控件(与存储过程联合使用)页面
    分页存储过程repeater分页
    查找DetailsView1数据控件中的数据
    c#读取文件
    SQL触发器实例讲解
    TreeView节点选中问题
    C# 实现版本自动更新
    .Net那点事儿系列:C#操作Xml:通过XmlDocument读写Xml文档
    Linq学习笔记
  • 原文地址:https://www.cnblogs.com/paulwhw/p/9077978.html
Copyright © 2020-2023  润新知