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

来源:互联网 发布:cpu百分率 linux命令 编辑:程序博客网 时间:2024/06/04 23:35

一、需求

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

 1.新建目录mkdir(完成)

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

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

 

二、程序目录结构

客户端:

服务端:

 

 

三、README

重要!

 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",99993     #Create the server,binding to localhost on port 99994     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)

目录示例如下图:

需求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模块:

复制代码
 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,json2 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,如何实现?我在配置文件写了:

#磁盘配额:每个用户默认最多100MMAX_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 = 011     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:\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.jsonfile exist{'password': '900150983cd24fb0d6963f7d28e17f72', 'username': 'zcl'}login successsend login_state127.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 NoneValueError: Expecting value: line 1 column 1 (char 0)----------------------------------------
复制代码

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

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!!

验证:

 View Code

输出:

 View Code

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

 

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

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

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

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

输出:

 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

 View Code

settings.py

 View Code

auth.py

 View Code

cd.py

 View Code

get.py

 View Code

interactive.py

 View Code

ls.py

 View Code

main.py

 View Code

mkdir.py

 View Code

progress_bar.py

 View Code

put.py

 View Code

pwd.py

 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

 View Code

settings.py

 View Code

auth.py

 View Code

db_handle.py

 View Code

deal_cd.py

 View Code

deal_get.py

 View Code

deal_ls.py

 View Code

deal_mkdir.py

 View Code

deal_put.py

 View Code

deal_pwd.py

 View Code

get_dirsize.py

 View Code

main.py

 View Code

 

七、测试

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

ftp_client_1:

 View Code

 

ftp_client_2:

 View Code

 

ftp_server:

 View Code

 

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