一、需求
1. 用户加密认证 (完成)
2. 允许同时多用户登录 (完成)
3. 每个用户有自己的家目录 ,且只能访问自己的家目录(完成)
4. 对用户进行磁盘配额,每个用户的可用空间不同(完成)
5. 允许用户在ftp server上随意切换目录cd(完成)
6. 允许用户查看当前目录下文件ls(完成)
7. 允许上传put和下载get文件(完成),保证文件一致性(此需求不做)
8. 文件传输过程中显示进度条(完成)
附加功能:
1.新建目录mkdir(完成)
2.查看当前工作目录的路径pwd(完成)
3.支持文件的断点续传(未完成)
二、程序目录结构
客户端:
服务端:
三、README
重要!
四、需求分析
做这个小项目之前,如果基础知识不牢的话,可以看我之前的两篇博客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)
目录示例如下图:
需求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,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:是在我做下载功能的时候遇到的。很奇葩差点怀颖人生。先看下代码:
客户端:
服务端:
实现客户端下载服务端文伯功能。首先客户端输入get + 文件名, 通过反射调用client_get(),发送含对应动作(get)的字典到服务端,服务端也通过反射调用server_deal_get(),此时就打开文件,发送给客户端?不,要先发送文件大小 给客户端,客户端才可以通过while,循环接收比较已接收文件大小与要接收文件大小。这里我发文件大小的同时也发了一个文件存在的状态码402,若服务端文件不存在则发送状态码403.
很好,接下来进行测试:
我先登陆成功,然后在客户端下载oldboy-25.avi文件,下载成功! 然后再下载一个不存在的文件aa, 就出BUG,下面看下具体的BUG提示:
客户端:
服务端(下面代码嫌多可以只看我加红的字体):
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) ----------------------------------------
第二次下载时,服务端接收到的数据是什么鬼?!!!
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!!
验证:
输出:
说明服务端接收到的是“客户端已准备好下载”,而不是含对应动作(get)的字典!!进一步证明我猜想的是对的!如何解决这个BUG,很简单,客户端只要对从服务端收到的状态码(文件存在402;服务端文件不存在则发送状态码403)进行分开讨论就可以解决!!
坑2: 个人觉得坑1很坑爹,我已经写得很详细了,还是怕你看不懂
下面写一个简单的吧,放松一下:
想实现切换目录,感觉得先实现ls,显示当前目录下的文件及目录较好,不然连当前目录下有什么目录都不知道,还怎么切换目录!如何查看当前目录(家目录)下的目录及文件?? 请看下面代码:
r=os.popen("dir%s"%BASE_DIR) print(r.read())
输出:
六、源代码与模块作用
写到这里感觉已经快没墨水了,如果有谁想做这个小项目的,希望我的博客与代码思路能帮到你,就像我一脸懵比去参考别人的博客一样。
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
settings.py
auth.py
cd.py
get.py
interactive.py
ls.py
main.py
mkdir.py
progress_bar.py
put.py
pwd.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
ftp_server.py
settings.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_dirsize.py
main.py
七、测试
一些测试用的输出为了方便查BUG我没去除~~有点懒~
ftp_client_1:
ftp_client_2:
ftp_server: