python之FTP程序(支持多用户在线)

时间:2021-08-02 04:08:02

一、需求

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

 1.新建目录mkdir(完成)

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

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

 

二、程序目录结构

客户端:

python之FTP程序(支持多用户在线)

服务端:

 python之FTP程序(支持多用户在线)

 

三、README

重要!

python之FTP程序(支持多用户在线)  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:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server\home

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

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

目录示例如下图:

python之FTP程序(支持多用户在线)

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

python之FTP程序(支持多用户在线)
 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())
python之FTP程序(支持多用户在线)

服务端的deal_ls模块:

python之FTP程序(支持多用户在线)
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())
python之FTP程序(支持多用户在线)

实现逻辑:

首先你得懂什么是反射!我会写这方面的博客,不过得很久以后,建议不懂具体实现的先百度一下。不懂具体实现也没事,顶多看不懂代码!你在客户端输入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交互模块,看下客户端反射的实现:

python之FTP程序(支持多用户在线)
 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()
python之FTP程序(支持多用户在线)

 

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

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

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

python之FTP程序(支持多用户在线)   python之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我也没见过。上传空间限制倒是很多,比如百度云盘~~。

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

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

python之FTP程序(支持多用户在线)
 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
python之FTP程序(支持多用户在线)

因为不懂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:是在我做下载功能的时候遇到的。很奇葩差点怀颖人生。先看下代码:

客户端:

python之FTP程序(支持多用户在线)  View Code

服务端:

python之FTP程序(支持多用户在线)  View Code

 

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

很好,接下来进行测试:

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

客户端:

python之FTP程序(支持多用户在线)  View Code

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

python之FTP程序(支持多用户在线)
C:\Python34\python3.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:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_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'\xe5\xae\xa2\xe6\x88\xb7\xe7\xab\xaf\xe5\xb7\xb2\xe5\x87\x86\xe5\xa4\x87\xe5\xa5\xbd\xe4\xb8\x8b\xe8\xbd\xbd'
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 53815)
Traceback (most recent call last):
  File "C:\Python34\lib\socketserver.py", line 617, in process_request_thread
    self.finish_request(request, client_address)
  File "C:\Python34\lib\socketserver.py", line 344, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "C:\Python34\lib\socketserver.py", line 673, in __init__
    self.handle()
  File "C:\Users\Administrator\PycharmProjects\laonanhai\ftp\ftp_server\core\main.py", line 27, in handle
    cmd_dic = json.loads(self.data.decode())   #字典格式
  File "C:\Python34\lib\json\__init__.py", line 318, in loads
    return _default_decoder.decode(s)
  File "C:\Python34\lib\json\decoder.py", line 343, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "C:\Python34\lib\json\decoder.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)
----------------------------------------
python之FTP程序(支持多用户在线)

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

127.0.0.1 wrote:
b'\xe5\xae\xa2\xe6\x88\xb7\xe7\xab\xaf\xe5\xb7\xb2\xe5\x87\x86\xe5\xa4\x87\xe5\xa5\xbd\xe4\xb8\x8b\xe8\xbd\xbd'

我测试了挺久的,单独地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!!

验证:

python之FTP程序(支持多用户在线)  View Code

输出:

python之FTP程序(支持多用户在线)  View Code

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

 

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

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

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

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

输出:

python之FTP程序(支持多用户在线)  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

python之FTP程序(支持多用户在线)  View Code

settings.py

python之FTP程序(支持多用户在线)  View Code

auth.py

python之FTP程序(支持多用户在线)  View Code

cd.py

python之FTP程序(支持多用户在线)  View Code

get.py

python之FTP程序(支持多用户在线)  View Code

interactive.py

python之FTP程序(支持多用户在线)  View Code

ls.py

python之FTP程序(支持多用户在线)  View Code

main.py

python之FTP程序(支持多用户在线)  View Code

mkdir.py

python之FTP程序(支持多用户在线)  View Code

progress_bar.py

python之FTP程序(支持多用户在线)  View Code

put.py

python之FTP程序(支持多用户在线)  View Code

pwd.py

python之FTP程序(支持多用户在线)  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

python之FTP程序(支持多用户在线)  View Code

settings.py

python之FTP程序(支持多用户在线)  View Code

auth.py

python之FTP程序(支持多用户在线)  View Code

db_handle.py

python之FTP程序(支持多用户在线)  View Code

deal_cd.py

python之FTP程序(支持多用户在线)  View Code

deal_get.py

python之FTP程序(支持多用户在线)  View Code

deal_ls.py

python之FTP程序(支持多用户在线)  View Code

deal_mkdir.py

python之FTP程序(支持多用户在线)  View Code

deal_put.py

python之FTP程序(支持多用户在线)  View Code

deal_pwd.py

python之FTP程序(支持多用户在线)  View Code

get_dirsize.py

python之FTP程序(支持多用户在线)  View Code

main.py

python之FTP程序(支持多用户在线)  View Code

 

七、测试

一些测试用的输出为了方便查BUG我没去除~~有点懒~

ftp_client_1:

python之FTP程序(支持多用户在线)  View Code

 

ftp_client_2:

python之FTP程序(支持多用户在线)  View Code

 

ftp_server:

python之FTP程序(支持多用户在线)  View Code

 

1.非系统的学习也是在浪费时间 2.做一个会欣赏美,懂艺术,会艺术的技术人