Python网络编程 03 实验:FTP详解

文章目录

一、小实验FTP程序需求

(1)用户加密认证

(2)允许同时多用户登录

(3)每个用户有自己的家目录,且只能访问自己的家目录

(4)对用户进行磁盘配额,每个用户的可用空间不同

(5)允许用户在FTP server上随意切换目录

(6)允许用户查看当前目录下文件

(7)允许上传和下载文件,保证文件一致性

(8)文件传输过程中显示进度条

(9)附加功能:支持文件的断点续传

二、项目文件架构

三、服务端

1、conf/settings.py

python 复制代码
import os
BASE_DIE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

IP = "127.0.0.1"
PORT = 8000

ACCOUNT_PATH = os.path.join(BASE_DIE, "conf", "accounts.cfg")

2、conf/accounts.cgf

python 复制代码
[DEFAULT]

[LuMX]
Password = 123
Quotation = 100

[root]
Password = root
Quotation = 100

3、conf/STATUS_CODE.py

python 复制代码
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"
}

4、启动文件 bin/ftp_server.py

python 复制代码
import os, sys

# 获取启动模块所在目录的父级目录路径,
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 将该目录路径添加到导入模块时要搜索的目录列表
sys.path.append(PATH)

from core import main


if __name__ == "__main__":
    main.ArgvHandler()	# 启动主函数

5、core/main.py

python 复制代码
import optparse
import socketserver
from conf.settings import *
from core import server

class ArgvHandler:
    def __init__(self):
        self.op = optparse.OptionParser()
        options, args = self.op.parse_args()

        self.verify_args(args)

    def verify_args(self,args):
        cmd = args[0]

        if hasattr(self, cmd):
            # 方便扩展,例如可以在终端执行 python ftp_server.py start
            func = getattr(self,cmd)
            func()

    def start(self):
        print("the server is working...")
        s = socketserver.ThreadingTCPServer((IP, PORT), server.ServerHandler)
        s.serve_forever()

6、core/server.py

python 复制代码
import json,configparser
import os,hashlib

from conf.STATUS_CODE import *
from conf import settings
import socketserver
class ServerHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            data = self.request.recv(1024).strip()
            if not data:
                print("the connect is breaked")
                break
            data = json.loads(data.decode("utf-8"))

            if data.get("action"):  # 字典data存在key为action,且不为空,执行下列代码
                func = getattr(self, data.get("action"))
                func(**data)
            else:
                print("Invalid cmd")

    def send_response(self, state_code):
        response = {"status_code": state_code}
        self.request.sendall(json.dumps(response).encode("utf-8"))

    def auth(self,**data):
        username = data["username"]
        password = data["password"]
        user = self.authenticate(username, password)

        if user:
            self.send_response(254)
        else:
            self.send_response(253)

    def authenticate(self,user,pwd):
        cfg = configparser.ConfigParser()
        cfg.read(settings.ACCOUNT_PATH)

        if user in cfg.sections():
            if cfg[user]["Password"]==pwd:
                self.user = user
                self.mainPath = os.path.join(settings.BASE_DIR,"home",self.user)
                print("passed authentication")
                return user

    def put(self,**data):
        print("data",data)
        file_name = data.get("file_name")
        file_size = data.get("file_size")
        target_path = data.get("target_path")

        abs_path = os.path.join(self.mainPath,target_path,file_name)

        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,"ab")
                else:
                    f=open(abs_path,"wb")
            else:
                # 文件存在,且完整
                self.request.sendall("801".encode("utf-8"))
                return
        else:
            # 文件不存在
            self.request.sendall("802".encode("utf-8"))
            f = open(abs_path,"wb")

        md5_obj = hashlib.md5()
        while has_received < file_size:
            md5_data = self.request.recv(32)
            f_data = self.request.recv(992)

            if self.md5_check(md5_obj,md5_data,f_data):
                self.send_response(900)
                f.write(f_data)
                has_received += len(f_data)
            else:
                print("md5 check error")
                self.send_response(901)
                break

        f.close()

    def md5_check(self,md5_obj,md5_data,f_data):
        md5_obj.update(f_data)

        if md5_obj.hexdigest().encode("utf-8") == md5_data:
            return True

    def ls(self, **data):
        file_list = os.listdir(self.mainPath)
        file_str="\n".join(file_list)

        if not len(file_list):
            file_str="<empty dir>"
        self.request.sendall(file_str.encode("utf-8"))

    def cd(self,**data):
        dirname=data.get("dirname")

        if dirname == "..":
            self.mainPath=os.path.dirname(self.mainPath)
        else:
            self.mainPath=os.path.join(self.mainPath,dirname)
        self.request.sendall(self.mainPath.encode("utf-8"))

    def mkdir(self,**data):
        dirname=data.get("dirname")
        path=os.path.join(self.mainPath,dirname)

        if not os.path.exists(path):
            if "/" in dirname:
                os.makedirs(path)
            else:
                os.mkdir(path)
            self.request.sendall("create success".encode("utf-8"))
        else:
            self.request.sendall("dirname exist".encode("utf-8"))

四、客户端

1、conf/STATUS_CODE.py

python 复制代码
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"
}

2、bin/ftp_client.py

python 复制代码
import os, sys, hashlib
import optparse,socket,json

PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PATH)

from conf.STATUS_CODE import STATUS_CODE

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()
        self.verify_args(self.options)
        self.make_connection()
        self.mainPath=os.path.dirname(os.path.abspath(__file__))

    def verify_args(self,options):
        '''
        验证端口号是否合法
        '''
        port = options.port

        if 0 <= int(port) <= 65535:
            return True
        else:
            exit("the port is not in 0-65535")

    def make_connection(self):
        '''
        处理链接
        '''
        self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        self.sock.connect((self.options.server, int(self.options.port)))

    def interactive(self):
        '''
        处理通讯
        '''
        print("begin to interactive...")

        if self.authenticate():
            while True:
                cmd_info = input("[%s]" %self.current_dir).strip()

                cmd_list = cmd_info.split()
                if hasattr(self,cmd_list[0]):
                    func=getattr(self,cmd_list[0])
                    func(*cmd_list)

    def put(self,*cmd_list):
        action,local_path,target_path=cmd_list
        local_path=os.path.join(self.mainPath,local_path)

        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.sock.send(json.dumps(data).encode("utf-8"))
        is_exist=self.sock.recv(1024).decode("utf-8")

        has_sent=0

        if is_exist=="800":
            # 文件不完整
            choice=input("the file exist,but not enough,do continue?[Y/N]").strip()
            if choice.upper()=="Y":
                self.sock.sendall("Y".encode("utf-8"))
                continue_position=self.sock.recv(1024).decode("utf-8")
                has_sent += int(continue_position)
            else:
                self.sock.sendall("N".encode("utf-8"))
        elif is_exist=="801":
            # 文件完全存在
            print("the file exist")
            return

        f = open(local_path,"rb")
        f.seek(has_sent)
        md5_obj = hashlib.md5()
        while has_sent < file_size:
            f_data = f.read(992)

            if self.md5_check(md5_obj,f_data):
                has_sent += len(f_data)
                self.show_progress(has_sent,file_size)
            else:
                print("md5 check is error!")
                break

        f.close()

        if has_sent == file_size:
            print("\n",end="")
            print("put success!")

    def md5_check(self,md5_obj,f_data):
        md5_obj.update(f_data)
        md5_data = md5_obj.hexdigest().encode("utf-8")
        data = md5_data + f_data
        self.sock.sendall(data)
        response = self.response()

        if response["status_code"] == 900:
            return True

    def show_progress(self,has,total):
        '''
        显示进度条
        '''
        rate=int(float(has)/float(total)*10000)/10000
        rate_num=int(rate*100)
        sys.stdout.write("{:.2%} {}\r".format(rate,rate_num*"#"))
        # \r 表示光标移动到行首

    def ls(self,*cmd_list):
        data={
            "action": "ls"
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))
        data=self.sock.recv(1024).decode("utf-8")
        print(data)

    def cd(self,*cmd_list):
        data={
            "action": "cd",
            "dirname": cmd_list[1]
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))

        data = self.sock.recv(1024).decode("utf-8")
        print(os.path.basename(data))
        self.current_dir=os.path.basename(data)

    def mkdir(self,*cmd_list):
        data={
            "action": "mkdir",
            "dirname": cmd_list[1]
        }
        self.sock.sendall(json.dumps(data).encode("utf-8"))
        data = self.sock.recv(1024).decode("utf-8")
        print(data)

    def authenticate(self):
        if self.options.username is None or self.options.password is None:
            username = input("username:")
            password = input("password:")
            return self.get_auth_result(username,password)

        return self.get_auth_result(self.options.username, self.options.password)

    def response(self):
        data = self.sock.recv(1024).decode("utf-8")
        data = json.loads(data)

        return data
    def get_auth_result(self,user,pwd):
        data = {
            "action":"auth",
            "username":user,
            "password":pwd
        }

        self.sock.send(json.dumps(data).encode("utf-8"))
        response=self.response()
        print("response:",response["status_code"])
        if response["status_code"]==254:
            self.user = user
            self.current_dir=user
            print(STATUS_CODE[254])
            return True
        else:
            print(STATUS_CODE[response["status_code"]])

ch = ClientHandler()
ch.interactive()

五、在终端操作示例

服务端

客户端

相关推荐
青松@FasterAI3 分钟前
【动手学大模型开发】VSCode 连接远程服务器
服务器·ide·vscode
Hy行者勇哥8 分钟前
HTTP, AMQP, MQTT之间的区别和联系是什么?华为云如何适配?
网络·网络协议·http·华为云
东木月8 分钟前
Python解析地址中省市区街道
开发语言·python
Linux运维老纪16 分钟前
交换机之配置系统基本信息(Basic Information of the Configuration System for Switches)
linux·网络·mysql·华为·云计算·运维开发
意.远22 分钟前
PyTorch卷积层填充(Padding)与步幅(Stride)详解及代码示例
人工智能·pytorch·python·深度学习
努力学习的小廉1 小时前
深度理解linux系统—— 了解操作系统
linux·运维·服务器
玩电脑的辣条哥1 小时前
一台服务器已经有个python3.11版本了,如何手动安装 Python 3.10,两个版本共存
服务器·python·python3.11
敲上瘾1 小时前
基于Tcp协议的应用层协议定制
linux·运维·服务器·网络·c++·网络协议·tcp/ip
weixin_307779131 小时前
PySpark实现ABC_manage_channel逻辑
开发语言·python·spark
莹莹学编程—成长记2 小时前
string的模拟实现
服务器·c++·算法