zoukankan      html  css  js  c++  java
  • ftp上传下载

    一、需求

    1. 用户加密认证 (完成)
    2. 允许同时多用户登录 (完成)
    3. 每个用户有自己的家目录 ,且只能访问自己的家目录(完成)
    4. 对用户进行磁盘配额,每个用户的可用空间不同(完成)
    5. 允许用户在ftp server上随意切换目录cd(完成)
    6. 允许用户查看当前目录下文件ls(完成)
    7. 允许上传put和下载get文件(完成),保证文件一致性(此需求不做)
    8. 文件传输过程中显示进度条(完成)
    附加功能:

     1.新建目录mkdir(完成)

     2.查看当前工作目录的路径pwd(完成)

     3.支持文件的断点续传(未完成)

    二、程序目录结构

    客户端:

    服务端:

     

    三、README

    重要!

    一、需求
    
    1. 用户加密认证 (完成)
    2. 允许同时多用户登录 (完成)
    3. 每个用户有自己的家目录 ,且只能访问自己的家目录(完成)
    4. 对用户进行磁盘配额,每个用户的可用空间不同(完成)
    5. 允许用户在ftp server上随意切换目录cd(完成)
    6. 允许用户查看当前目录下文件ls(完成)
    7. 允许上传put和下载get文件(完成),保证文件一致性(此需求不做)
    8. 文件传输过程中显示进度条(完成)
    附加功能:
    
     1.新建目录mkdir(完成)
    
     2.查看当前工作目录的路径pwd(完成)
    
     3.支持文件的断点续传(未完成)
    
    
    
    二、目录结构及模块功能解释
    
    ftp_client
    
      |----bin(可执行目录)
    
      |         |----__init__.py
    
      |         |----ftp_client.py(客户端接口)   
    
      |----conf(配置文件目录)
    
      |     |----__init__.py
    
      |     |----settings.py(配置文件) 
    
      |----core(核心代码)
    
      |     |----__init__.py
    
      |     |----auth.py(客户端身份验证)
    
      |     |----cd.py(实现客户端在服务随意切换目录的功能,但只能访问自己的家目录)
    
      |     |----get.py(客户端下载功能)
    
      |     |----interactive.py(用于客户端与服务端的交互/反射)
    
      |     |----ls.py(查看当前目录下的文件(包括目录))
    
      |     |----main.py(主函数,运行被ftp_client.py客户端接口调用)
    
      |   |----mkdir.py(实现用户在当前目录下可创建目录的功能)
    
      |   |----progress_bar.py(进度条:用于显示上传与下载的进度)
    
      |   |----put.py(处理客户端上传功能)
    
      |   |----pwd.py(查看用户当前的目录)
    
      |----__init__.py
    
    
    
    
    ftp_server
    
      |----bin
    
      |     |----__init__.py
    
      |     |----ftp_server.py(服务端接口)
    
      |----core
    
      |     |----__init__.py
    
      |     |----auth.py(用户加密认证,登陆模块)
    
      |     |----db_handle.py(读用户数据与写用户数据--感觉这个模块有点多余~)
    
      |     |----deal_cd.py(处理用户切换目录的功能)
    
      |     |----deal_get.py(处理客户端下载文件的请求)
    
      |     |----deal_ls.py(完成用户显示当前目录下文件(包括目录)的请求)
    
      |     |----deal_mkdir.py(处理用户在当前目录(家目录下)创建目录的请求)
    
      |     |----deal_put.py(处理客户端上传文件的请求)
    
      |     |----deal_pwd.py(用来处理客户端查看当前目录下的请求)
    
      |     |----get_dirisize.py(获取用户家目录的大小(字节))
    
      |     |----main.py(主函数--运行时被ftp_server.py服务端接口调用)
    
      |----data(用户数据库)
    
      |     |----__init__.py
    
      |     |----Alex.json(Alex用户的数据库)
    
      |     |----zcl.json(zcl用户的数据库)
    
      |----home(home目录,用来存放各用户的家目录)
    
      |     |----Alex(Alex的家目录)
    
      |     |----zcl(zcl的家目录)
    
      |     |----__init__.py
    
      |----log(日志--未拓展)
    
      |     |----__init__.py
    
      |----__init__.py
    
    
    
    三、状态码
    
    LOGIN_STATE = {
        "auth_True":"200",   #认证成功
        "auth_False":"400",  #认证失败
        "file_exit":"202",   #文件存在
        "file_no_exit":"402", #文件不存在
        "cmd_right":"201",  #命令正确
        "cmd_error":"401",  #命令错误
        "dir_exit":"203",   #目录已存在
        "dir_no_exit":"403", #目录不存在
        "cmd_success":"204",  #命令成功执行
        "cmd_fail":"404",      #命令执行失败
        "size_enough":"205", #磁盘空间足够
        "size_empty":"405",  #磁盘空间不足
    }
    
    
    四、功能解释
    
     1.conf目录下settings.py模块记录可操作用户信息,根据用户信息生成用户字典和宿主目录,已经生成的不再新建。
     2.每个用户的宿主目录磁盘空间配额默认为100M,可在settings.py模块里进行修改
     3.程序运行在windows8.1系统上,pycharm 3.4,程序需求除断点续传与保证文件一致性外全部实现。
     4.切换目录: cd .. 返回上一级目录   cd dirname  进入dirname  eg:cd aa
        用户登陆后默认进入宿主目录,只可在宿主目录下随意切换.
     5.创建目录:mkdir dirname
                     在当前目录下创建目录,如果目录存在则报错,不存在创建.
     6.查看当前目录完整路径: pwd
     7.查看当前路径下的文件名和目录名: ls
     8.下载文件(不可续传):get filename
         ①、服务端当前目录存在此文件,客户端不存在此文件,直接下载.
            ②、服务端当前目录存在此文件,客户端存在此文件名,之前下载中断,文件不可续传(未实现).
            ③、服务端当前目录存在此文件,客户端存在此文件名,下载,文件名为filename+".new".
    
     9.上传文件:put  filename
            判断宿主目录磁盘空间是否够用,可以,上传文件;否则,报错.
    View Code

    四、需求分析

    做这个小项目之前,如果基础知识不牢的话,可以看我之前的两篇博客python之socket-ssh实例和[原创]python之socket-ftp。

    需求1:用户加密认证

    服务端与用户端进行交互前,肯定需要进行认证。在服务端认证还是在客户端?当然是服务端啦,客户端至少需发送用户名与密码,服务端接收后在数据库中查找相应用户的密码,若正确,则发送给客户端相应的状态码。这是认证的功能,如何实现加密认证?可以导入hashlib模块,用md5对密码加密,为了安全起见,服务端数据库中的密码应该是加密后的密文。客户端登陆认证时也应发送密文到服务端,服务端将接收到的密文与数据库中对应用户的密文比较。

     

    需求2:允许同时多用户登录

    其实需求1是在需求2的登陆功能中实现的。那关键就在如何解决多用户与同时(高并发)。其实这个需求挺简单的。多用户我这里不用数据库(还没玩透~),我是建一个包来存放数据,每个用户对应一个xxx.json(xxx为用户名)。json文件里面存放一个字典,为什么要用字典来存,而不是字符串,列表,回答是更简单,更易于拓展~~。高并发是什么?多个用户(客户端),发送指令,服务端能及时处理。下面看一个非高并发化的例子。

    1 if__name__=="__main__":
    2     HOST,PORT="localhost",9999
    3     #Create the server,binding to localhost on port 9999
    4     server=socketserver.TCPServer((HOST,PORT),MyTCPHandler)#实例化
    5     server.serve_forever()

    服务端用上述代码实例化,当开一个客户端时,运行没问题,但如果先后再开客户端2,3,并向服务端发送指令。客户端2,3是接收不到服务端的数据的(卡住了),但当客户端1关闭时,客户端2收到数据,当客户湍2关闭时,客户端3收到数据。将上述代码第四行改为下面的代码,则可以处理高并发:

    #每来一个请求,服务端就开启一个新的线程
    server=socketserver.ThreadingTCPServer((HOST,PORT),MyTCPHandler)#实例化

    需求3: 每个用户有自己的家目录 ,且只能访问自己的家目录

    此需求可分为两个小需求,得先有用户家目录,然后用户有访问权限,只能访问家目录下。

    每个用户都有家目录,怎么实现?刚开始我是很懵比的,后来我参考Linux,在home目录下存放各个用户的家目录。用户的家目录可以用os.path.join(HOME_PATH, xxx)来拼接(xxx为家目录),然后就可以创建用户的家目录了。越往后开发发现代码越来越多,于是我最开始就将HOME目录放在服务端的配置文件中。

    BASE_DIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    HOME_PATH=os.path.join(BASE_DIR,"home")
    print(HOME_PATH)
    输出:C:UsersAdministratorPycharmProjectslaonanhaiftpftp_serverhome

    HOME目录应当是服务端初始化时自动生成的。我用下面的代码实现。os.popen()很重要,后来的实现中还会用到~

    os.popen("mkdir%s"%user_home_path)

    目录示例如下图:

    需求3的第二个小需求。如何只能访问家目录?访问当然是通过cd命令来实现的!这与需求5是有很大联系的,可以顺手做需求5!!而想要cd切换目录,得先有目录啊!此时只有上图home目录下的两个空用户目录Alex,zcl目录。于是我顺手做了附加功能的1--mkdir新建目录。回到正题,如何只能访问家目录,我想了好久,也参考了别人的博客才一点点做出来的。Linux有cd ..可以回到上一级目录,我在cd功能也实现了这个。以zcl用户为例,zcl目录是他的家目录,他没有权限在C:UsersAdministratorPycharmProjectslaonanhaiftpftp_serverhomezcl路径下调用cd ..回到上一级目录!!

    具体实现中,应当是用户一登陆成功便进入用户的家目录。于是我在auth模块写了下两的代码。self.current_path是用户当前目录,用户在与服务端交互(cd)中是会改变的。

        # 登陆后用户当前目录, 即用户的家目录
        self.current_path = os.path.join(settings.HOME_PATH, recv_list[0])
        # 用户宿主目录
        self.user_home_path = os.path.join(settings.HOME_PATH, recv_list[0])

    需求5、6、附加功能1、2:允许用户在ftp server上随意切换目录cd、允许用户查看当前目录下文件ls、新建目录mkdir、查看当前工作目录的路径pwd

    这四个需求都没有什么难度,有共同点。以需求6(ls)为例,先看下代码实现:

    客户端的ls模块:

    复制代码
     1 import json
     2 
     3 
     4 def client_ls(self, *args):
     5     """查看当前目录下的文件(包括目录)"""
     6     cmd_split = args[0].split()
     7     if len(cmd_split) == 1 and cmd_split[0] == "ls":
     8         msg_dic = {
     9             "action":"ls",
    10         }
    11         self.client.send(json.dumps(msg_dic).encode())
    12         server_response = self.client.recv(1024)
    13         print(server_response.decode())
    复制代码

    服务端的deal_ls模块:

    复制代码
    1 import os,json
    2 
    3 def server_deal_ls(self, *args):
    4     """完成用户显示当前目录下文件(包括目录)的请求"""
    5     cmd_dic = args[0]
    6     r = os.popen("dir %s" % self.current_path)
    7     dir_message = r.read()
    8     self.request.send(dir_message.encode())
    复制代码

    实现逻辑:

    首先你得懂什么是反射!我会写这方面的博客,不过得很久以后,建议不懂具体实现的先百度一下。不懂具体实现也没事,顶多看不懂代码!你在客户端输入ls命令(或者 cd xx/mkdir xx/pwd/get xx/put xx)就通过反射调用客户端ls模块的def client_ls(self, *args):方法。然后发送包含相应action的字典(方便拓展)到服务端。服务端接收后,通过字典的action再次反射调用deal_ls模块的def server_deal_ls(self, *args):方法,处理ls命令,完成后将数据发送到客户端,客户端再将其打印到界面。

    嗯,反射太强大了!! 下面看下interactive.py交互模块,看下客户端反射的实现:

    复制代码
     1 def interactive(self):
     2     """
     3     本模块用于客户端与服务端的交互
     4     """
     5     while True:
     6         cmd = input(">>>:").strip()
     7         if len(cmd) == 0:
     8             continue
     9         cmd_str = cmd.split()[0]  # 指令
    10         if hasattr(self, "cmd_%s" % cmd_str):  # 反射
    11             func = getattr(self, "cmd_%s" % cmd_str) #获得方法对应的内存地址
    12             func(cmd)
    13         else:
    14             self.help()
    复制代码

     需求7:允许上传put和下载get文件

    这是个很有意思的功能,刚开始实现感觉蛮6的。上传与下载文件,还得保持文件的一致性。为什么得保持文件一致性?是因为怕传的时候万一丢了什么数据,被黑客改了数据。举个例子: 在下载的时候保持文件的一致性,服务端在发送文件给客户端是一行一行发的,也一行一行用md5加密,通过m.update(line)可以得出原文件的md5值m1,而客户端在接收的时候也会一行一行加密,通过m.update(line)得出收到文件的md5值m2,然后服务端发送m1给客户端进行比较,若m2与m1相同则说明客户端收到的文件是一致的,反之,说明该文件在传输过程中出现了不可告人的问题!具体的可以看我之前写的博客[原创]python之socket-ftp

    我很早就实现上传下载的功能,当时只想,能把文件传过去,下载过来就好了。于是出现了下图的问题:下传下载的文件与执行文件在同一个目录下。

       

    仔细想一下,这样真的可以吗?客户端下载的文件在bin目录下无所谓,我觉得是可以的。我这里将服务端供客户端下载的文件放在服务端的bin目录下;但上传的文件放在服务端的bin目录下,肯定是不行的。一个目录有如此多的文件,你让用户怎么找??而且用户根本没有权限访问bin目录。应当是用户当前在哪个目录(肯定是家目录以内)就上传到哪个目录,即上传到用户当前所在目录。还有一个点,用户上传空间是有限的,这就与需求4有关联了。

     需求4:对用户进行磁盘配额,每个用户的可用空间不同

    比如我想限制每个用户100M,如何实现?我在配置文件写了:

    #磁盘配额:每个用户默认最多100M
    MAX_SIZE = 102400000

    初始化时也将用户的磁盘配额写到数据库中,下面是zcl.json文件:

    {"max_size": 102400000, "username": "zcl", "password": "900150983cd24fb0d6963f7d28e17f72", "user_path": "C:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server\home\zcl"}

    接下来我想,你下载不可能需要限制配额吧!就算在yellow website我也没见过。上传空间限制倒是很多,比如百度云盘~~。

    接下来我遇到一个很头疼的问题:上传文件时要如何判断已上传文件的大小??即用户家目录的大小。

    通过看别人的博客,我找到下面的代码:

    复制代码
     1 import os
     2 
     3 
     4 def get_dirsize(dir):
     5     """
     6     获取目录的大小
     7     :param dir: 目录的路径
     8     :return: 大小(字节)
     9     """
    10     size = 0
    11     for root, dirs, files in os.walk(dir):
    12         size += sum([os.path.getsize(os.path.join(root, name)) for name in files])
    13     return size
    复制代码

    因为不懂os.walk(dir),就去看别人的博客Python 3 os.walk使用详解。大家可以看看。反正是解决我实际的问题了,哈哈~

     需求8:文件传输过程中显示进度条

    进度条我上传和下载都有做。首先我想的是,进度条是在客户端还是服务端实现?当然是客户端!才能显示在用户的界面嘛。下载的进度条较容易做,已经从服务器收到将要下载的文件的大小(字节),也知道此时刻接收文件数据的大小,两者比一下就好了。

    1         while receive_size < server_response["file_size"]:
    2             data = self.client.recv(1024)
    3             receive_size += len(data)
    4             #调用progress_bar模块的方法
    5             progress_bar.progress_bar(self, receive_size, server_response["file_size"])
    6             f.write(data)

    但上传的进度条我就卡住了。文件总大小是知道的,但已经上传的大小呢?要从服务端发送过来?那样交互就变多了,而且也不大现实……怎么办?我又上网查资料。

    终于我找到了文件操作的tell()方法:获取当前指针位置(字节)

    1     for line in f:  # 上传文件一行一行
    2         self.client.send(line)
    3         send_size = f.tell()   #获取当前指针位置(字节)
    4         progress_bar.progress_bar(self, send_size, file_size)

    五、遇到困难

    做这个小项目我遇到很多问题,一脸懵比的时候都是停下来想想,再不行看别人的博客参考一下,遇到的BUG就更多了,当然大部分稍稍修改下就好了。我觉得最难的是刚开始做的时候,整个结构都不清楚,到后面大体框架出来了,加一些功能倒是蛮简单的。

    坑1:是在我做下载功能的时候遇到的。很奇葩差点怀颖人生。先看下代码:

    客户端:

    View Code

    服务端:

    View Code

    实现客户端下载服务端文伯功能。首先客户端输入get + 文件名, 通过反射调用client_get(),发送含对应动作(get)的字典到服务端,服务端也通过反射调用server_deal_get(),此时就打开文件,发送给客户端?不,要先发送文件大小 给客户端,客户端才可以通过while,循环接收比较已接收文件大小与要接收文件大小。这里我发文件大小的同时也发了一个文件存在的状态码402,若服务端文件不存在则发送状态码403.

    很好,接下来进行测试:

    我先登陆成功,然后在客户端下载oldboy-25.avi文件,下载成功! 然后再下载一个不存在的文件aa, 就出BUG,下面看下具体的BUG提示:

    客户端:

    View Code

    服务端(下面代码嫌多可以只看我加红的字体):

    复制代码
    C:Python34python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/ftp/ftp_server/bin/ftp_server.py
    ['C:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server', 'C:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server\bin', 'C:\Python34\lib\site-packages\pip-8.1.2-py3.4.egg', 'C:\Users\Administrator\PycharmProjects\laonanhai', 'C:\Windows\SYSTEM32\python34.zip', 'C:\Python34\DLLs', 'C:\Python34\lib', 'C:\Python34', 'C:\Python34\lib\site-packages']
    {'zcl': 'abc', 'Alex': '123'}
    {'zcl': 'abc', 'Alex': '123'}
    zcl:900150983cd24fb0d6963f7d28e17f72 <class 'str'>
    ['zcl', '900150983cd24fb0d6963f7d28e17f72']
    C:UsersAdministratorPycharmProjectslaonanhaiftpftp_server/data/zcl.json
    file exist
    {'password': '900150983cd24fb0d6963f7d28e17f72', 'username': 'zcl'}
    login success
    send login_state
    127.0.0.1 wrote:
    b'{"filename": "oldboy-25.avi", "action": "get", "overridden": true}'
    客户端已准备好下载
    127.0.0.1 wrote:
    b'{"filename": "aa", "action": "get", "overridden": true}'
    127.0.0.1 wrote:
    b'xe5xaexa2xe6x88xb7xe7xabxafxe5xb7xb2xe5x87x86xe5xa4x87xe5xa5xbdxe4xb8x8bxe8xbdxbd'
    ----------------------------------------
    Exception happened during processing of request from ('127.0.0.1', 53815)
    Traceback (most recent call last):
      File "C:Python34libsocketserver.py", line 617, in process_request_thread
        self.finish_request(request, client_address)
      File "C:Python34libsocketserver.py", line 344, in finish_request
        self.RequestHandlerClass(request, client_address, self)
      File "C:Python34libsocketserver.py", line 673, in __init__
        self.handle()
      File "C:UsersAdministratorPycharmProjectslaonanhaiftpftp_servercoremain.py", line 27, in handle
        cmd_dic = json.loads(self.data.decode())   #字典格式
      File "C:Python34libjson\__init__.py", line 318, in loads
        return _default_decoder.decode(s)
      File "C:Python34libjsondecoder.py", line 343, in decode
        obj, end = self.raw_decode(s, idx=_w(s, 0).end())
      File "C:Python34libjsondecoder.py", line 361, in raw_decode
        raise ValueError(errmsg("Expecting value", s, err.value)) from None
    ValueError: Expecting value: line 1 column 1 (char 0)
    ----------------------------------------
    复制代码

    第二次下载时,服务端接收到的数据是什么鬼?!!!

    127.0.0.1 wrote:
    b'xe5xaexa2xe6x88xb7xe7xabxafxe5xb7xb2xe5x87x86xe5xa4x87xe5xa5xbdxe4xb8x8bxe8xbdxbd'

    我测试了挺久的,单独地get oldboy-25.avi(服务端存在的文件)是不会出异常的,但是先get aa(服务端不存在此文件),再get oldboy-25.avi;或者get aa, 再get aa都会出异常。
    我看了服务端的代码及BUG提示后猜想,当输入get aa时,服务端发送状态码,客户端接收后,还发给服务端self.client.send("客户端已准备好下载".encode()),而再次输入get oldboy-25.avi时,服务端接收到的也许是“客户端已准备好下载”,而不是含对应动作(get)的字典.MY GOD!!

    验证:

    1 s=b'xe5xaexa2xe6x88xb7xe7xabxafxe5xb7xb2xe5x87x86xe5xa4x87xe5xa5xbdxe4xb8x8bxe8xbdxbd'
    2 print(type(s))
    3 s1=str(s,encoding="utf-8")
    4 print(s1)
    5 
    6 ss="客户端已准备好下载"
    7 b=bytes(ss,encoding="utf-8")
    8 print(b)
    View Code

    输出:

    C:Python34python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/ftp/ftp_server/core/test.py
    <class 'bytes'>
    客户端已准备好下载
    b'xe5xaexa2xe6x88xb7xe7xabxafxe5xb7xb2xe5x87x86xe5xa4x87xe5xa5xbdxe4xb8x8bxe8xbdxbd'
    
    Process finished with exit code 0
    View Code

    说明服务端接收到的是“客户端已准备好下载”,而不是含对应动作(get)的字典!!进一步证明我猜想的是对的!如何解决这个BUG,很简单,客户端只要对从服务端收到的状态码(文件存在402;服务端文件不存在则发送状态码403)进行分开讨论就可以解决!!

    坑2: 个人觉得坑1很坑爹,我已经写得很详细了,还是怕你看不懂

    下面写一个简单的吧,放松一下:

    想实现切换目录,感觉得先实现ls,显示当前目录下的文件及目录较好,不然连当前目录下有什么目录都不知道,还怎么切换目录!如何查看当前目录(家目录)下的目录及文件?? 请看下面代码:

    r=os.popen("dir%s"%BASE_DIR)
    print(r.read())

    输出:

     驱动器 C 中的卷没有标签。
     卷的序列号是 000C-3580
    
     C:UsersAdministratorPycharmProjectslaonanhaiftpftp_server 的目录
    
    2017/01/10 周二  上午 01:02    <DIR>          .
    2017/01/10 周二  上午 01:02    <DIR>          ..
    2017/01/10 周二  上午 12:43    <DIR>          bin
    2017/01/10 周二  下午 08:54    <DIR>          conf
    2017/01/10 周二  下午 10:09    <DIR>          core
    2017/01/10 周二  上午 01:31    <DIR>          data
    2017/01/10 周二  上午 11:45    <DIR>          home
    2016/11/02 周三  下午 09:42    <DIR>          log
    2016/11/02 周三  下午 09:41                 0 __init__.py
                   1 个文件              0 字节
                   8 个目录 33,612,865,536 可用字节
    
    
    Process finished with exit code 0
    View Code

    六、源代码与模块作用

    写到这里感觉已经快没墨水了,如果有谁想做这个小项目的,希望我的博客与代码思路能帮到你,就像我一脸懵比去参考别人的博客一样。

    ftp_client

      |----bin(可执行目录)

      |         |----__init__.py

      |         |----ftp_client.py(客户端接口)   

      |----conf(配置文件目录)

      |     |----__init__.py

      |     |----settings.py(配置文件) 

      |----core(核心代码)

      |     |----__init__.py

      |     |----auth.py(客户端身份验证)

      |     |----cd.py(实现客户端在服务随意切换目录的功能,但只能访问自己的家目录)

      |     |----get.py(客户端下载功能)

      |     |----interactive.py(用于客户端与服务端的交互/反射)

      |     |----ls.py(查看当前目录下的文件(包括目录))

      |     |----main.py(主函数,运行被ftp_client.py客户端接口调用)

      |   |----mkdir.py(实现用户在当前目录下可创建目录的功能)

      |   |----progress_bar.py(进度条:用于显示上传与下载的进度)

      |   |----put.py(处理客户端上传功能)

      |   |----pwd.py(查看用户当前的目录)

      |----__init__.py

    ftp_client.py

     1 """
     2 客户端接口
     3 """
     4 
     5 import os,sys
     6 
     7 
     8 dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     9 sys.path.insert(0, dir)
    10 
    11 from core import main
    12 
    13 if __name__ == "__main__":
    14     main.run()
    View Code

    settings.py

     1 """
     2 客户端配置
     3 """
     4 
     5 LOGIN_STATE = {
     6     "auth_True":"200",   #认证成功
     7     "auth_False":"400",  #认证失败
     8     "file_exit":"202",   #文件存在
     9     "file_no_exit":"402", #文件不存在
    10     "cmd_right":"201",  #命令正确
    11     "cmd_error":"401",  #命令错误
    12     "dir_exit":"203",   #目录已存在
    13     "dir_no_eixt":"403", #目录不存在
    14     "cmd_success": "204",  # 命令成功执行
    15     "cmd_fail": "404",  # 命令执行失败
    16     "size_enough": "205",  # 磁盘空间足够
    17     "size_empty": "405",  # 磁盘空间不足
    18 }
    View Code

    auth.py

     1 """
     2 客户端身份验证
     3 """
     4 from conf import settings
     5 import hashlib
     6 
     7 def send_auth(self):
     8     """
     9     发送用户名:密码到服务端,进行登陆验证
    10     :param self:
    11     :return: 返回400(成功)或200(失败)
    12     """
    13     user_name = input("Username:")
    14     password = input("Password:")
    15     #客户端发送用户名与密码(列表)
    16     password_hash = hash(password)
    17     user_data = "%s:%s" % (user_name, password_hash)
    18     self.client.send(user_data.encode())
    19     #接收服务端返回,认证成功True,认证失败False
    20     auth_recv = self.client.recv(1024).decode()
    21     if auth_recv == settings.LOGIN_STATE["auth_True"]:
    22         print("Welcome Login".center(50, "*"))
    23         return  auth_recv
    24     elif auth_recv == settings.LOGIN_STATE["auth_False"]:
    25         print("usename or password not exist")
    26         return auth_recv
    27 
    28 
    29 
    30 def hash(data):
    31     #对密码进行md5加密
    32     m = hashlib.md5()
    33     m.update(data.encode())
    34     #返回加密后的数据
    35     return m.hexdigest()
    View Code

    cd.py

     1 import json
     2 from conf import settings
     3 
     4 
     5 def client_cd(self, *args):
     6     """实现客户端在服务随意切换目录的功能,但只能访问自己的家目录"""
     7     cmd_split = args[0].split()
     8     if len(cmd_split) > 1:
     9         cd_dir = cmd_split[1]
    10         msg_dic = {             # 为了可拓展性,用字典形式
    11             "action": "cd",   # 发送给服务端的指令
    12             "cd_dir": cd_dir,
    13         }
    14         self.client.send(json.dumps(msg_dic).encode())
    15         server_response = self.client.recv(1024)
    16         print(server_response.decode())
    17     else:
    18         print("%s:命令错误" % settings.LOGIN_STATE["cmd_error"])
    View Code

    get.py

     1 import os,json
     2 from conf import settings
     3 from core import progress_bar
     4 
     5 def client_get(self, *args):
     6     """
     7     用来处理客户端下载功能
     8     """
     9     cmd_split = args[0].split()
    10     if len(cmd_split) > 1:
    11         filename = cmd_split[1]
    12         msg_dic = {             # 为了可拓展性,用字典形式
    13             "action": "get",  # 发送给服务端的指令
    14             "filename": filename,
    15             "overridden": True
    16         }
    17         self.client.send(json.dumps(msg_dic).encode())
    18         # 防止粘包,等服务器确认
    19         # 可优化,确认同时服务端看客户端是否有权限等404 403(标准码)
    20         server_response = json.loads(self.client.recv(1024).decode())
    21         print(server_response,type(server_response))
    22         if server_response["file_exit"] == settings.LOGIN_STATE["file_exit"]:
    23             self.client.send("客户端已准备好下载".encode())
    24             if os.path.isfile(msg_dic["filename"]):    #文件已经存在
    25                 f = open(filename + ".new", "wb")
    26             else:
    27                 f = open(filename, "wb")
    28             receive_size = 0
    29             while receive_size < server_response["file_size"]:
    30                 data = self.client.recv(1024)
    31                 receive_size += len(data)
    32                 #调用progress_bar模块的方法
    33                 progress_bar.progress_bar(self, receive_size, server_response["file_size"])
    34                 f.write(data)
    35             else:
    36                 print("download from server success")
    37 
    38         elif server_response["file_exit"] == settings.LOGIN_STATE["file_no_exit"]:
    39             print("%s:请求文件不存在" % server_response["file_exit"])
    View Code

    interactive.py

     1 def interactive(self):
     2     """
     3     本模块用于客户端与服务端的交互
     4     """
     5     while True:
     6         cmd = input(">>>:").strip()
     7         if len(cmd) == 0:
     8             continue
     9         cmd_str = cmd.split()[0]  # 指令
    10         if hasattr(self, "cmd_%s" % cmd_str):  # 反射
    11             func = getattr(self, "cmd_%s" % cmd_str) #获得方法对应的内存地址
    12             func(cmd)
    13         else:
    14             self.help()
    View Code

    ls.py

     1 import json
     2 
     3 
     4 def client_ls(self, *args):
     5     """查看当前目录下的文件(包括目录)"""
     6     cmd_split = args[0].split()
     7     if len(cmd_split) == 1 and cmd_split[0] == "ls":
     8         msg_dic = {
     9             "action":"ls",
    10         }
    11         self.client.send(json.dumps(msg_dic).encode())
    12         server_response = self.client.recv(1024)
    13         print(server_response.decode())
    View Code

    main.py

     1 import socket
     2 import hashlib
     3 import os
     4 import json
     5 from core import interactive
     6 from core import put
     7 from core import get
     8 from core import auth
     9 from conf import settings
    10 from core import pwd
    11 from core import mkdir
    12 from core import ls
    13 from core import cd
    14 
    15 class FtpClient(object):
    16     def __init__(self):
    17         self.client = socket.socket()
    18 
    19     def help(self):
    20         msg = """
    21         ls
    22         pwd
    23         cd ../..
    24         get filename
    25         put filename
    26         """
    27         print(msg)
    28 
    29     def connect(self, ip, port):
    30         self.client.connect((ip, port))
    31 
    32 
    33     def interactive(self):   #交互
    34         interactive.interactive(self
    35 )
    36 
    37     def cmd_put(self, *args):  #*args是为了将来参数的拓展
    38         put.client_put(self, *args)
    39 
    40     def cmd_get(self, *args):
    41         get.client_get(self, *args)
    42 
    43     def send_auth_data(self):
    44         #返回用户帐号名与密码
    45         login_state = settings.LOGIN_STATE["auth_False"]
    46         while login_state != settings.LOGIN_STATE["auth_True"]:
    47             login_state = auth.send_auth(self)
    48 
    49     def cmd_pwd(self, *args):
    50         pwd.client_pwd(self, *args)
    51 
    52     def cmd_mkdir(self, *args):
    53         mkdir.client_mkdir(self, *args)
    54 
    55     def cmd_ls(self, *args):
    56         ls.client_ls(self, *args)
    57 
    58     def cmd_cd(self, *args):
    59         cd.client_cd(self, *args)
    60 
    61 
    62 def run():
    63     ftp_client = FtpClient()
    64     ftp_client.connect("localhost", 8787)
    65     #身份验证
    66     ftp_client.send_auth_data()
    67     #客户端与服务端交户
    68     ftp_client.interactive()
    View Code

    mkdir.py

     1 import json
     2 from conf import settings
     3 
     4 
     5 def client_mkdir(self, *args):
     6     """实现用户在当前目录下可创建目录的功能"""
     7     cm_split = args[0].split()
     8     if len(cm_split) > 1:
     9         new_dir = cm_split[1]
    10         msg_dic = {
    11             "action":"mkdir",
    12             "new_dir":new_dir,  #将新建的目录
    13             "overriden":False, #已存在的目录不可覆盖
    14         }
    15         self.client.send(json.dumps(msg_dic).encode())
    16         server_response = self.client.recv(1024)
    17         print(json.loads(server_response.decode()))
    18 
    19     else:
    20         print("%s:命令错误" % settings.LOGIN_STATE["cmd_error"])
    View Code

    progress_bar.py

     1 import sys
     2 
     3 
     4 def progress_bar(self, num, total):
     5     """
     6     进度条:用于显示上传与下载的进度
     7   :return: 无
     8     """
     9     rate = num/total
    10     rate_num = int(rate * 100)
    11     r = "
    %s%d%%" % ("|" * rate_num, rate_num)  #/r表示重新回到当前行输出
    12     sys.stdout.write(r)   #输出没有换行符
    13     sys.stdout.flush()   #清空缓存
    View Code

    put.py

     1 import os,json
     2 from conf import settings
     3 from core import progress_bar
     4 
     5 
     6 def client_put(self, *args):
     7     """
     8     用于处理客户端上传功能
     9     """
    10     cmd_split = args[0].split()  # 列表
    11     if len(cmd_split) > 1:
    12         filename = cmd_split[1]
    13         if os.path.isfile(filename):  # 判断是否存在文件
    14             file_size = os.stat(filename).st_size
    15             msg_dic = {             # 为了可拓展性,用字典形式
    16                 "action": "put",  # 发送给服务端的指令
    17                 "filename": filename,
    18                 "file_size": file_size,
    19                 "overridden": True
    20             }
    21             self.client.send(json.dumps(msg_dic).encode())
    22             # 防止粘包,等服务器确认
    23             # 可优化,确认同时服务端看客户端是否有权限等404 403(标准码)
    24             server_response = json.loads(self.client.recv(1024).decode())
    25             print(server_response)
    26             if server_response == settings.LOGIN_STATE["file_exit"] or
    27                             server_response == settings.LOGIN_STATE["file_no_exit"]:
    28                 f = open(filename, "rb")
    29                 for line in f:  # 上传文件一行一行
    30                     self.client.send(line)
    31                     send_size = f.tell()   #获取当前指针位置(字节)
    32                     progress_bar.progress_bar(self, send_size, file_size)
    33                 else:
    34                     print("file upload success")
    35                     f.close()
    36             #如果磁盘空间不足
    37             elif server_response == settings.LOGIN_STATE["size_empty"]:
    38                 print("server_response:磁盘空间不足")
    39 
    40         else:
    41             print(filename, "is not exist")
    View Code

    pwd.py

     1 import json
     2 from conf import settings
     3 
     4 def client_pwd(self, *args):
     5     """用来查看用户当前的目录"""
     6     cmd_split = args[0].split()
     7     if len(cmd_split) == 1 and cmd_split[0] == "pwd":
     8         msg_dic = {
     9             "action":"pwd",
    10         }
    11         self.client.send(json.dumps(msg_dic).encode())
    12         server_response = json.loads(self.client.recv(1024).decode())
    13         print(server_response)
    14         print(server_response["current_path"])
    15     else:
    16         print("%s:命令错误" % settings.LOGIN_STATE["cmd_error"])
    View Code

    ftp_server

      |----bin

      |     |----__init__.py

      |     |----ftp_server.py(服务端接口)

      |----core

      |     |----__init__.py

      |     |----auth.py(用户加密认证,登陆模块)

      |     |----db_handle.py(读用户数据与写用户数据--感觉这个模块有点多余~)

      |     |----deal_cd.py(处理用户切换目录的功能)

      |     |----deal_get.py(处理客户端下载文件的请求)

      |     |----deal_ls.py(完成用户显示当前目录下文件(包括目录)的请求)

      |     |----deal_mkdir.py(处理用户在当前目录(家目录下)创建目录的请求)

      |     |----deal_put.py(处理客户端上传文件的请求)

      |     |----deal_pwd.py(用来处理客户端查看当前目录下的请求)

      |     |----get_dirisize.py(获取用户家目录的大小(字节))

      |     |----main.py(主函数--运行时被ftp_server.py服务端接口调用)

      |----data(用户数据库)

      |     |----__init__.py

      |     |----Alex.json(Alex用户的数据库)

      |     |----zcl.json(zcl用户的数据库)

      |----home(home目录,用来存放各用户的家目录)

      |     |----Alex(Alex的家目录)

      |     |----zcl(zcl的家目录)

      |     |----__init__.py

      |----log(日志--未拓展)

      |     |----__init__.py

      |----__init__.py

    ftp_server.py

     1 import os,sys
     2 
     3 dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     4 sys.path.insert(0, dir)
     5 print(sys.path)
     6 
     7 from core import main
     8 
     9 if __name__ == "__main__":
    10     main.run()
    View Code

    settings.py

     1 """
     2 服务器配置
     3 """
     4 import os
     5 
     6 PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     7 HOME_PATH = os.path.join(PATH, "home")
     8 
     9 #帐号名与密码
    10 USER_DATA = {
    11     "zcl":"abc",
    12     "Alex":"123"
    13 }
    14 
    15 LOGIN_STATE = {
    16     "auth_True":"200",   #认证成功
    17     "auth_False":"400",  #认证失败
    18     "file_exit":"202",   #文件存在
    19     "file_no_exit":"402", #文件不存在
    20     "cmd_right":"201",  #命令正确
    21     "cmd_error":"401",  #命令错误
    22     "dir_exit":"203",   #目录已存在
    23     "dir_no_exit":"403", #目录不存在
    24     "cmd_success":"204",  #命令成功执行
    25     "cmd_fail":"404",      #命令执行失败
    26     "size_enough":"205", #磁盘空间足够
    27     "size_empty":"405",  #磁盘空间不足
    28 }
    29 
    30 
    31 #磁盘配额:每个用户默认最多100M
    32 MAX_SIZE = 102400000
    View Code

    auth.py

     1 """
     2 用户加密认证,登陆模块
     3 """
     4 import os,sys,hashlib
     5 import json
     6 from conf import settings
     7 from core import db_handle
     8 
     9 
    10 def auth_login(self):
    11     """用户登陆时调用"""
    12     recv_data = self.request.recv(1024).strip()
    13     recv_data = recv_data.decode()
    14     print(recv_data,type(recv_data))
    15     recv_list = recv_data.split(":")
    16     print(recv_list)
    17     # 登陆后用户当前目录, 即用户的家目录
    18     self.current_path = os.path.join(settings.HOME_PATH, recv_list[0])
    19     # 用户宿主目录
    20     self.user_home_path = os.path.join(settings.HOME_PATH, recv_list[0])
    21 
    22     user_path = "%s/data/%s.json" % (settings.PATH, recv_list[0])
    23     print(user_path)
    24     if os.path.isfile(user_path):
    25         print("user(file) exist")
    26         file_data = db_handle.user_load(user_path)
    27         print(file_data)
    28         if file_data["password"] == recv_list[1]:
    29             print("login success")
    30             #发送状态码给客户端
    31             self.request.send(settings.LOGIN_STATE["auth_True"].encode())
    32             print("send login_state")
    33             #认证成功的状态码
    34             return settings.LOGIN_STATE["auth_True"]
    35         else:
    36             #发送状态码给客户端
    37             self.request.send(settings.LOGIN_STATE["auth_False"].encode())
    38             print("send login_state")
    39             #认证失败的状态码
    40             return settings.LOGIN_STATE["auth_False"]
    41     else:
    42         # 发送状态码给客户端
    43         self.request.send(settings.LOGIN_STATE["auth_False"].encode())
    44         print("send login_state")
    45         print("False,please registe")
    46         return settings.LOGIN_STATE["auth_False"]
    47 
    48 
    49 def create_user():
    50     #服务端初始化时,先创建两个用户Alex,zcl
    51     path = settings.PATH
    52     for key in settings.USER_DATA:
    53         print(settings.USER_DATA)
    54         user_path = "%s/data/%s.json" % (path, key)
    55         if not os.path.isfile(user_path):
    56             password_hash = hash(settings.USER_DATA[key])
    57             user_data = {
    58                 "username":key,
    59                 "password":password_hash,
    60                 "user_path":os.path.join(settings.HOME_PATH, key),  #创建同时添加用户个人目录
    61                 "max_size": settings.MAX_SIZE      #磁盘配额100M
    62             }
    63             #json.dump(user_data, open(user_path,"w",encoding="utf-8"))
    64             db_handle.user_dump(user_path, user_data)
    65     user_mkdir()
    66 
    67 
    68 def user_mkdir():
    69     """创建用户个人目录,在home目录下"""
    70     for key in settings.USER_DATA:
    71         user_home_path = os.path.join(settings.HOME_PATH, key)
    72         if not os.path.isdir(user_home_path):
    73             os.popen("mkdir %s" % user_home_path)
    74 
    75 
    76 def hash(data):
    77     m = hashlib.md5()
    78     m.update(data.encode())
    79     #返回加密后的数据
    80     return m.hexdigest()
    View Code

    db_handle.py

     1 """
     2 读用户数据与写用户数据
     3 """
     4 import json
     5 from conf import settings
     6 
     7 
     8 def user_load(user_path):
     9     """
    10     读用户数据
    11     :param user_path:读出的路径
    12     :return: 用户数据字典
    13     """
    14     user_data = json.load(open(user_path, "r", encoding="utf-8"))
    15     return user_data
    16 
    17 
    18 def user_dump(user_path, user_data):
    19     """
    20     将数据写到用户数据库
    21     :param user_path:写入的路径
    22     :param user_data: 要写入的数据
    23     :return:
    24     """
    25     json.dump(user_data, open(user_path, "w", encoding="utf-8"))
    View Code

    deal_cd.py

     1 import json,os
     2 from conf import settings
     3 
     4 def server_deal_cd(self, *args):
     5     """处理用户切换目录的功能"""
     6     cmd_dic = args[0]
     7     cd_dir = cmd_dic["cd_dir"]
     8     dir_path = self.current_path + r"%s" % cd_dir
     9     if cd_dir == ".." and len(self.current_path) > len(self.user_home_path):
    10         #返回上一级目录
    11         self.request.send(json.dumps(settings.LOGIN_STATE["cmd_success"]).encode())
    12         self.current_path = os.path.dirname(self.current_path)
    13     elif os.path.isdir(dir_path):   #切换目录
    14         if cd_dir != "." and cd_dir != "..":
    15             self.request.send(json.dumps(settings.LOGIN_STATE["cmd_success"]).encode())
    16             self.current_path = self.current_path + r"%s" % cd_dir
    17             print(self.current_path)
    18         else:
    19             self.request.send(json.dumps(settings.LOGIN_STATE["cmd_fail"]).encode())
    20     else:   #切换的目录不存在
    21         self.request.send(json.dumps(settings.LOGIN_STATE["dir_no_exit"]).encode())
    View Code

    deal_get.py

     1 import os,json
     2 from conf import settings
     3 
     4 def server_deal_get(self, *args):
     5     """处理客户端下载文件的请求"""
     6     cmd_dic = args[0]
     7     filename = cmd_dic["filename"]
     8     if os.path.isfile(filename):
     9         file_size = os.stat(filename).st_size #服务端文件大小
    10         msg_dic = {
    11             "file_size":file_size,  #服务端将发给客户端的文件的大小
    12             "file_exit":settings.LOGIN_STATE["file_exit"]
    13         }
    14         self.request.send(json.dumps(msg_dic).encode())
    15         #防止粘包,服务端与客户端再进行一次交互
    16         client_response = self.request.recv(1024)
    17         print(client_response.decode())
    18         f = open(filename, "rb")
    19         for line in f:
    20             self.request.send(line)
    21         else:
    22             print("server:file upload to client success")
    23     else:
    24         msg_dic = {"file_exit":settings.LOGIN_STATE["file_no_exit"]}
    25         self.request.send(json.dumps(msg_dic).encode())
    View Code

    deal_ls.py

    1 import os,json
    2 
    3 def server_deal_ls(self, *args):
    4     """完成用户显示当前目录下文件(包括目录)的请求"""
    5     cmd_dic = args[0]
    6     r = os.popen("dir %s" % self.current_path)
    7     dir_message = r.read()
    8     self.request.send(dir_message.encode())
    View Code

    deal_mkdir.py

     1 import json,os
     2 from conf import settings
     3 
     4 
     5 def server_deal_mkdir(self, *args):
     6     """处理用户在当前目录(家目录下)创建目录的请求"""
     7     cmd_dir = args[0]
     8     new_dir = cmd_dir["new_dir"]  #当前目录在将创建的目录
     9     new_dir_path = os.path.join(self.current_path, new_dir)
    10     print(new_dir_path)
    11     if not os.path.isdir(new_dir_path):
    12         #不存在目录,则创建
    13         print("new_dir no exit")
    14         os.popen("mkdir %s" % new_dir_path)
    15         msg_dic = {
    16             "cmd_state":settings.LOGIN_STATE["cmd_success"],
    17         }
    18         self.request.send(json.dumps(msg_dic).encode())
    19     else:
    20         msg = "%s:目录已存在,请先删除再创建" % settings.LOGIN_STATE["dir_exit"]
    21         self.request.send(json.dumps(msg).encode())
    View Code

    deal_put.py

     1 import os,json
     2 from conf import settings
     3 from core import get_dirsize
     4 
     5 
     6 def server_deal_put(self, *args):
     7     """处理客户端上传文件的请求"""
     8     cmd_dic = args[0]       #字典格式
     9     filename = cmd_dic["filename"]
    10     file_size = cmd_dic["file_size"]
    11     file_path = os.path.join(self.current_path, filename)
    12     print("AA")
    13     dir_size = get_dirsize.get_dirsize(self.user_home_path)
    14     print("BB")
    15     print("当前用户磁盘空间大小:%s" % dir_size)
    16     #如果用户家目录下的大小加上本次将上传文件的大小仍小于最大的磁盘配额,则可以继续上传
    17     if dir_size+file_size < settings.MAX_SIZE:
    18 
    19         if os.path.isfile(file_path):     # 如果文件已经存在
    20             f = open(file_path + ".new", "wb")
    21             # 交互,防止粘包
    22             self.request.send(json.dumps(settings.LOGIN_STATE["file_exit"]).encode())
    23         else:  # 如果不存在,就上传
    24             f = open(file_path, "wb")
    25             self.request.send(json.dumps(settings.LOGIN_STATE["file_no_exit"]).encode())
    26 
    27         #self.request.send(b"200, OK")  # 可优化成字典json,状态码
    28         # 开始接收数据
    29         received_size = 0
    30         while received_size < file_size:
    31             data = self.request.recv(1024)
    32             received_size += len(data)
    33             f.write(data)
    34         else:  # 文件上传完成
    35             print("file [%s] has uploaded..." % filename)
    36 
    37     else:
    38         #磁盘配额不足
    39         self.request.send(json.dumps(settings.LOGIN_STATE["size_empty"]).encode())
    View Code

    deal_pwd.py

     1 import json
     2 from conf import settings
     3 
     4 
     5 def server_deal_pwd(self, *args):
     6     """ 用来处理客户端查看当前目录下的请求"""
     7     cmd_dic = args[0]  # 字典格式
     8     msg_dic = {
     9         "current_path":self.current_path,   #发送当前目录
    10         "cmd_state":settings.LOGIN_STATE["cmd_right"] #发送命令状态
    11     }
    12     self.request.send(json.dumps(msg_dic).encode())
    View Code

    get_dirsize.py

     1 import os
     2 
     3 
     4 def get_dirsize(dir):
     5     """
     6     获取目录的大小
     7     :param dir: 目录的路径
     8     :return: 大小(字节)
     9     """
    10     size = 0
    11     for root, dirs, files in os.walk(dir):
    12         size += sum([os.path.getsize(os.path.join(root, name)) for name in files])
    13     return size
    View Code

    main.py

    View Code

    七、测试

    ftp_client_1:

    View Code

    ftp_client_2:

    C:Python34python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/ftp/ftp_client/bin/ftp_client.py
    Username:Alex
    Password:123
    ******************Welcome Login*******************
    >>>:pwd
    {'cmd_state': '201', 'current_path': 'C:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server\home\Alex'}
    C:UsersAdministratorPycharmProjectslaonanhaiftpftp_serverhomeAlex
    >>>:
    View Code

    ftp_server:

    View Code
  • 相关阅读:
    JDBC的步骤
    Java异常
    两个init方法的区别
    迭代器、foreach循环、泛型集合
    servlet的生命周期
    集合类对比
    在servlet中的中文乱码,相对路径和绝对路径
    【转】学习使用Jmeter做压力测试(一)--压力测试基本概念
    【转】jmeter压力测试
    【转】配置Jmeter的自定义参数
  • 原文地址:https://www.cnblogs.com/mosson/p/6344310.html
Copyright © 2011-2022 走看看