python开发之实现FTP服务功能
一、需求
- 用户加密认证
- 允许同时多用户登录
- 每个用户有自己的家目录 ,且只能访问自己的家目录
- 对用户进行磁盘配额,每个用户的可用空间不同 (未完成)
- 允许用户在ftp server上随意切换目录 cd (未完成)
- 允许用户查看当前目录下文件 dir
- 允许上传 post 和下载 download 文件
- 文件传输过程中显示进度条
- 附加功能:支持文件的断点续传
二、程序目录结构
- EasyClientFTP
|----bin(可执行目录)
| |----__init__.py
| |----ftp_client.py(客户端入口)
|----config(配置文件目录)
| |----__init__.py
| |----settings.py(配置文件)
|----core(核心代码)
| |----__init__.py
| |----service.py(主要逻辑)
|----lib(公共的类库)
| |----__init__.py
| |----commons.py(小功能)
|----log(未实现)
| |----__init__.py
- EasyServerFTP
|----bin(可执行目录)
| |----__init__.py
| |----ftp_server.py(服务端入口)
|----config(配置文件目录)
| |----__init__.py
| |----settings.py(配置文件)
|----core(核心代码)
| |----__init__.py
| |----service.py(主要逻辑)
|----home(家目录)
| |----joe
| | |----333.txt
| | |----666.jpg
| |----alex
|----log(未实现)
| |----__init__.py
三、需求分析及实现思路
1. 用户加密认证:
首先验证用户是否登录的代码应该写在服务端,在客户端只写与客户的交互(输入用户名密码),然后把用户输入的数据发给服务端进行验证,写一个类,类下的is_login默认是False,用户登录后创建一个对象把is_login改为True,加密用到hashlib库的md5()加密,md5不能反解,因为数据库里的密码是加密过的,要比较用户输入的密码是否正确,只能把用户在客户端输入的密码用同样方式加密与数据库中加密过的密码作比较。
2. 允许同时多用户登录:
通过socketserver的ThreadingTCPServer实现。
3. 每个用户有自己的家目录 ,且只能访问自己的家目录:
家目录在服务器端,客户端不需要家目录。假设每个用户的家目录格式为home/用户名/ ,在类中先定义一个前缀路径:home/ ,当用户登录后创建一个对象,把该用户名添加到前缀路径后面,作为该用户的家目录,如joe用户的家目录格式为home/joe/ 。
4. 允许用户查看当前目录下文件:
用subprocess.check_output,可以把用户输入的命令的结果返回,假如多个用户输入dir命令,subprocess.check_output(‘dir’),服务端不能只接收该命令,还要把发送命令的用户的家目录返回给服务端,result = subprocess.check_output(‘dir home/joe/’), 最后把result返回给客户端。
5. 文件传输过程中显示进度条:
用已经传输的数据/数据总大小 * 100% = 当前传输进度的百分比,让这个百分比持续输出显示。
6.文件断点续传:
- 一个文件有一个md5值,如果文件的内容不变,则这个md5值也不变。上传过程中,若客户端有一个大小为100M、名字为xxx.mp4的文件要上传,先把该文件的md5值计算出来发给服务端,服务端用该md5值作为文件名,创建一个文件(wb模式)。当文件上传完成时(接收数据大小 = 文件大小)该文件名自动变为原文件名(xxx.mp4),该过程中用md5值作为文件名的文件相当于一个标识符,它存在说明文件传输未结束。
- 客户端上传数据时,先发送该文件的md5值给服务端,服务端先判断有无此数据md5值的文件,如果有,把这个文件大小发给客户端(假设已经传了66M),客户端拿到后,可以与用户交互,让用户选择是想要断点续传还是重新上传,如果用户选择断点续传,把本地数据的指针指向66M,继续向服务端上传,服务端以ab模式将数据追加到此数据md5值的文件;如果用户不想断点续传而是重新发送,服务端直接以wb模式将数据写入文件即可。
- 客户端下载数据时,服务端发送该文件的md5值给客户端,客户端先判断有无此数据md5值的文件,如果有,接着客户端做判断让用户选择是否继续下载。若用户继续下载,发给服务端响应码以及已下载文件大小,服务端把指针指向用户已下载完的地方并发送,以ab模式写入本地文件,若用户重新下载,向服务端发送响应码,以wb模式写入本地文件。
- 客户端与服务端互相发数据时,应该先发送文件大小,告知对方数据有多大, 然后对方就可以循环接收数据直到 接收数据大小 = 文件大小, 但是先发送文件大小再发送文件就是连着发了两次,容易产生粘包, 解决方法是在两次发送之间加一个接收数据,接收数据可由对方随便发送。
四、代码部分
- EasyClientFTP
ftp_client.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import sys
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
from core import service
if __name__ == '__main__':
service.main()
settings.py
server = '127.0.0.1'
port = 9992
service.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import os
import re
import json
from config import settings
from lib import commons
def login(conn):
while True:
username = input('请输入用户名(q退出):')
pwd = input('请输入密码:')
login_info = [username,pwd]
conn.sendall(bytes(json.dumps(login_info), 'utf-8'))
if str(conn.recv(1024), encoding='utf-8') == "4002":
print('用户身份授权成功...')
break
else:
print('用户名或密码错误...')
def cmd(conn,inp):
conn.sendall(bytes(inp, 'utf-8'))
# 在未登录状态下,这里接收到的是login方法:self.conn.sendall(bytes("4001", 'utf-8')) 传过来的4001
# 在登录状态下,这里接收到的是cmd方法:self.conn.sendall(bytes(info_str, 'utf-8')) 传过来的info_str
info_bytes = conn.recv(1024)
print(str(info_bytes, 'utf-8'))
info_str = str(info_bytes, 'utf-8')
if info_str == '4001':