美文网首页
python 写的ftp服务器和客户端

python 写的ftp服务器和客户端

作者: 两分与桥 | 来源:发表于2018-04-25 08:49 被阅读64次

    有着上传下载功能,添加了一些常用的命令,还有断点续传的功能,有许多bug,当练练手了,这个东西搞了好几天

    python ftp.png

    输入启动命令:python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1

    #ftp_client
    # python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1
    import optparse # 解析获得的命令行参数
    import socket
    import json #传输
    import os
    import sys
    import hashlib #对上传下载文件的 md5 校验
    # 用数字代表一些特定字符,可以减少发送的字符,不过我只用到了几个
    STATUS_CODE  = {
        250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
        251 : "Invalid cmd ",
        252 : "Invalid auth data",
        253 : "Wrong username or password",
        254 : "Passed authentication",
        255 : "Filename doesn't provided",
        256 : "File doesn't exist on server",
        257 : "ready to send file",
        258 : "md5 verification",
    
        800 : "the file exist,but not enough ,is continue? ",
        801 : "the file exist !",
        802 : " ready to receive datas",
    
        900 : "md5 valdate success"
    
    }
    
    #输入启动命令:python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1
    #定义一个类,实例化在最下面
    class ClientHandler:
        def __init__(self): # 解析参数
            self.op = optparse.OptionParser()
            self.op.add_option('-S', '--server', dest='server')
            self.op.add_option('-P', '--port', dest='port')
            self.op.add_option('-U', '--username', dest='username')
            self.op.add_option('-p', '--password', dest='password')
            self.options, self.args = self.op.parse_args()
            # print(self.options)
            # print(self.args)
            self.verify_args() # 检测输入是否合法
            self.make_connect() # 建立连接
            self.mainPath = os.path.dirname(os.path.abspath(__file__)) #获得文件的执行路径,上传下载时需要用到
        # 检测输入的是否合法,我这里只检测了端口值
        def verify_args(self):
                if int(self.options.port) >0 and int(self.options.port)<65535:
                    return True
                else:
                    exit('port is error')
        # 建立连接,传入元组形式的 ip 地址和端口值,(要先启动服务器)
        def make_connect(self):
            self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.s.connect((self.options.server, int(self.options.port)))
            print('connect successful')
    
        def get_response(self):
            data= self.s.recv(1024)
            data = json.loads(data.decode('utf-8'))
            return data
        # 通信开始,用的也是反射
        def interactive(self):
            if self.authenticate():
                print('pass')
                while True:
                    cmd_info = input('[%s]' %self.current_dir).strip()
                    if len(cmd_info) == 0:continue # 如果输入为空,重新输入一遍,TCP协议不能发送空
                    cmd_list = cmd_info.split()
                    if hasattr(self, cmd_list[0]):
                        func = getattr(self, cmd_list[0])
                        func(*cmd_list)
                    else:
                        print('please input again')
        def ls(self, *cmd_list):
            data = {
                'action' : 'ls'
            }
            self.s.sendall(json.dumps(data).encode('utf-8')) #把 data 转成json字符串发送给服务器
            msg = self.s.recv(1024).decode('utf-8')
            if msg != 'empty': # 用 empty 代替发送为空
                print(msg)
    
        def cd(self, *cmd_list):
            cmd_list = list(cmd_list)
            if len(cmd_list) == 1: #如果命令中只有一个 cd 时,返回最上级目录,也就是 home 下的家目录
                cmd_list.append('top')
            if len(cmd_list) != 2: # cmd_list 的长度不等于2,就是输入出错了
                print('please input again')
                return
            data = {
                'action' : 'cd',
                'dirname' : cmd_list[1]
            }
            self.s.sendall(json.dumps(data).encode('utf-8'))
            msg = self.s.recv(1024).decode('utf-8')
            if msg != 'no':
                self.current_dir = msg[msg.find('libai', 10):] #
                #libai 是我定义的账号名称,在home 下会有一个libai的文件作为家目录
                # print(self.current_dir)
            else:
                print('no this dir')
    
        def put(self, *cmd_list):
            print('put')
            # put 12.jpg image 命令形式
            s = hashlib.md5() #实例一个 md5,检查文件是否传输无误
            action,local_path,target_path = cmd_list
            local_path = os.path.join(self.mainPath, local_path)
            #把 self.mainPath 和 image 合成一个完整目录
    
            file_name = os.path.basename(local_path) #这句好像是多余的,额
            file_size = os.stat(local_path).st_size
    
            data = {
                'action' : 'put',
                'file_name' : file_name,
                'file_size' : file_size,
                'target_path' : target_path
            }
            self.s.send(json.dumps(data).encode('utf-8'))
    
            ###############################################
            has_send = 0
            is_exit = self.s.recv(1024).decode('utf-8')
    
            if is_exit == '800':
                #文件不完整
                choice = input('the file is exist, but no enough, is continue? [Y/N  ]').strip()
                if choice.upper() == 'Y':#是否续传
                    self.s.sendall('Y'.encode('utf-8'))
                    has_send = int(self.s.recv(1024).decode('utf-8')) #获得已经传输的文件大小
                else:
                    self.s.sendall('N'.encode('utf-8'))
            elif is_exit == '801':
                #文件存在,就直接返回了
                print('the file is exist')
                return
            #如果文件已经传输了一部分,需要续传,重新计算md5 的值,
            print('send filename: %s, filesize: %s' %(file_name, file_size))
            f = open(local_path, 'r+b')
            has_send_b = has_send
            if has_send > 0:
                while True:
                    if has_send - 1024 > 0:
                        s.update(f.read(1024)) # 更新md5
                        has_send = has_send - 1024
                    else:
                        s.update(f.read(has_send))
                        break
            f.seek(has_send_b) #这句好像也是多余的额,
            print('has_send_b = ', has_send_b)
            while has_send_b < file_size:
                data = f.read(1024)
                s.update(data) # 更新md5
                self.s.sendall(data)
                has_send_b += len(data)
                self.show_progress(has_send_b, file_size) #这是一个进度条函数
            f.close()
    
            self.s.sendall(str(s.hexdigest()).encode('utf-8')) #收发client和server的md5
            server_md5 = self.s.recv(1024).decode('utf-8')
            if server_md5 == s.hexdigest():
                print('\nMd5 checksum succeeded, Uploaded successfully')
            else:
                print('\nMd5 check failed ,upload failed')
    
            #time.sleep(20)
        # 下载和上传也是差不多了,我不写了
        def downloads(self, *cmd_list):
            # downloads 12.jpg
            s = hashlib.md5()
            if len(cmd_list) != 2:
                print('please input again')
                return
            data = {
                'action' : 'downloads',
                'file_name' : cmd_list[1],
            }
    
            self.s.sendall(json.dumps(data).encode('utf-8'))
            msg = self.s.recv(1024).decode('utf-8')
            print(msg)
            path = os.path.dirname(os.path.abspath(__file__))
            path = os.path.join(path, cmd_list[1]).replace('\\', '/')
            has_send = 0
    
            if msg == 'exist this file':
                if os.path.exists(path):
                    print('you want to continue transmission?')
                    choice = input('[Y/N]').upper()
                    if choice == 'Y':
                        has_send = os.stat(path).st_size
                        f = open(path, 'r+b') #keep going
                    else:
                        f = open(path, 'r+b') #downloading
                else:
                    f = open(path, 'wb')
            else:
                print(msg)#bu cun zai wen jian
                return
            file_size = int(self.s.recv(1024).decode('utf-8'))
            self.s.sendall(str(has_send).encode('utf-8'))
            print('downloading')
            has_send_b = has_send
            f.seek(0)
            if has_send > 0:
                while True:
                    if has_send - 1024 > 0:
                        mess = f.read(1024)
                        s.update(mess)
                        has_send = has_send - 1024
                    else:
                        s.update(f.read(has_send))
                        break
            f.seek(has_send_b)
            print('has_send = ', has_send)
            while has_send_b < file_size:
                message = self.s.recv(1024)
                f.write(message)
                s.update(message)
                has_send_b += len(message)
                self.show_progress(has_send_b, file_size)
            f.close()
            self.s.sendall(str(s.hexdigest()).encode('utf-8'))
            server_md5 = self.s.recv(1024).decode('utf-8')
            if server_md5 == s.hexdigest():
                print('\nMd5 checksum succeeded, Uploaded successfully')
            else:
                print('\nMd5 check failed ,upload failed')
    
    
            # print('downloads data = ', data)
            # self.s.sendall(json.dumps(data).encode('utf-8'))
            # msg = self.s.recv(1024)
            # print(msg)
        #创建文件夹
        def mkdir(self, *cmd_list):
            if len(cmd_list) != 2:
                print('please input again')
                return
            data = {
                'action' : 'mkdir',
                'dirname' : cmd_list[1]
            }
            self.s.sendall(json.dumps(data).encode('utf-8'))
            msg = self.s.recv(1024).decode('utf-8')
            print(msg)
        #删除文件夹或文件
        def rm(self, *cmd_list):
            if len(cmd_list) != 2:
                print('please input again')
                return
            data = {
                'action' : 'rm',
                'dirname' : cmd_list[1]
            }
            self.s.sendall(json.dumps(data).encode('utf-8'))
            msg = self.s.recv(1024).decode('utf-8')
            print(msg)
    
        # 这个是进度条函数
        def show_progress(self, has_send, file_size):
            procentage = int(float(has_send)/file_size*100)
            sys.stdout.write('%s%% %s\r' %(procentage, '*'*procentage))
    
        #这个是检测你是否输入了用户名和密码,如果没有,会提示你输入的
        def authenticate(self):
            if self.options.username == None or self.options.password == None:
                username = input('username : ')
                password = input('password : ')
                return self.get_auth_result(username, password)
            return self.get_auth_result(self,self.options.username,self.options.password)
    
        #校验你的用户名和密码对不对
        def get_auth_result(self, username, password):
            data = {
                'action' : 'auth',
                'username' : username,
                'password' : password
            }
            self.s.send(json.dumps(data).encode('utf-8'))
            response = self.get_response()
    
            print(response)
            print('status_code : ', response['status_code'])
            if response['status_code'] == 254:
                self.username = username
                self.current_dir = username
                print(STATUS_CODE[254])
                return True
            else:
                print(STATUS_CODE[response['status_code']])
    
    ch = ClientHandler() # 实例化的同时,连接上服务器
    ch.interactive() # 通信开始
    
    

    ftp_server 我分为几个模块来写,这是一个启动文件,整个 ftp_server 的入口 ,启动命令: C:/PycharmProjects/begin/ftp_server/bin/ftp_server.py srart,当然每个人的文件存放路径都不相同了

    # ftp_server 启动
    
    import sys,os
    base_path = os.path.dirname(os.path.dirname(__file__))# 获得本文件的上上级目录,也就是ftp_server目录
    sys.path.append(base_path) # 将 base_path 添加到临时的环境变量
    print(base_path)
    from core import main  # 在环境变量中就可以找到 core 文件夹了
    main.test() # 用文件名加类名运行
    
    

    setting 配置文件

    #配置文件
    
    import os
    BASE_DIR = os.path.dirname(os.path.dirname(__file__))
    
    IP = '127.0.0.1'
    PORT = 8889
    
    ACCOUNT_PATH = os.path.join(BASE_DIR, 'conf', 'accounts.cfg')
    
    

    accounts.cfg 账号密码文件

    
    [DEFAULT]
    
    [libai]
    password = 123456
    quotation = 100
    
    [root]
    password = root
    quotation = 100
    
    

    main.py 文件

    import optparse #解析出start
    import socketserver
    
    # 在启动文件中已经加入了环境变量,不需要再添加了
    from conf import setting
    from core import server
    
    # 启动 ftp_server 的命令:python C:\PycharmProjects\begin\ftp_server\bin\ftp_server.py start
    # 运行 test 会首先运行 test 的构造方法 __init__,解析命令行参数,
    # 用反射,可以十分方便的增添函数,
    class test:
        def __init__(self):
            self.op = optparse.OptionParser()
            options, args = self.op.parse_args()
            print(options)
            print(args)
            if hasattr(self, args[0]): # 传入的参数是 start ,用 hasattr 检测类是否有 start 方法,有返回True
                func = getattr(self,args[0]) # 存在 start 方法,用 getattr 获取,加括号运行
                func()
        def start(self):
            print('waiting for connect') # 连接
            # 用 socketserver 内置的类实例化 s ,并传入自定义的 server 文件中的 ServerHandler 类
            s = socketserver.ThreadingTCPServer((setting.IP, setting.PORT), server.ServerHandler)
            # 启动 实例化的 s 对象
            s.serve_forever()
    
    

    最核心的文件,server.py

    import socketserver
    import json #作为传输
    import configparser #解析存储的账号密码
    from conf import setting
    import os
    import shutil #删除文件夹的库
    import hashlib
    
    STATUS_CODE  = {
        250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
        251 : "Invalid cmd ",
        252 : "Invalid auth data",
        253 : "Wrong username or password",
        254 : "Passed authentication",
        255 : "Filename doesn't provided",
        256 : "File doesn't exist on server",
        257 : "ready to send file",
        258 : "md5 verification",
    
        800 : "the file exist,but not enough ,is continue? ",
        801 : "the file exist !",
        802 : " ready to receive datas",
    
        900 : "md5 valdate success"
    
    }
    #定义一个继承 socketserver.BaseRequestHandler 的类,实例化时自动运行 handle 方法
    class ServerHandler(socketserver.BaseRequestHandler):
        def handle(self):
            print(self.request) # self.request 就是类同与 socket 定义服务器时返回的实例对象
            print(self.client_address) #
            while True:
                data = self.request.recv(1024)
                data = json.loads(data.decode('utf-8'))
                if data.get('action'):
                    if hasattr(self, data.get('action')):
                        func = getattr(self, data.get('action'))
                        func(**data)
                    else:
                        print('invaild')
                else:
                    print('invaild')
    
        def send_response(self, status_code):
            response = {'status_code': status_code}
            self.request.sendall(json.dumps(response).encode('utf-8'))
    
        def auth(self, **data):
            username = data['username']
            password = data['password']
            username = self.authenticate(username,password)
            if username:
                self.send_response(254) #正常返回
            else:
                self.send_response(253) #用户名或密码错误返回
    
        def authenticate(self, username, password): #验证用户名了密码,我放在了conf/accounts.cfg
            cfg = configparser.ConfigParser()
            cfg.read(setting.ACCOUNT_PATH)
            if username in cfg.sections():
                if password == cfg[username]['Password']:
                    print('login successful')
                    self.username= username
                    self.mainPath = os.path.join(setting.BASE_DIR,'home',self.username).replace('\\','/')
                    return username
    
        def put(self, **data):
            s = hashlib.md5()
            print('put')
            print('data : ', data)
            print(type(data))
            file_name = data['file_name']
            file_size = data['file_size']
            target_path = data['target_path']
            print('file_name = ', file_name)
            print('file_size = ', file_size)
            print('target_path = ', target_path)
    
            abs_path = os.path.join(self.mainPath,target_path,file_name)
            print('self.mainPath = ', self.mainPath)
            print('target_path = ', target_path)
            print('file_name = ', file_name)
            print(abs_path)
    
            ################################################
            has_received = 0
        #断点续传十分重要的就是调整文件指针的位置
            if os.path.exists(abs_path):
                file_has_size = os.stat(abs_path).st_size
                if file_has_size < file_size:
                    #断点续传
                    self.request.sendall('800'.encode('utf-8'))
                    choice = self.request.recv(1024).decode('utf-8')
                    if choice == "Y":
                        self.request.sendall(str(file_has_size).encode('utf-8'))
                        has_received = file_has_size
                        f = open(abs_path, 'r+b') #
                    else:
                        f = open(abs_path, 'r+b')
                else:
                    # 文件存在
                    self.request.sendall('801'.encode('utf-8'))
                    return
            else:
                self.request.sendall('802'.encode('utf-8'))
                f = open(abs_path, 'wb')
            print('putting : %s' %file_name)
            has_received_b = has_received
            if has_received > 0:
                f.seek(0)
                while True:
                    if has_received - 1024 > 0:
                        s.update(f.read(1024))
                        has_received = has_received - 1024
                    else:
                        s.update(f.read(has_received))
                        break
            f.seek(has_received_b)
            print('has_received_b = ',has_received_b)
            while has_received_b < file_size:
                msg = self.request.recv(1024)
                f.write(msg)
                s.update(msg)
                has_received_b += len(msg)
            f.close()
            client_md5 = self.request.recv(1024).decode('utf-8')
            self.request.sendall(str(s.hexdigest()).encode('utf-8'))
            if client_md5 == s.hexdigest():
                print('Md5 checksum succeeded, Uploaded successfully')
            else:
                print('Md5 check failed ,upload failed')
    
        def ls(self, **data): #接收到传来的数据 data
            print('ls')
            file_list = os.listdir(self.mainPath) #列出在当前目录下的所有文件
            file_str = '\n'.join(file_list) #获得的为列表,转成字符串
            if not len(file_list):
                file_str = 'empty'
            self.request.sendall(file_str.encode('utf-8')) #将结果发送回去
    
        def cd(self, **data):
            top_directory = 'C:PycharmProjects/begin/ftp_server/home/libai'#需要用的话要这里要改一下路径啊
            print('cd')
            dirname = data.get('dirname')
            if dirname == 'top': #返回顶级目录
                self.mainPath = top_directory
            elif dirname =='..': # 返回上一层目录
                if self.mainPath != top_directory:
                    self.mainPath = os.path.dirname(self.mainPath)
            else: #else 就是进入某某目录了
                back = self.mainPath
                self.mainPath = os.path.join(self.mainPath, dirname).replace('\\','/')
                if not os.path.isdir(self.mainPath):
                    self.mainPath = back
                    self.request.sendall('no'.encode('utf-8'))
                    return
            self.request.sendall(self.mainPath.encode('utf-8'))
    
        def mkdir(self, **data):#创建文件夹,嵌套或者单个文件夹
            print('mkdir')
            dirname = data.get('dirname')
            path = os.path.join(self.mainPath, dirname).replace('\\','/')
    
            if not os.path.exists(path):
                if '/' in dirname:
                    os.makedirs(path)
                else:
                    os.mkdir(path)
                self.request.sendall('Created successfully'.encode('utf-8'))
            else:
                self.request.sendall('Directory already exists'.encode('utf-8'))
    
        def rm(self, **data): #删除命令,嵌套或者单个文件
            print('rm')
            dirname = data.get('dirname')
            path = os.path.join(self.mainPath, dirname).replace('\\', '/')
    
            if not os.path.exists(path):
                print('wen jian bu cun zai')
                self.request.sendall('no'.encode('utf-8'))
            else:
                try:
                    shutil.rmtree(path) #这个是嵌套删除文件
                except:
                    os.remove(path) #删除单个文件
                print('delete')
                self.request.sendall('remove'.encode('utf-8'))
    
        #下载和上传是一样的了,
        def downloads(self, **data):
            # downloads 12.jpg
            print('downloads')
            s = hashlib.md5()
            print('data : ', data)
    
            file_name = data['file_name']
            abs_path = os.path.join(self.mainPath,file_name).replace('\\', '/')
            print('self.mainPath = ', self.mainPath)
    
            print('abs_path = ', abs_path)
    
            if os.path.exists(abs_path):
                print('have this abs_path')
                self.request.sendall('exist this file'.encode('utf-8'))
            else:
                self.request.sendall('no this file'.encode('utf-8'))
                return
            ###############################################
            file_size = os.stat(abs_path).st_size
    
            print('file_size = ', file_size)
            self.request.sendall(str(file_size).encode('utf-8'))
            has_send = int(self.request.recv(1024).decode('utf-8'))
            f = open(abs_path, 'rb')
            has_send_b = has_send
            if has_send > 0:
                while True:
                    if has_send - 1024 > 0:
                        s.update(f.read(1024))
                        has_send = has_send - 1024
                    else:
                        s.update(f.read(has_send))
                        break
            f.seek(has_send_b)
            print('has_send = ', has_send)
            while has_send_b < file_size:
                message = f.read(1024)
                s.update(message)
                self.request.sendall(message)
                has_send_b += len(message)
            f.close()
            client_md5 = self.request.recv(1024).decode('utf-8')
            self.request.sendall(str(s.hexdigest()).encode('utf-8'))
            if client_md5 == s.hexdigest():
                print('Md5 checksum succeeded, Uploaded successfully')
            else:
                print('Md5 check failed ,upload failed')
    
    #我自我觉得功能还是不错的,有些bug,修改了一下,发现bug还挺多,就算了,
    #当练练手了,
    

    对了,我的家目录设置在 ftp_server 下的home/libai 和root,上传默认在ftp_clent.py 目录下寻找文件上传,用 put 12.jpg image 这样的形式,要不然报错啊,会提示列表出界

    下载啊,在你所在的家目录下的所在的路径下载,下载到ftp_client 所在的目录
    大概就是这样了

    总结: 断点续传主要就是要调整文件指针所在的位置,而文件指针的位置可以用已存在的文件大小来得出,
    追加模式的文件指针是默认在文件末尾的
    好了,就记录到这里了,
    分享一下文件:链接:https://pan.baidu.com/s/1CCaIpw5D48Lf0cUFUud2zw 密码:lz2r
    (不知道啥时候会和谐掉呢)

    相关文章

      网友评论

          本文标题:python 写的ftp服务器和客户端

          本文链接:https://www.haomeiwen.com/subject/wsgflftx.html