嘘~ 正在从服务器偷取页面 . . .

多线程FTP项目(3)—— socketserver版本多线程FTP项目


多线程FTP项目(3)—— socketserver版本多线程FTP项目

threading 版

​ 本来是想自己写一个实现多进程的 FTP 项目的,也就是说不使用 socketserver 模块实现多线程 FTP 项目,但是我写到一半调试的时候发现,虽然可以实现多用户同时登录,但是在输入命令之后,客户端很容易被 “远程计算机直接断开连接”。目前还是不清楚出了什么问题,不过看了 socketserver 模块源码后,发现该模块的多线程实现是比较复杂的,所以我觉得出现这个 bug 很大可能是因为我对于多线程的理解使用还不到家,所以还是使用 socketserver 模块实现多线程FTP项目。

项目开发目录


项目可实现功能

  • 用户注册
  • 多用户登录
  • 用户查看家目录
  • 用户切换家目录
  • 用户在家目录下创建其他文件夹
  • 用户从服务端下载文件,并且在下载过程中显示进度条
  • 实现断点续存,文件下载过程中中断连接,可以继续下载,并显示进度条

不足之处

  • 用户注册在服务端,其实是有问题的,从实际上来看,用户注册是用户做的事情,但是我们不可能在用户端注册用户然后将用户名和密码等信息传入服务端的文件中。目前能想到的解决方法是使用数据库,所以这样来看,FTP项目我大概到时候还要写一个含数据库的版本
  • 断点续存功能写完之后发现每次退出后都会删除未下载完的文件信息,所以这样好像一次只会存储一个未下载完成的文件信息。
  • 本来想写一个注销用户的功能,比如启动服务端时可以选择删除操作,在删除前需要输入管理员密码,但最后没有写;
  • 还有用户目录空间大小分配的功能,比如用户在上传文件时,判断分配的空间大小是否足够,因为没有写上传功能,所以也没有写判断空间大小的功能。
  • 暂时能想到的不足之处只有这么多,但实际上应该还有挺多不足之处的,所以大概率之后还会再写一个优化后的含数据库的FTP项目。

项目代码

FTPclient.py 文件

import socket
import optparse, struct, json, time, os
from optparse import OptionParser
from configparser import ConfigParser

STR_RECV_LENGTH = 8 # 接收信息头字典长度编码后的数据
MAX_RECV_LENGTH = 1024 # 接收文件时一次性接收的数据长度

INI_PATH = os.path.dirname(os.path.dirname(__file__)) + "\\conf\\download.ini" # 因为没有专门写客户端的settings.py 所以ini的路径就直接写在 FTPclient.py 文件内部

class MyClient(object):
    def __init__(self):
        self.username = None # 记录登录的用户名
        self.current_dir = None # 记录用户在服务端的当前目录

        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 实例化 socket

        # 启动 FTPclient.py 文件时,用于解析命令行选项
        opt = OptionParser()
        opt.add_option("-H", dest="HOST", help="FTP Server HOST")
        opt.add_option("-P", dest="PORT", type="int", help="FTP Serve PORT")

        values, args = opt.parse_args()

        self.connection(values) # 连接服务端

    def connection(self, values:optparse.Values):
        """和服务端连接"""
        # 服务器的端口
        HOST = values.HOST
        PORT = values.PORT

        if HOST and PORT: # 输入的 HOST 和 PORT 都不能为空
            self.client.connect((HOST, PORT))
            print("connect succeed")
            self.login() # 连接完成后,进行用户登录
        else:
            exit("ERROR: should supply HOST and PORT !") # 退出并打印提示

    def create_msg_to_send(self, action_type, **kwargs) -> dict:
        """制作信息头"""
        # 信息头字典一定要有 action_type 的数据,方便服务端识别命令类型
        dic = {
            "action_type": action_type
        }
        # 传入的 **kwargs 是键值形式,update 将 dic 和 **kwargs 整合成一个信息头字典
        dic.update(kwargs)

        return dic

    def send_msg(self, dic:dict) -> None:
        """发送信息"""
        # 字典转换成字符串
        dic_str = json.dumps(dic)
        # 对字符串传毒进行编码,编码后的编码长度恒为 8
        dic_str_length = struct.pack("q", len(dic_str))

        self.client.sendall(dic_str_length) # 发送编码
        self.client.sendall(dic_str.encode("utf-8")) # 发送字符串

    def recv_msg(self) -> dict:
        """接收信息"""
        # 解码获取字符串长度
        str_length = struct.unpack("q", self.client.recv(STR_RECV_LENGTH))[0]

        # 根据字符串长度选择不同的方式接收消息
        if str_length < MAX_RECV_LENGTH: # 没有超过最大接收长度
            dic_str = self.client.recv(str_length)

        else: # 字符串长度超过 1024
            recv_size = 0
            dic_str = ""
            while recv_size < str_length:
                data = self.client.recv(MAX_RECV_LENGTH)
                dic_str += data
                recv_size += len(data)

        dic = json.loads(dic_str) # 将字符串转化为字典,并返回字典

        return dic

    def login(self):
        """客户端登录"""
        num = 0
        while num < 3: # 用户可以尝试三次
            username = input("username >>:").strip() # 去除输入后左右两边的空格,防止因为某些原因用户一开始输入时输入多个空格
            if not username: # 用户名不能为空
                print("username can not be empty")
                continue

            password = input("password >>:").strip()
            if not password: # 密码不能为空
                print("password can not be empty")
                continue

            dic = self.create_msg_to_send(action_type="login", username=username, password=password) # 制作信息头
            self.send_msg(dic) # 发送信息头

            dic = self.recv_msg() # 接收服务端的反馈

            if dic.get("status_code") == 200: # 根据状态码判断用户是否登录成功,200 代表登陆成功
                print("{} {} login succeed !!! {}".format("-" * 25, username, "-" * 25))
                self.current_dir = dic.get("current_dir") # 记录用户在服务端的当前目录
                self.username = username # 登录成功,记录用户登录的用户名
                self.re_get() # 登录成功后,首先让用户选择是否需要对未完成的文件进行断点续存
                self.handle() # 和服务端进行交互
            else:
                status_msg = dic.get("status_msg") # 登录失败,打印服务端反馈的状态信息
                print("{} {} !!! {}".format("-" * 25, status_msg, "-" * 25))
                num += 1 # 尝试次数减少一次

    def handle(self):
        """和服务端交互"""
        while True:
            try:
                # 用户输入交互命令,输入时的提示信息是用户目前在客户端的当前目录,一样需要去除空格
                cmd = input("[{}]>>:".format(self.current_dir)).strip()
                if not cmd: # 输入命令的不能控
                    print("{} command should not be empty !!! {}".format("-" * 25, "-" * 25))
                    continue

                cmd_list = cmd.split(" ") # 对用户输入的内容进行按空格分割,分割之后的第一个词是交互命令名称
                if hasattr(self, cmd_list[0]): # 如果客户端存在该方法
                    func = getattr(self, cmd_list[0])
                    func(cmd_list) # 执行该方法,并传入分割后的命令列表作为参数
                else: # 用户输入的交互命令不存在
                    print("{} this command is not existed !!! {}".format("-" * 25, "-" * 25))
            except Exception as e:
                print(e) # 打印报错

    def parameter_num_judgment(self, parameter_length, Min_num = None, Max_num = None, Exact_num = None):
        """命令参数个数判断"""
        # 判断参数个数时候符合规范,可以输入最大参数个数,最小参数个数,准确的参数个数
        if Min_num and Min_num > parameter_length:
            print("{} the least number of parameter is {}, but you supply the {} parameter {}".format("-" * 25,  Min_num, parameter_length, "-" * 25))
            return False

        if Max_num and Max_num < parameter_length:
            print("{} the most number of parameter is {}, but you supply the {} parameter {}".format("-" * 25,  Max_num, parameter_length, "-" * 25))
            return False

        if Exact_num and Exact_num != parameter_length:
            print("{} the exact number of parameter is {}, but you supply the {} parameter {}".format("-" * 25,  Exact_num, parameter_length, "-" * 25))
            return False

        return True

    def dir(self, cmd_list:list):
        """查看在服务端的当前目录"""
        # 首先判断查看目录方法参数是否准确,因为是查看当前目录,所以我们除了命令名称 dir 外,其他参数都是无关紧要的,所以最小需要一个参数
        if self.parameter_num_judgment(parameter_length = len(cmd_list), Min_num = 1):
            # 制作信息头并发送
            dic = self.create_msg_to_send(action_type="dir")
            self.send_msg(dic)

            # 接收服务端的反馈
            dic = self.recv_msg()

            # 根据服务端的反馈输出查看目录的结果
            stdout = dic.get("stdout")
            stderr = dic.get("stderr")

            print(stdout, stderr)

    def cd(self, cmd_list:list):
        """切换目录"""
        # 切换目录最少需要两个参数,分别是 命令名称 cd 和目标目录
        if self.parameter_num_judgment(parameter_length=len(cmd_list), Min_num=2):
            # 制作信息头并发送
            dic = self.create_msg_to_send(action_type = "cd", path = cmd_list[1])
            self.send_msg(dic=dic)

            # 接收服务端的反馈,并打印状态信息
            dic = self.recv_msg()
            print(dic.get("status_msg"))

            if dic.get("status_code") == 300: # 如果切换目录成功,当前目录就变为切换后的目录
                self.current_dir = dic.get("current")

    def mkdir(self, cmd_lsit:list):
        """创造文件夹"""
        # 至少需要传入两个参数,命令名称和要创造的文件夹名称
        if self.parameter_num_judgment(parameter_length=len(cmd_lsit), Min_num=2):
            dic = self.create_msg_to_send(action_type="mkdir", dirname = cmd_lsit[1])
            self.send_msg(dic=dic)

            dic = self.recv_msg()
            # 打印服务端反馈的状态信息
            print(dic.get("status_msg"))

    def get(self, cmd_list:list):
        """从用户端下载文件"""
        # 至少两个参数,命令名称和需要下载的文件名称
        if self.parameter_num_judgment(parameter_length=len(cmd_list), Min_num=2):
            dic = self.create_msg_to_send(action_type="get", file_name = cmd_list[1])
            self.send_msg(dic=dic)

            # 当前时间,下载的文件在本地有同命名文件时,将作为后缀添加到下载后的文件中
            now = time.localtime()
            now_time = time.strftime("%Y年%m月%d日%H时%M分%S秒", now)

            # 打开记录未下载完成的文件信息的 ini 文件
            conf = ConfigParser()
            conf.read(INI_PATH)

            conf.add_section(cmd_list[1] + "." + now_time)

            dic = self.recv_msg()

            if dic.get("status_code") == 303: # 表示服务端文件存在,可以下载
                file_size = dic.get("file_size") # 要下载的文件总大小
                # 记录文件信息到 ini 中
                conf[cmd_list[1] + "." + now_time]["username"] = self.username # 此时登录的用户,因为一个客户端可以登录不同的用户,所以记录是什么用户未下载完成的文件,防止断点续存时出现 用户A 可以下载 用户B 的文件的情况
                conf[cmd_list[1] + "." + now_time]["total_size"] = str(file_size) # 下载文件总大小
                conf[cmd_list[1] + "." + now_time]["file_path"] = self.current_dir # 下载文件在服务端的路径
                f = open(INI_PATH, "w")
                conf.write(f) # 将记录的信息写入 ini 文件

                # 下载了的大小
                recv_size = 0

                bar = self.process_bar(total_size=file_size, recv_size=recv_size) # 实例化进度条,是一个迭代器
                bar.__next__()

                # 接收服务端传输的文件
                with open(cmd_list[1] + "." + now_time, "wb") as f:
                    while recv_size < file_size:
                        data = self.client.recv(MAX_RECV_LENGTH)
                        f.write(data)
                        recv_size += len(data)
                        bar.send(recv_size) # 进度条显示

                print("\n")

                if not os.path.exists(cmd_list[1]): # 如果没有相同命名的文件,就不添加时间
                    os.rename(cmd_list[1] + "." + now_time, cmd_list[1])
                    # os.remove(cmd_list[1] + "." + now_time)
                else:
                    now = time.localtime()
                    now_time_finally = time.strftime("%Y年%m月%d日%H时%M分%S秒", now)

                    os.rename(cmd_list[1] + "." + now_time, cmd_list[1] + "." +now_time_finally) # 存在相同命名的文件,添加时间作为后缀
                    # os.remove(cmd_list[1] + "." + now_time)

                # 因为文件下载成功,删除断点续存 ini 文件中的文件信息
                conf.remove_section(cmd_list[1] + "." + now_time)
                f = open(INI_PATH, "w")
                conf.write(f)
            else: # 要下载的文件在服务端中不存在,客户端打印服务端的反馈状态信息
                print(dic.get("status_msg"))

    def process_bar(self, total_size, recv_size):
        """进度条"""
        while True:
            current_size = yield recv_size
            if current_size / total_size <= 1:
                print("#" * int(current_size / total_size * 50), int(current_size / total_size * 100), "%", end="\r", flush=True) # 每次打印都会覆盖,end="\r" 表示每次打印都从首行开头打印,flush=True 表示打印时覆盖

    def re_get(self):
        """断点续传"""

        files = [] # 记录该用户未完成下载的文件文件名

        # 打开 ini 文件
        conf = ConfigParser()
        conf.read(INI_PATH)

        file_list = conf.sections()
        if len(file_list) != 0:
            for file in file_list:
                if conf[file]["username"] == self.username: # 判断在 ini 文件中哪些文件是登录用户的未完成下载的文件
                    files.append(file) # 文件添加到 files 列表中


        print("\n")

        try:
            while len(files) != 0: # 如果该用户有未下载完成的文件
                conf.read(INI_PATH)
                file_list = conf.sections()

                # 打印该用户未下载完成的文件信息,包括 文件名称,要下载的文件总大小,文件在服务端的位置
                for file in files:
                    index = 0
                    print("""index: {}          file_name: {}                         total_size: {}              current_path: {}""".format(index, file, conf[file]["total_size"], conf[file]["file_path"]))
                    index += 1

                # 如果用户需要继续下载,就输入前面打印的 index,否则输入 “quit” 退出,在退出时会删除 ini 文件中未完成下载的文件信息(退出就删除在写完之后才发现其实不合理,但先这样吧,毕竟以后还要再修改成含数据库的版本的)
                print("\n","If you want to continue downloading the file that was not downloaded last time, please enter the index of the file.")
                print(' Enter "quit" to exit the function. Unfinished files will be automatically deleted when you exit the function')

                cmd = input("[{}]>>:".format(self.current_dir)).strip()
                if not cmd: # 输入不能为空
                    print("{} command should not be empty !!! {}".format("-" * 25, "-" * 25))
                    continue

                if cmd.isdigit(): # 判断输入的是否是数字
                    """如果输入数字"""
                    if 0 <= int(cmd) < len(files): # 因为是索引,所以索引必须要大于0,而且要小于 files 的长度
                        file_name = files[int(cmd)]

                        recv_size = os.path.getsize(file_name) # 获取未完成下载的文件已经下载的大小

                        # 制作信息头并发送
                        dic = self.create_msg_to_send(action_type="re_get", file_name=file_name, total_size=conf[file_name]["total_size"], recv_size=recv_size, path=conf[file_name]["file_path"])
                        self.send_msg(dic=dic)

                        dic = self.recv_msg()
                        if dic.get("status_code") == 303: # 要下载的文件在服务端存在
                            self.re_download(file_name, conf[file_name]["total_size"], recv_size) # 继续下载
                        else: # 文件在服务端不存在
                            print(dic.get("status_msg"))

                        # 下载成功,移除在 ini 文件中的信息
                        conf.remove_section(files[int(cmd)])
                        f = open(INI_PATH, "w")
                        conf.write(f)

                elif cmd == "quit":
                    # 如果不想在退出时删除 ini 中未下载完成的文件信息,可以删除下面的 循环
                    for file in files:
                        conf.remove_section(file)
                        f = open(INI_PATH, "w")
                        conf.write(f)
                        # os.remove(file)
                    break
                else:
                    print("{} no this index(may you want to enter 'quit') {}".format("-"*25, "-"*25))
        except Exception as e:
            print(e)

    def re_download(self, file_name, total_size, recv_size):
        """断点续传的下载"""
        # 下载进度条
        bar = self.process_bar(total_size=int(total_size), recv_size=recv_size)
        bar.__next__()

        with open(file_name, "a") as f:
            while recv_size < int(total_size):
                data = self.client.recv(MAX_RECV_LENGTH)
                recv_size += len(data)
                bar.send(recv_size)

    def quit(self, cmd_list:list):
        """断开连接"""
        dic = self.create_msg_to_send(action_type="quit")
        self.send_msg(dic)
        self.client.close()
        exit("{} Quit succeed !!! {}".format("-"*25, "-"*25))

if __name__ == "__main__":
    client = MyClient()

FTPServer.py 文件

import os,sys

path = os.path.dirname(os.path.dirname(__file__)) # 这里的 path 是 FTPServer 文件夹
# 选择 os.path.dirname 有一个好处就是不用管 FTPServer 文件夹到底在哪个路径,不需要因为FTPServer位置更改而更改路径代码
# print(path)

sys.path.append(path) # 将目录添加到环境变量,这样就可以导入 lib 中的文件了

if __name__ == "__main__":
    from lib import Mangement

    server = Mangement.management(sys.argv)
    # sys.argv 解析命令行命令,比如说我们在命令行输入 python FTPServer.py start,那么此时的 sys.argv 就是 ["FTPServer", "start"] 这样一个列表
    # 将 sys.argv 传入Mangement文件中的management类,实例化该类

settings.py 文件

import os

# 服务端端口信息
HOST = "127.0.0.1"
PORT = 8080

# 存储用户信息的 ini 文件的路径
ACCOUNTS_INI = os.path.dirname(__file__) + "\\accounts.ini"

# 存放用户家目录的文件夹目录
HOME_PATH = os.path.dirname(os.path.dirname(__file__)) + "\\home"

# 服务端日志文件夹目录
LOG_PATH = os.path.dirname(os.path.dirname(__file__)) + "\\log"

Management.py 文件

import socketserver
from lib import main
from lib import Create
from conf import settings

class management(object):
    def __init__(self, sys_argv):
        sys_argv = sys_argv
        self.verify_argv(sys_argv)

    def verify_argv(self, sys_argv):
        """验证命令是否正确"""
        tips = """
        ---------------------------------------------
                    FTP Server command
        ---------------------------------------------
              start              -- 启动 FTP 服务端
        ---------------------------------------------
              create             -- 注册新用户
        ---------------------------------------------
              delete             -- 管理员注销用户
        ---------------------------------------------
        """
        command = sys_argv[1]
        if hasattr(self, command): # 如果该命令存在,就执行改命令
            func = getattr(self, command)
            func()

    def start(self):
        """启动服务端"""
        print("{}FTP Server start {} {}{}".format("-"*25, settings.HOST, settings.PORT, "-"*25))

        server = socketserver.ThreadingTCPServer((settings.HOST, settings.PORT), main.MyServer)
        server.serve_forever()

    def create(self):
        """创建用户"""
        create = Create.createUser()

    def delete(self):
        """删除用户"""
        pass

Create.py 文件

from configparser import ConfigParser
from conf import settings
import hashlib,os

class createUser(object):
    def __init__(self):
        # 打开 ini 文件,记录创建的用户名和密码信息
        self.conf = ConfigParser()
        self.conf.read(settings.ACCOUNTS_INI)
        self.create()

    def create(self):
        while True:
            username = input("the username of the user what you want to create >>:").strip()
            if not username: # 用户名不能为空
                print("the username cannot be empty")
                continue
            if not self.verify_username(username): # 判断该用户名是否已存在
                print("this username is existed")
                continue

            password = input("the password of the user you want to create >>:").strip()
            if not username: # 密码不能为空
                print("the password cannot be empty")
                continue

            self.create_user(username, password)
            break

    def verify_username(self, username):
        """检验用户名是否已存在"""
        username_list = self.conf.sections()

        if username in username_list:
            return False

        return True

    def create_user(self, username, password):
        """将用户名和密码写入配置文件"""
        self.conf.add_section(username)
        self.conf.set(username, "username", username)

        md5_password = self.md5_password(password) # 得到加密后的密码
        
        # 用户名和密加密后的密码写入 ini 文件
        self.conf.set(username, "password", md5_password)

        f = open(settings.ACCOUNTS_INI, "w")

        self.conf.write(f)

        print("{} {} create succeed !!! {}".format("-"*25, username, "-"*25))

        self.create_dir(username)

    def md5_password(self, password):
        """加密密码并写入配置文件"""
        md5_ = hashlib.md5()
        md5_.update(password.encode("utf-8"))
        md5_password = md5_.digest()
        # print(md5_password)

        return str(md5_password)

    def create_dir(self, username):
        """创建用户时,创建用户家目录"""
        os.mkdir(settings.HOME_PATH + "\\" + username)

main.py 文件

import socketserver, json, struct,hashlib, subprocess, os,time
from configparser import ConfigParser
from conf import settings

STR_RECV_LENGTH = 8
MAX_RECV_LENGTH = 8096

STATUS = {
    200: "Login succeed",
    201: "Error: wrong password or wrong username",
    202: "Check the current dir",
    300: "{} cd the path done {}".format("-"*25, "-"*25),
    301: "{} the path is not existed {}".format("-"*25, "-"*25),
    302: "{} dir create succeed {}".format("-"*25, "-"*25),
    303: "{} the file is existed {}".format("-"*25, "-"*25),
    304: "{} the file is not existed {}".format("-"*25, "-"*25)
} # 状态码,反馈给客户端,帮助客户端判断服务端的执行命令的结果

class MyServer(socketserver.BaseRequestHandler):
    def handle(self) -> None:
        self.client_name = None
        self.HOME_PATH = None
        self.current_path = None
        self.show_to_client_path = None

        print(str(self.client_address), "connect succeed") # 客户端连接成功
        self.write_log(msg = "connect succeed")

        while True: # 通信循环
            try:
                dic = self.recv_msg()
                if not dic:
                    break

                action_type = dic.get("action_type") # 判断客户端的命令类型

                if hasattr(self, "_%s" % action_type): # 如果存在该命令,就执行,并将客户端发送的 dic 作为参数传入执行命令
                    func = getattr(self, "_%s" % action_type)
                    func(dic)

            except Exception as e:
                print(e)
                break

    def create_msg_to_send(self, status_code, **kwargs) -> dict:
        """制作信息头"""
        # 和客户端基本一致
        dic = {
            "status_code": status_code,
            "status_msg": STATUS.get(status_code)
        }

        dic.update(kwargs)

        return dic

    def send_msg(self, dic: dict) -> None:
        """发送信息"""
        # 和客户端基本一致
        dic_str = json.dumps(dic)
        dic_str_length = struct.pack("q", len(dic_str))

        self.request.sendall(dic_str_length)
        self.request.sendall(dic_str.encode("utf-8"))

    def recv_msg(self) -> dict:
        """接收信息"""
        # 和客户端基本一致
        pack_length = self.request.recv(STR_RECV_LENGTH)

        if not pack_length:
            dic = {}
            return dic

        str_length = struct.unpack("q", pack_length)[0]
        if str_length < MAX_RECV_LENGTH:  # 没有超过最大接收长度
            dic_str = self.request.recv(str_length)

        else:
            recv_size = 0
            dic_str = ""
            while recv_size < str_length:
                data = self.request.recv(MAX_RECV_LENGTH)
                dic_str += data
                recv_size += len(data)

        dic = json.loads(dic_str)

        return dic

    def _login(self, dic:dict):
        """用户登录验证"""
        # 获取客户端传入的用户名和密码
        username = dic.get("username")
        password = dic.get("password")

        # 将客户端传入的密码进行加密并且和 ini 文件记录的加密密码进行匹配
        md5 = hashlib.md5()
        md5.update(password.encode("utf-8"))
        md5_password = md5.digest()

        conf = ConfigParser()
        conf.read(settings.ACCOUNTS_INI)

        if username in conf.sections(): # 该用户已注册,表示可以登录(这里写的多用户登录并不能防止一个用户同时在线,这也是需要修改的地方)
            password = conf[username]["password"]

            if str(password) == str(md5_password): # 密码正确
                print("{} login succeed, username: {}".format(self.client_address, username))
                self.write_log(msg = "login succeed, username: {}".format(username))

                self.client_name = username # 记录此时登录的用户名

                self.HOME_PATH = settings.HOME_PATH + "\\" + username # 登录的用户家目录
                self.current_path = self.HOME_PATH # 登录的用户当前在服务端的路径位置
                self.show_to_client_path = self.current_path.replace(settings.HOME_PATH, "") # 发送给用户的在服务端的位置,从 home 以后发送,比如说登录 用户为 aoteman,发送给客户端的目录为 “aoteman” 而不包括前面的路径
                
                # 制作信息头并发送
                dic = self.create_msg_to_send(status_code=200, current_dir = self.show_to_client_path)
                self.send_msg(dic=dic)

                self.write_log(msg="Error: wrong password or wrong username")
            else:# 登录失败,密码不正确
                dic = self.create_msg_to_send(status_code=201)
                self.send_msg(dic=dic)

                self.write_log("Login succeed")
        else: # 登陆失败,登录用户未注册,即用户不存在
            dic = self.create_msg_to_send(status_code=201)
            self.send_msg(dic=dic)

            self.write_log("")

    def _dir(self, dic:dict):
        """查看用户当前目录"""
        # 执行终端命令,并储存运行结果
        obj = subprocess.Popen("cd {} & dir /s/b".format(self.current_path),
                               shell = True,
                               stdout = subprocess.PIPE,
                               stderr = subprocess.PIPE)

        stdout = obj.stdout.read()
        stderr = obj.stderr.read()
        
        # 制作信息头并发送
        dic = self.create_msg_to_send(status_code=202, stdout=stdout.decode("gbk"), stderr=stderr.decode("gbk"))
        self.send_msg(dic)

        print("{} {} check the current dir {}".format("-"*25, self.client_name, "-"*25))

        self.write_log(msg = "check the current dir")

    def _cd(self, dic:dict):
        """切换用户目录"""
        full_path = os.path.abspath(self.current_path + "\\" + dic.get("path"))

        if self.HOME_PATH in full_path and os.path.exists(full_path):
            """判断切换的路径存在并且需要保证用户切换的路径不会是用户家目录的父目录"""
            self.current_path = full_path
            self.show_to_client_path = self.current_path.replace(settings.HOME_PATH, "")

            subprocess.Popen("cd %s" % self.current_path, shell=True)

            dic = self.create_msg_to_send(status_code=300, current=self.show_to_client_path)
            self.send_msg(dic)

            print("{} {} cd the dir {} {}".format("-" * 25, self.client_name, self.current_path,"-" * 25))

        else:
            dic = self.create_msg_to_send(status_code=301)
            self.send_msg(dic)

            print("{} {} cd the dir {} , but this path is not existed {}".format("-" * 25, self.client_name, self.current_path,"-" * 25))

            self.write_log(msg = "cd the dir {} , but this path is not existed ".format(self.current_path))

    def _mkdir(self, dic:dict):
        """制作文件夹"""
        # 允许用户在自己的家目录下新建新的文件夹
        dir_name = dic.get("dirname")

        os.mkdir(self.current_path + "\\" + dir_name)

        dic = self.create_msg_to_send(status_code=302)
        self.send_msg(dic)

        print("{} {} create the dir {} {}".format("-" * 25, self.client_name, self.current_path + "\\" + dir_name, "-" * 25))

        self.write_log(msg = "create the dir {}".format(self.current_path))

    def _get(self, dic:dict):
        """用户下载文件"""
        file_name = dic.get("file_name") # 用户端想要下载的文件
        file_path = self.current_path + "\\" + file_name # 拼接文件路径,查看服务端是否存在该文件

        if os.path.isfile(file_path): # 如果文件存在
            file_size = os.path.getsize(file_path) # 获取文件总大小
            
             # 制作信息头并发送
            dic = self.create_msg_to_send(status_code=303, file_size=file_size)
            self.send_msg(dic)
            
            # 打开文件发送给客户端,因为网络通信传输的数据类型是二进制数据,所以 "rb" 打开文件
            with open(file_path, "rb") as f:
                for i in f:
                    self.request.send(i)

            print("{} {} get done {}".format("-"*25, file_name, "-"*25))

            self.write_log(msg = "{} get done".format(file_name))

        else: # 要下载的文件不在服务端(或者不在服务端当前目录)
            dic = self.create_msg_to_send(status_code=304)
            self.send_msg(dic)

    def _re_get(self, dic:dict):
        """断点续传"""
        # print(dic)

        file_name_list = dic.get("file_name").split(".") 
        file_name_list.pop() # 去除时间后缀
        file_name = ".".join(file_name_list) # 用户想要继续下载的文件名称

        path = settings.HOME_PATH + dic.get("path") + "\\" + file_name # 文件在服务端的路径(其实这里有一个问题,应该还要判断文件在服务端是否存在)

        total_size = int(dic.get("total_size")) # 客户端记录的文件总大小
        recv_size = dic.get("recv_size")

        size = os.path.getsize(path) # 获取服务端文件的总大小
        if total_size == size: # 总大小进行对并,避免出现名称一致,但是大小不一致的情况(比如说文件在服务端被更新,这时候继续下载毫无意义)
            dic = self.create_msg_to_send(status_code=303)
            self.send_msg(dic)

            with open(path, "rb") as f:
                f.seek(recv_size)
                for i in f:
                    self.request.send(i)

            print("{} {} re-get done {}".format("-" * 25, file_name, "-" * 25))

            self.write_log(msg="{} re-get done".format(file_name))

        else: # 服务端的文件和客户端想要继续下载的文件总大小并不一致
            dic = self.create_msg_to_send(status_code=304)
            self.send_msg(dic)

    def _quit(self, dic:dict):
        """断开连接"""
        print("{} close".format(self.client_address))
        self.write_log(msg = "close")
        self.request.close()

    def write_log(self, msg):
        """日志制作"""
        with open(settings.LOG_PATH + "//" + str(self.client_address) + ".txt", "a") as f:
            now = time.localtime()
            now_time = time.strftime("%Y年%m月%d日%H时%M分%S秒", now)
            f.write("{} {} {} {} \t{}".format("-"*25, self.client_name, msg, "-"*25, now_time) + "\n")

演示结果


文章作者: New Ass
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 New Ass !
  目录