实现ftp_FTP文件服务器的实现

FTP文件服务器

"""
FTP服务器(File Transfer Protocol Server)是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务。 FTP是File Transfer Protocol(文件传输协议)。顾名思义,就是专门用来传输文件的协议。简单地说,支持FTP协议的服务器就是FTP服务器。
"""

从上述定义中,我们知道了文件服务器的两个核心功能:上传和下载

在现实生活中,常用的文件服务器有哪些了?

Xftp,WinScp,FileZilla

如何实现FTP文件服务器

"""
我们需要一个客户端,和一个服务端(基本需求):
1、客户端用来发送服务请求
2、服务端用来处理服务请求,并返回相应的数据给客户端
3、使用socket来实现客户端和服务端之间的通信
"""

实验准备

为了模拟并发的效果,我们准备两个客户端,一个服务端

"""
服务端:阿里云服务器(172.21.44.11)
客户端1:本地虚拟机1(192.168.80.129)
客户端2:本地虚拟机1(192.168.80.130)
"""

主要实现功能

"""
1.文件上传
2.文件下载
3.文件MD5校验
4.文件进度条显示
"""

使用模块

import os
import sys
import struct
import json
import hashlib
import argparse
from prettytable import PrettyTable
from socket import *

客户端代码

"""
​
FTP客户端
time:2020/4/21
version:1.0
coder:surpass
​
"""
​
import os
import sys
import struct
import json
import hashlib
import argparse
from prettytable import PrettyTable
from socket import *
​
func_dic = {
    'upload': ['上传文件', 'upload'],
    'download': ['下载文件', 'download']
}
​
base_path = os.path.dirname(os.path.abspath(__file__))
​
​
def get_file_md5(file_path):
    if os.path.isfile(file_path):
        size = os.path.getsize(file_path) // 5
        m = hashlib.md5()
        with open(file_path, mode='rb') as f:
            for i in range(4):
                m.update(f.read(5))
                f.seek(size, 1)
        return m.hexdigest()
​
​
def check_file_md5(filename, file_md5):
    size = os.path.getsize(filename) // 5
    m = hashlib.md5()
    with open(filename, mode='rb') as f:
        for i in range(4):
            m.update(f.read(5))
            f.seek(size, 1)
    if m.hexdigest() == file_md5:
        return True
​
​
def get_pretty_tb(table_list, *args):
    tb = PrettyTable(field_names=table_list)
    for item in args:
        tb.add_row(item)
    return tb
​
​
class MYFTPClient:
    def __init__(self, ip_address, buf_size=1024):
        self.ip_address = ip_address
        self.buf_size = buf_size
        self.client = socket(AF_INET, SOCK_STREAM)
        self.client_connect()
​
    def client_connect(self):
        try:
            self.client.connect_ex(self.ip_address)
        except Exception as e:
            sys.stderr.write(f'connect error:{e}')
​
    def client_close(self):
        self.client.close()
​
    def send_head_pack(self, cmd=None, file=None, remote_path=None, file_path=None,
                       filename=None, file_size=None, file_md5=None):
        """
        cmd:操作命令,file:文件或文件夹集合,remote_path:远程路径,file_path:文件路径
        filename:文件名,file_size:文件大小,file_md5:文件的md5值
        """
        head_dic = {'cmd': cmd, 'file': file, 'remote_path': remote_path, 'file_path': file_path,
                    'filename': filename, 'file_size': file_size, 'file_md5': file_md5}
        head_dic = {k: v for k, v in head_dic.items() if v is not None}
        # print(head_dic)
        head_json = json.dumps(head_dic)
        head_bytes = bytes(head_json, encoding='utf-8')
        head_struct = struct.pack('i', len(head_bytes))
        # 将报头传给服务端
        self.client.send(head_struct)
        self.client.send(head_bytes)
​
    def show_remote_path(self, remote_path=None):
        """
        获取远端的文件夹,包括文件和文件夹
        """
        self.send_head_pack(cmd='show_remote_path', file={}, remote_path=remote_path)
​
        recv_size = struct.unpack('i', self.client.recv(4))[0]
        file_dic = json.loads(self.client.recv(recv_size).decode('utf-8'))
​
        print('获取远程路径'.center(60, '='))
        print(f"{file_dic.get('base_path')}")
        table_list = ['名字', '属性', '大小', '创建日期']
        result = []
        file_list = []
        dir_list = []
        for item in file_dic['file_list']:
            item.insert(1, 'file')
            result.append(item)
            file_list.append(os.path.join(file_dic.get('base_path'), item[0]))
        for obj in file_dic['dir_list']:
            obj.insert(1, 'dir')
            result.append(obj)
            dir_list.append(os.path.join(file_dic.get('base_path'), obj[0]))
        tb_show = get_pretty_tb(table_list, *result)
        print(tb_show)
        dir_list.append(os.path.dirname(file_dic.get('base_path')))
        return file_list, dir_list
​
    def show_progress(self, func=None, filename=None, percent=None):
        res = int(50 * percent) * '#'
        sys.stdout.write('r%s %s[%-50s] %d%%' % (func, filename, res, int(100 * percent)))
        sys.stdout.flush()
​
    def upload(self, msg):
        cmd = msg
        while 1:
            filepath = input('请输入上传文件的路径>>:').strip()
            if not filepath: continue
            break
        filename = os.path.basename(filepath)
        file_size = os.path.getsize(filepath)
        file_md5 = get_file_md5(filepath)
        if not file_md5:
            raise FileNotFoundError(f'file does not exits:{filepath}')
        self.send_head_pack(cmd=cmd, filename=filename, file_size=file_size, file_md5=file_md5)
        current_size = 0
        with open(filepath, mode='rb') as f:
            for line in f:
                current_size += len(line)
                self.show_progress('upload', filename, current_size // file_size)
                self.client.send(line)
        res = self.client.recv(4)
        msg_len = struct.unpack('i', res)[0]
        msg = self.client.recv(msg_len).decode('utf-8')
        print(f'n{msg}')
​
    def download(self, msg):
        cmd = msg
        file_all = self.show_remote_path()
        while 1:
            choice = input("请选择相应的文件下载,或者通过选相应的路径切换路径:")
            if choice in file_all[1]:
                file_all = self.show_remote_path(choice)
                continue
            elif choice in file_all[0]:
                self.send_head_pack(cmd=cmd, file_path=choice, filename=os.path.basename(choice))
​
                file_struct = struct.unpack('i', self.client.recv(4))[0]
                file_json = self.client.recv(file_struct).decode('utf-8')
                file_dic = json.loads(file_json)
                # print(file_dic)
                filename = file_dic.get('filename')
                file_size = file_dic.get('file_size')
                download_size = 0
                while download_size < file_size:
                    content = self.client.recv(1024)
                    download_size += len(content)
                    self.show_progress('download', filename, download_size // file_size)
                    with open(os.path.join(base_path, filename), mode='ab+') as fw:
                        fw.write(content)
                else:
                    file_path = os.path.join(base_path, filename)
                    if check_file_md5(file_path, file_dic.get('file_md5')):
                        sys.stdout.write(f'ndownload {filename} success!n')
                    else:
                        os.remove(file_path)
                        sys.stderr.write(f'nfiled to download {filename}!n')
                break
            else:
                print('请输入正确的文件或路径!')
​
    def run(self):
        while 1:
            func_table_list = ['功能选项', '功能名称', '操作示例']
            result = [(lambda x, y, z: [x, y, z])(k, v[0], v[1]) for k, v in func_dic.items()]
            tb_func = get_pretty_tb(func_table_list, *result)
            msg = input(f'{tb_func}nplease enter the operate signal>>:').strip()
            if not msg: continue
            if hasattr(self, msg):
                func = getattr(self, msg)
                func(msg)
            _continue = input('是否继续进行操作(y,n):').strip().lower()
            if _continue == 'n': break
        self.client_close()
​
​
def main():
    parser = argparse.ArgumentParser(description='MY FTP Client v1.0')
    parser.add_argument('--ip', action='store', dest='ip', required=True, help='The remote ip to connect')
    parser.add_argument('--port', action='store', dest='port', required=True, type=int, help='The port to connect')
    parser.add_argument('--buf_size', action='store', dest='buf_size', type=int,
                        help='the max size(one time) to upload/download')
    args = parser.parse_args()
    if args.buf_size:
        client = MYFTPClient((args.ip, args.port), args.buf_size)
    else:
        client = MYFTPClient((args.ip, args.port))
    client.run()
​
​
if __name__ == '__main__':
    main()

服务端代码

基于socketserver实现并发

"""
FTP服务端
time:2020/4/21
version:v1.0
coder:surpass
"""
​
import os
import sys
import json
import struct
import time
import hashlib
import socketserver
​
IP_ADDRESS = ('0.0.0.0', 9090)
BUF_SIZE = 1024
​
​
def get_file_md5(file_path):
    if os.path.isfile(file_path):
        size = os.path.getsize(file_path) // 5
        m = hashlib.md5()
        with open(file_path, mode='rb') as f:
            for i in range(4):
                m.update(f.read(5))
                f.seek(size, 1)
        return m.hexdigest()
​
​
def check_file_md5(filename, file_md5):
    size = os.path.getsize(filename) // 5
    m = hashlib.md5()
    with open(filename, mode='rb') as f:
        for i in range(4):
            m.update(f.read(5))
            f.seek(size, 1)
    if m.hexdigest() == file_md5:
        return True
​
​
class MYFTPServer(socketserver.BaseRequestHandler):
    def handle(self):
        while 1:
            try:
                head_struct = self.request.recv(4)
                if not head_struct: break
                head_len = struct.unpack('i', head_struct)[0]
                head_dic_json = self.request.recv(head_len).decode('utf-8')
                head_dic = json.loads(head_dic_json)
                if hasattr(self, head_dic.get('cmd')):
                    func = getattr(self, head_dic.get('cmd'))
                    func(head_dic)
            except Exception as e:
                sys.stderr.write(f'error message:{e}')
        self.request.close()
​
    def show_remote_path(self, head_dic):
        if not head_dic.get('remote_path'):
            base_path = os.path.dirname(os.path.abspath(__file__))
        else:
            base_path = head_dic.get('remote_path')
        head_dic['file']['base_path'] = base_path
        head_dic['file']['file_list'] = []
        head_dic['file']['dir_list'] = []
        for obj in os.listdir(base_path):
            obj_abs_path = os.path.join(base_path, obj)
            obj_name = os.path.basename(obj_abs_path)
            obj_size = os.path.getsize(obj_abs_path)
            timestamp = os.path.getmtime(obj_abs_path)
            obj_mtime = time.strftime('%Y/%m/%d %X', time.localtime(timestamp))
            if os.path.isfile(obj_abs_path):
                head_dic['file']['file_list'].append([obj_name, obj_size, obj_mtime])
            elif os.path.isdir(obj_abs_path):
                obj_size = ''
                head_dic['file']['dir_list'].append([obj_name, obj_size, obj_mtime])
        head_json = json.dumps(head_dic.get('file'))
        head_bytes = bytes(head_json, encoding='utf-8')
        head_struct = struct.pack('i', len(head_bytes))
​
        self.request.send(head_struct)
        self.request.send(head_json_bytes)
​
    def upload(self, head_dic):
        file_size = head_dic.get('file_size')
        recv_size = 0
        while recv_size < file_size:
            content = self.request.recv(BUF_SIZE)
            recv_size += len(content)
            with open(head_dic.get('filename'), mode='ab+') as f:
                f.write(content)
        else:
            if check_file_md5(head_dic.get('filename'), head_dic.get('file_md5')):
                msg = f"upload file {head_dic.get('filename')} success!"
            else:
                os.remove(head_dic.get('filename'))
                msg = f"failed to upload file,file {head_dic.get('filename')} is damaged!"
            msg_len = struct.pack('i', len(msg.encode('utf-8')))
            self.request.send(msg_len)
            self.request.send(msg.encode('utf-8'))
​
    def download(self, head_dic):
        file_path = head_dic.get('file_path')
        filename = os.path.basename(file_path)
        file_md5 = get_file_md5(file_path)
        file_size = os.path.getsize(file_path)
        file_dic = {'filename': filename, 'file_size': file_size, 'file_md5': file_md5}
        file_json = json.dumps(file_dic)
        file_bytes = bytes(file_json, encoding='utf-8')
        file_struct = struct.pack('i', len(file_bytes))
​
        # 发送报头消息
        self.request.send(file_struct)
        self.request.send(file_bytes)
​
        with open(file_path, mode='rb') as fr:
            for line in fr:
                self.request.send(line)
​
​
def main():
    server = socketserver.ThreadingTCPServer(IP_ADDRESS, MYFTPServer)
    server.serve_forever()
​
​
if __name__ == '__main__':
    main()

但是采用socketserver实现并发,容易遇到地址占用问题:

2298c576d6744a058bc4d29560600904.png

解决方案:

"""
1.使用Thread实现并发
2.使用 setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)解决地址占用问题
"""

基于Thread实现并发

"""
FTP服务端
time:2020/4/24
version:v2.0
coder:surpass
"""
​
import os
import sys
import json
import struct
import time
import hashlib
from socket import *
from threading import Thread
​
IP_ADDRESS = ('0.0.0.0', 9090)
BUF_SIZE = 1024
BACK_LOG = 5
​
​
class MYFTPServer:
    def __init__(self, ip_address):
        self.ip_address = ip_address
        self.server = socket(AF_INET, SOCK_STREAM)
​
    def bind(self):
        self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        self.server.bind(self.ip_address)
​
    def listen(self):
        self.server.listen(BACK_LOG)
​
    def accept(self):
        return self.server.accept()
​
    @staticmethod
    def show_remote_path(head_dic, conn):
        if not head_dic.get('remote_path'):
            remote_path = os.path.dirname(os.path.abspath(__file__))
        else:
            remote_path = head_dic.get('remote_path')
        head_dic['file']['base_path'] = remote_path
        head_dic['file']['file_list'] = []
        head_dic['file']['dir_list'] = []
        for item in os.listdir(remote_path):
            obj_path = os.path.join(remote_path, item)
            obj_mtime = os.path.getmtime(obj_path)
            if os.path.isfile(obj_path):
                head_dic['file']['file_list'].append(
                    os.path.basename(obj_path),
                    os.path.getsize(obj_path),
                    time.strftime('%Y-%m-%d %X', time.localtime(obj_mtime))
                )
            elif os.path.isdir(obj_path):
                head_dic['file']['dir_list'].append(
                    os.path.basename(obj_path), '',
                    time.strftime('%Y-%m-%d %X', time.localtime(obj_mtime))
                )
        head_json = json.dumps(head_dic['file'])
        head_bytes = bytes(head_json, encoding='utf-8')
        head_struct = struct.pack('i', len(head_bytes))
​
        conn.send(head_struct)
        conn.send(head_bytes)
​
    @staticmethod
    def check_file_md5(filename, file_size, file_md5):
        m = hashlib.md5()
        move_size = file_size // 5
        with open(filename, mode='rb') as fr:
            for i in range(4):
                m.update(fr.read(5))
                fr.seek(move_size, 1)
        if m.hexdigest() == file_md5:
            return True
​
    @staticmethod
    def get_file_md5(file_path, file_size):
        m = hashlib.md5()
        move_size = file_size // 5
        with open(file_path, mode='rb') as fr:
            for i in range(4):
                m.update(fr.read(5))
                fr.seek(move_size, 1)
        return m.hexdigest()
​
    def upload(self, head_dic, conn):
        recv_size = 0
        file_size = head_dic.get('file_size')
        while recv_size < file_size:
            content = conn.recv(BUF_SIZE)
            recv_size += len(content)
            with open(head_dic.get('filename'), mode='ab+') as fw:
                fw.write(content)
        else:
            filename = head_dic.get('filename')
            file_size = head_dic.get('file_size')
            file_md5 = head_dic.get('file_md5')
            if self.check_file_md5(filename, file_size, file_md5):
                msg = f'upload file {filename} success!'
            else:
                os.remove(head_dic.get('file_path'))
                msg = f'failed to upload file {filename}!'
            msg_struct = struct.pack('i', len(msg.encode('utf-8')))
            conn.send(msg_struct)
            conn.send(msg.encode('utf-8'))
​
    def download(self, head_dic, conn):
        file_path = head_dic.get('file_path')
        file_size = os.path.getsize(file_path)
        filename = head_dic.get('filename')
        file_md5 = self.get_file_md5(file_path, file_size)
        head_dic = {'filename': filename, 'file_size': file_size, 'file_md5': file_md5}
        head_json = json.dumps(head_dic)
        head_bytes = bytes(head_json, encoding='utf-8')
        head_struct = struct.pack('i', len(head_bytes))
        conn.send(head_struct)
        conn.send(head_bytes)
​
        with open(file_path, mode='rb') as fr:
            for line in fr:
                conn.send(line)
​
    def communicate(self, conn, addr):
        print(f'收到客户端[{addr[0]}:{addr[1]}]的信息...')
        while 1:
            try:
                head_struct = conn.recv(4)
                if not head_struct: break
                head_len = struct.unpack('i', head_struct)[0]
                head_bytes = conn.recv(head_len)
                head_dic = json.loads(head_bytes.decode('utf-8'))
                if hasattr(self, head_dic.get('cmd')):
                    func = getattr(self, head_dic.get('cmd'))
                    func(head_dic, conn)
            except ConnectionResetError as e:
                sys.stderr.write(f'connect error:{e}')
        conn.close()
​
    def run(self):
        self.bind()
        self.listen()
        while 1:
            conn, addr = self.accept()
            t = Thread(target=self.communicate, args=(conn, addr))
            t.start()
​
​
if __name__ == '__main__':
    server = MYFTPServer(IP_ADDRESS)
    server.run()

上传功能

客户端1:

8eacd4b217f2107960207fe1aeee23e1.png

客户端2:

d14893337de3de3c38413977972dc86f.png

服务端:

d299d58d1ae04709679db444fa109aee.png

下载功能

客户端1:

25099958c504b10ebea6fe6de47391f4.png

客户端2:

c9149ab622fad1198cb63cb88c2d89b7.png

服务端:

ecc3676bafd32da17f6f7f74ab1aa3ee.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值