网络编程学习(7)/ FTP项目(1) —— 项目需求梳理以及基本框架搭建

148 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

项目需求梳理

1. 用户登录认证(目前只实现一次连接一个客户端,即单线程)
    1.1 用户密码 md5 加密
2. 查看目录
    2.1 ls 命令 表示查看用户在服务端的目录,默认是家目录
    2.2 dir 命令 表示用户查看在客户端的目录
3. 切换目录
    3.1 用户使用 cd 命令切换服务端目录
        3.1.1 实际上在服务端并没有真正切换目录
        3.1.2 而且服务端代码编写时要注意,对用户来说,家目录就是最高一级,即对用户而言,home/username
              是可以达到的路径,但 server/home 目录对用户来说是不存在路径
        3.1.3 在切换路径后,客户端要记录用户此时切换到的路径,为接下来的 断点续存 功能做准备,不然会出现客
              户端重新连接继续下载时找不到要下载文件在服务端路径的情况
4. 下载/上传/断点续存 进度条 创建
5. 下载功能
    5.1 下载完成前,文件命名为 “filename.download”
        5.1.1 在创建 “filename.download” 文件前判断该文件是否存在,如果存在,就在 download 后面再
              加时间戳后缀
        5.1.2 若 FTP 在文件下载完成前崩溃导致文件未下载完成,创建 shelvel 对象,记录 文件名,未下载完
              成文件在服务端的路径,已下载的大小,为 断点续存 功能编写做准备
        5.1.3 文件下载完成后,将后缀部分去除,如果去除后缀后文件已存在,就增加时间戳后缀,即“filename.时
              间戳”
6. 上传功能(基本和下载功能一致)
    6.1 下载完成前,文件命名为 “filename.upload”
        6.1.1 在创建 “filename.upload” 文件前判断该文件是否存在,如果存在,就在 upload 后面再
              加时间戳后缀
        6.1.2 若 FTP 在文件上传完成前崩溃导致文件未上传完成,创建 shelvel 对象,记录 文件名,未上传完
              成文件在服务端的路径,已下载的大小,为 断点续存 功能编写做准备
        6.1.3 文件下载完成后,将后缀部分去除,如果去除后缀后文件已存在,就增加时间戳后缀,即“filename.时
              间戳”
 7. 下载/上传 断点续存功能
    7.1 在用户登陆成功后,打印未下载完成、未上传完成的文件名,供用户选择是否要继续上传或下载
    7.2 在用户选择继续下载或上传后,仍出现进度条并且从上次断的百分比开始增加
    7.3 文件命名仍和下载/上传功能中文件命名一致`

项目基本框架搭建

项目文件说明

-- FTP (项目名称)
    -- client 客户端
    -- server 服务端
        -- bin (存放可执行的二进制文件)
        -- conf (存放配置文件)
            -- accounts.ini (包括用户名称和密码,在此文件中的密码使用 md5 加密,非明文显示)
            -- setting.py (包括服务端接口、用户家目录等内容)
        -- home(存放用户家目录,比如 home\aoteman 是 用户 aoteman 的家目录)
        -- lib (存放的是函数调用的信息,在windows操作系统中起到链接程序和函数的作用。其意义在于代码重用,程
                序员将常用的功能写成函数,保存为lib文件,在以后编程要实现这些功能的时候,就不需要再重新编写代
                码,而是直接调用写好的lib文件,这很大程度上减轻程序员的负担。)
        -- log (项目日志)

请添加图片描述

bin 文件夹下 FTPServer.py 文件

# coding=gbk

import os,sys

# 因为要调用 server 文件夹下的其他 python 文件,所以程序。路径应为 \..\..\server
# 先返回 FTPServer.py 文件的绝对路径,在往上两级得到 server 文件夹的绝对路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# print(BASE_DIR)

sys.path.append(BASE_DIR)

if __name__ == '__main__':
    # 从 lib 文件夹下 导入 management.py 文件
    from lib import management

    # sys.argv 可以简单的理解为 将 终端命令 作为参数传入, sys.argv 的结果是列表
    # 比如说在终端输入 python server\bin\FTPServer.py start 运行,那么此时的 sys.argv 就是 ['server\\bin\\FTPServer.py', 'start']
    FTP = management.FTPManagement(sys.argv)
    FTP.execute()

lib 文件夹下 management.py

# coding=gbk

from lib import main

class FTPManagement(object):
    def __init__(self, sys_argv):
        self.sys_argv = sys_argv # 终端命令运行时传入的参数
        # print(self.sys_argv)

    def help_msg(self):
        """
        帮助信息,为用户打印提示信息
        """
        help_msg = """
        start   start up server
        """

        # 退出并打印
        exit(help_msg)

    def parameter_verification(self):
        """
        验证参数合法性
            1. 项目启动时的命令设置为 python 文件路径 命令(如 start)
                1.1 所以如果参数长度小于 2,应该向用户提示服务端可以接受的,命令
                1.2 只需要判断长度是否小于2,如果大于2,程序只把 sys.argv 列表中的第二位参数视为命令
        """
        if len(self.sys_argv) < 2:
            self.help_msg()

    def execute(self):
        """解析指令"""
        self.parameter_verification() # 如果未通过参数验证,退出并打印 帮助信息,不会再运行下面的代码
        parameter = self.sys_argv[1] # 取出代表命令的参数
        # print(parameter)

        # 判断 FTPManagement类 中是否有与命令参数相同的函数
        # 即如果命令是 start, 那么需要判断 FTPManagement类 中是否有 self.start 函数
            # 如果有此函数,调用函数
            # 如果没有此函数,打印 提示信息
        if hasattr(self, parameter): # FTPManagement类 中有 self.start 函数
            func = getattr(self, parameter) # func 赋值为 self.start 函数
            func() # 运行 self.start 函数
        else:
            print(" command does not exist !")
            print(" start   start up server !")

    def start(self):
        # print("------------")
        Server = main.Server(self)
        Server.run_forever()

lib 文件夹下 main.py

# coding=gbk
import socket

from conf import setting

class Server(object):
    MSG_SIZE = 8192  # 一次性接收的信息长度

    # 状态码
    STATUS_CODE = {

    }

    def __init__(self, management):
        self.management = management

        self.user_home = setting.USER_HOME_DIR # 记录用户家目录
        self.current_dir = self.user_home # 记录用户当前目录,用户切换目录时使用
        self.shelve_obj = None # 记录下载/上传未完成的文件信息

        # 建立服务端
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
        self.sock.bind((setting.HOST, setting.PORT)) # 装上手机卡
        self.sock.listen(5) # 开机,最大监听数为 5

    def run_forever(self):
        """正式启动服务端"""
        print(("FTP Server start %s %s " % (setting.HOST, setting.PORT)).center(50, "-"))

        while True:
            self.conn, self.addr = self.sock.accept()  # 等待电话
            print(self.addr)

            print("Connection from %s" % (self.addr, )) # self.addr 是元组,所以选择 (self.addr, )的形式

            try: # 防止报错而服务端退出程序
                self.handle() # 处理客户端和服务端和交互问题
            except Exception as e:
                print(e)

    def handle(self):
        pass

conf 文件夹下 setting.py 文件

# coding=gbk
import os.path

# 服务器端口
HOST = "0.0.0.0"
PORT = 9999

# 用户家目录
# 先切换到 server 目录,在切换到 home 目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
USER_HOME_DIR = os.path.join(BASE_DIR, "home")

client 文件夹下 FTPClient.py 文件

# coding=gbk
import optparse
import shelve
import socket

class Client(object):
    MSG_SIZE = 8192 # 一次性接收的信息长度

    def __init__(self):
        self.show_to_client = None # 输入命令时显示给用户的信息,如 【\username】>>:
        self.username = None # 记录用户名称
        self.current_dir = None # 记录用户下载文件时,文件在客户端的路径
        self.shelve_obj = shelve.open("FTP_Download") # 记录未下载完成的文件信息
        self.shelve_obj = shelve.open("FTP_Upload") # 记录未上传完成的文件信息

        # 终端命令参数
        # 输入 python client/FTPClient.py -h 得到提示信息
        # 比如输入 python client/FTPClient.py -H 123 -P 12456 返回的结果是 self.option = {'serverHost': '123', 'port': 12456}, self.args = []
        # 比如输入 python client/FTPClient.py 123   456   789, 返回的结果是 self.option = {'serverHost': None, 'port': None}, self.args = ['123', '456', '789']
        parser = optparse.OptionParser()
        parser.add_option("-H", "--serverHost", dest="serverHost", help="ftp server ip_addr")
        parser.add_option("-P", "--port", type="int", dest="port", help="ftp server port")
        self.options, self.args = parser.parse_args()  # 取出参数
        # print(self.options, self.args)

        self.parameter_test()
        self.make_connection()

    def parameter_test(self):
        """参数检验,保证用户输入了 -H 和 -P 内容,不然客户端无法连接服务端端口"""
        if not self.options.serverHost or not self.options.port: # 其中一项为空
            exit("Error: must supply serverHost and port !")

    def make_connection(self):
        """客户端连接"""
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买手机
        self.client.connect((self.options.serverHost, self.options.port)) # 打电话
        print("Connection successful !")

if __name__ == '__main__':
    client = Client()

当前运行结果(终端运行)

服务端

请添加图片描述

客户端

请添加图片描述