服务端
import socket
import hashlib
import struct
import os
import setting
class FtpClient(object):
features = ['get', 'put', 'ls', 'cd', 'mkdir', 'rm'] # 客户端提供的命令提供功能
def __init__(self, server_ip, port, user, pwd):
'''
:param server_ip: 服务器ip
:param port: 服务器端口
:param user: 用户名
:param pwd: 密码
'''
self.server_ip = server_ip
self.port = port
self.user = user
self.pwd = pwd
self.sock = socket.socket() # 初始化时创建套接字
try:
self.sock.connect((self.server_ip, self.port)) # 连接服务器
except ConnectionRefusedError:
print('501', setting.CODE['501'])
return
self.pwd_hash() # 密码hash
if not self.auth():
return # 验证失败
# self.run() # 登录成功继续操作
def pwd_hash(self): # 对用户输入密码进行hash
md5 = hashlib.md5(setting.MD5_SALT.encode('utf-8'))
md5.update(self.pwd.encode('utf-8'))
self.pwd = md5.hexdigest()
def auth(self): # 上传用户密码进行验证
account = ','.join([self.user, self.pwd])
self.sock.send(account.encode('utf-8'))
auth_result = self.sock.recv(1024).decode() # 获取登录结果
if auth_result == '8000':
print(auth_result, setting.CODE[auth_result])
self.run()
else:
print(auth_result, setting.CODE[auth_result])
self.sock.close()
return False
def run(self):
while True:
user_input = input('\n>>>').strip() # 用户输入命令
command = user_input.split()
self.cmd = command[0] # 获取命令前缀
if self.cmd == 'exit': # 检测到exit则退出
self.sock.close()
break
else:
if self.cmd in self.features: # 判断命令是否支持
self.command_s = user_input.encode('utf-8')
func = getattr(self, self.cmd) # 使用反射获取命令对应方法
if len(command) == 1:
if command[0] in ['get', 'put', 'cd', 'mkdir', 'rm']:
print(setting.CODE['4000'])
continue
func()
elif len(command) == 2:
func(command[1])
else:
print(setting.CODE['4000'])
else:
print(setting.CODE['4000'])
def get(self, *args, **kwargs):
self.send_cmd(self.command_s)
filename = args[0]
file_stat = self.sock.recv(4).decode('utf-8') # 接收文件是否找到状态码
if file_stat == '1000':
tmp_file = filename + '.tmp'
file_path = os.path.join(setting.DOWNLOAD_DIR, tmp_file) # 存放文件路径
real_path = os.path.join(setting.DOWNLOAD_DIR, filename)
tmp_size = 0
if not os.path.exists(file_path):
self.sock.sendall(b'2000')
else:
self.sock.sendall(b'2001')
tmp_size = os.path.getsize(file_path)
self.sock.sendall(self.struck_info(tmp_size))
file_size = struct.unpack('i', self.sock.recv(4))[0] # 接收文件大小
receive_size = 0
receive_size += tmp_size
with open(file_path, 'ab') as f:
while receive_size < file_size:
if file_size - receive_size <= 1024:
data = self.sock.recv(file_size - receive_size)
else:
data = self.sock.recv(1024)
if not data:
print('文件下载中断!')
return
receive_size += len(data)
f.write(data)
self.toolbar(receive_size, file_size)
print()
src_md5 = self.sock.recv(1024).decode('utf-8')
dec_md5 = self.file_md5(file_path)
if src_md5 == dec_md5:
print('{}下载成功!'.format(filename))
os.rename(file_path, real_path)
else:
print('文件一致性校验失败,上传失败')
os.remove(file_path)
else:
print(setting.CODE[file_stat])
def put(self, *args, **kwargs):
'''
上传文件,断点续传
:param args:
:param kwargs:
:return:
'''
if args: # args为文件名
file_name = args[0]
if os.path.exists(file_name): # 判断文件是否存在
self.send_cmd(self.command_s)
file_size = os.path.getsize(file_name)
self.sock.send(self.struck_info(file_size)) # 发送文件大小
ques = self.sock.recv(1024).decode('utf-8') # 获取是否需要断点续传
send_size = 0 # 发送的数据计数
with open(file_name, 'rb') as f: # 打开文件
if ques == 'all_file': # 发送整个文件
start_index = 0
else:
start_index = int(ques) # 获取断点文件大小
f.seek(start_index)
send_size += start_index
for line in f:
self.sock.sendall(line) # 发送数据
send_size += len(line)
self.toolbar(send_size, file_size) # 进度条
print()
src_md5 = self.file_md5(file_name) # 文件md5
self.sock.send(src_md5.encode('utf-8')) # 发送自己文件的md5
result = self.sock.recv(1024).decode('utf-8') # 获取md5比对结果,
print(result)
else:
print('{} file not found!'.format(file_name))
def ls(self, *args, **kwargs):
self.send_cmd(self.cmd.encode('utf-8'))
'''
接收打印服务器发送的当前目录信息
:param args:
:param kwargs:
:return:
'''
info = self.sock.recv(4)
info_size = struct.unpack('i', info)[0] # 接收数据长度
receive_size = 0
info = b''
while receive_size < info_size: # 循环接收
data_size = 1024
if info_size - receive_size < 1024:
data_size = info_size - receive_size
data = self.sock.recv(data_size)
info += data
receive_size += len(data)
print(info.decode('utf-8')) # 打印目录结构
def cd(self, *args, **kwargs):
'''
切换目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
print(setting.CODE[self.sock.recv(4).decode()]) # 打印切换结果
def mkdir(self, *args, **kwargs):
'''
创建目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
status = self.sock.recv(1024).decode('utf-8')
print(setting.CODE[status])
def rm(self, *args, **kwargs):
'''
删除文件或者目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
status = self.sock.recv(1024).decode('utf-8')
print(setting.CODE[status])
def struck_info(self, info_size):
'''
打包数据
:param info_size:
:return:
'''
return struct.pack('i', info_size)
def send_cmd(self, cmd):
'''
发送命令长度包和命令信息包
:param cmd:
:return:
'''
self.sock.send(self.struck_info(len(cmd)))
self.sock.send(cmd)
def file_md5(self, file):
'''
获取文件md5
:param file:
:return:
'''
md5 = hashlib.md5()
with open(file, 'rb') as f:
for i in f:
md5.update(i)
return md5.hexdigest()
def toolbar(self, current, total):
'''
打印进度条
:param current:
:param total:
:return:
'''
val = current / total * 100
print('\r{}{:.2f}%'.format(int(val) // 2 * '>', val), end='')
if __name__ == '__main__':
user = input('请输入用户名:').strip()
pwd = input('请输入密码').strip()
cli = FtpClient('127.0.0.1', 8001, user, pwd)
客户端
import socket
import hashlib
import struct
import os
import setting
class FtpClient(object):
features = ['get', 'put', 'ls', 'cd', 'mkdir', 'rm'] # 客户端提供的命令提供功能
def __init__(self, server_ip, port, user, pwd):
'''
:param server_ip: 服务器ip
:param port: 服务器端口
:param user: 用户名
:param pwd: 密码
'''
self.server_ip = server_ip
self.port = port
self.user = user
self.pwd = pwd
self.sock = socket.socket() # 初始化时创建套接字
try:
self.sock.connect((self.server_ip, self.port)) # 连接服务器
except ConnectionRefusedError:
print('501', setting.CODE['501'])
return
self.pwd_hash() # 密码hash
if not self.auth():
return # 验证失败
# self.run() # 登录成功继续操作
def pwd_hash(self): # 对用户输入密码进行hash
md5 = hashlib.md5(setting.MD5_SALT.encode('utf-8'))
md5.update(self.pwd.encode('utf-8'))
self.pwd = md5.hexdigest()
def auth(self): # 上传用户密码进行验证
account = ','.join([self.user, self.pwd])
self.sock.send(account.encode('utf-8'))
auth_result = self.sock.recv(1024).decode() # 获取登录结果
if auth_result == '8000':
print(auth_result, setting.CODE[auth_result])
self.run()
else:
print(auth_result, setting.CODE[auth_result])
self.sock.close()
return False
def run(self):
while True:
user_input = input('\n>>>').strip() # 用户输入命令
command = user_input.split()
self.cmd = command[0] # 获取命令前缀
if self.cmd == 'exit': # 检测到exit则退出
self.sock.close()
break
else:
if self.cmd in self.features: # 判断命令是否支持
self.command_s = user_input.encode('utf-8')
func = getattr(self, self.cmd) # 使用反射获取命令对应方法
if len(command) == 1:
if command[0] in ['get', 'put', 'cd', 'mkdir', 'rm']:
print(setting.CODE['4000'])
continue
func()
elif len(command) == 2:
func(command[1])
else:
print(setting.CODE['4000'])
else:
print(setting.CODE['4000'])
def get(self, *args, **kwargs):
self.send_cmd(self.command_s)
filename = args[0]
file_stat = self.sock.recv(4).decode('utf-8') # 接收文件是否找到状态码
if file_stat == '1000':
tmp_file = filename + '.tmp'
file_path = os.path.join(setting.DOWNLOAD_DIR, tmp_file) # 存放文件路径
real_path = os.path.join(setting.DOWNLOAD_DIR, filename)
tmp_size = 0
if not os.path.exists(file_path):
self.sock.sendall(b'2000')
else:
self.sock.sendall(b'2001')
tmp_size = os.path.getsize(file_path)
self.sock.sendall(self.struck_info(tmp_size))
file_size = struct.unpack('i', self.sock.recv(4))[0] # 接收文件大小
receive_size = 0
receive_size += tmp_size
with open(file_path, 'ab') as f:
while receive_size < file_size:
if file_size - receive_size <= 1024:
data = self.sock.recv(file_size - receive_size)
else:
data = self.sock.recv(1024)
if not data:
print('文件下载中断!')
return
receive_size += len(data)
f.write(data)
self.toolbar(receive_size, file_size)
print()
src_md5 = self.sock.recv(1024).decode('utf-8')
dec_md5 = self.file_md5(file_path)
if src_md5 == dec_md5:
print('{}下载成功!'.format(filename))
os.rename(file_path, real_path)
else:
print('文件一致性校验失败,上传失败')
os.remove(file_path)
else:
print(setting.CODE[file_stat])
def put(self, *args, **kwargs):
'''
上传文件,断点续传
:param args:
:param kwargs:
:return:
'''
if args: # args为文件名
file_name = args[0]
if os.path.exists(file_name): # 判断文件是否存在
self.send_cmd(self.command_s)
file_size = os.path.getsize(file_name)
self.sock.send(self.struck_info(file_size)) # 发送文件大小
ques = self.sock.recv(1024).decode('utf-8') # 获取是否需要断点续传
send_size = 0 # 发送的数据计数
with open(file_name, 'rb') as f: # 打开文件
if ques == 'all_file': # 发送整个文件
start_index = 0
else:
start_index = int(ques) # 获取断点文件大小
f.seek(start_index)
send_size += start_index
for line in f:
self.sock.sendall(line) # 发送数据
send_size += len(line)
self.toolbar(send_size, file_size) # 进度条
print()
src_md5 = self.file_md5(file_name) # 文件md5
self.sock.send(src_md5.encode('utf-8')) # 发送自己文件的md5
result = self.sock.recv(1024).decode('utf-8') # 获取md5比对结果,
print(result)
else:
print('{} file not found!'.format(file_name))
def ls(self, *args, **kwargs):
self.send_cmd(self.cmd.encode('utf-8'))
'''
接收打印服务器发送的当前目录信息
:param args:
:param kwargs:
:return:
'''
info = self.sock.recv(4)
info_size = struct.unpack('i', info)[0] # 接收数据长度
receive_size = 0
info = b''
while receive_size < info_size: # 循环接收
data_size = 1024
if info_size - receive_size < 1024:
data_size = info_size - receive_size
data = self.sock.recv(data_size)
info += data
receive_size += len(data)
print(info.decode('utf-8')) # 打印目录结构
def cd(self, *args, **kwargs):
'''
切换目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
print(setting.CODE[self.sock.recv(4).decode()]) # 打印切换结果
def mkdir(self, *args, **kwargs):
'''
创建目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
status = self.sock.recv(1024).decode('utf-8')
print(setting.CODE[status])
def rm(self, *args, **kwargs):
'''
删除文件或者目录
:param args:
:param kwargs:
:return:
'''
self.send_cmd(self.command_s)
status = self.sock.recv(1024).decode('utf-8')
print(setting.CODE[status])
def struck_info(self, info_size):
'''
打包数据
:param info_size:
:return:
'''
return struct.pack('i', info_size)
def send_cmd(self, cmd):
'''
发送命令长度包和命令信息包
:param cmd:
:return:
'''
self.sock.send(self.struck_info(len(cmd)))
self.sock.send(cmd)
def file_md5(self, file):
'''
获取文件md5
:param file:
:return:
'''
md5 = hashlib.md5()
with open(file, 'rb') as f:
for i in f:
md5.update(i)
return md5.hexdigest()
def toolbar(self, current, total):
'''
打印进度条
:param current:
:param total:
:return:
'''
val = current / total * 100
print('\r{}{:.2f}%'.format(int(val) // 2 * '>', val), end='')
if __name__ == '__main__':
user = input('请输入用户名:').strip()
pwd = input('请输入密码').strip()
cli = FtpClient('127.0.0.1', 8001, user, pwd)
服务端配置
SOCKET_INFO = {
'BindIP': '127.0.0.1',
'Port': 8001
}
# TMP_SHARE_DIR = '/Users/zhangjin/2018/share_dir/'
# TMP_USER_INFO = {
# 'USERNAME': 'Louis',
# 'PASSWORD': '1a2e43405eaf0fa52b5b12eadd2b5eaf',
# 'HOMEPATH': '/Users/zhangjin/2018/share_dir/'
# }
TMP_USER_INFO = {
'Louis': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/Louis/'],
'zhangjin': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/zhangjin/'],
'zhangsan': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/zhangsan/'],
'lisi': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/lisi/'],
'wangwu': ['1a2e43405eaf0fa52b5b12eadd2b5eaf', '/Users/zhangjin/2018/wangwu/'],
}
MD5_SALT = '@#%$GjajhdbkwJGTkl'
CODE = {
'8000': 'LOGIN_SUCCESS',
'8001': 'LOGIN_FAILED',
'8002': 'Already logged in, unable to log in repeatedly',
'5000': 'NETWORK ERROR',
'1000': 'FILE FOUND',
'1001': 'FILE NOT FOUND OR TARGET IS A DIR',
'1010': 'Directory successful changed',
'1011': 'Directory not found, directory change failed',
'1012': 'Already a top-level directory, directory change failed',
'1013': 'Target must be a directory',
'2000': 'TMP FILE NOT FOUND',
'2001': 'TMP FILE FOUND',
'3000': 'SUCCESSFULLY DELETE',
'3001': 'DELETE FAILED',
'3002': 'FILE OR DIR NOT FOUND',
'4000': 'Invalid command',
'6000': 'Create directory successful',
'6001': 'Create directory failed, directory already exists'
}
客户端配置
MD5_SALT = '@#%$GjajhdbkwJGTkl'
CODE = {
'8000': 'LOGIN_SUCCESS',
'8001': 'LOGIN_FAILED',
'8002': 'Already logged in, unable to log in repeatedly',
'5000': 'NETWORK ERROR',
'1000': 'FILE FOUND',
'1001': 'FILE NOT FOUND OR TARGET IS A DIR',
'1010': 'Directory successful changed',
'1011': 'Directory not found, directory change failed',
'1012': 'Already a top-level directory, directory change failed',
'1013': 'Target must be a directory',
'2000': 'TMP FILE NOT FOUND',
'2001': 'TMP FILE FOUND',
'3000': 'SUCCESSFULLY DELETE',
'3001': 'DELETE FAILED',
'3002': 'FILE OR DIR NOT FOUND',
'4000': 'Invalid command',
'6000': 'Create directory successful',
'6001': 'Create directory failed, directory already exists'
}
DOWNLOAD_DIR = '/Users/zhangjin/2018/download/'
目录结构

readme
#注意,本程序在mac下开发,目前没有考虑windows系统下功能的兼容问题,如需测试请在类unix环境下运行。
功能需求:
1. 多用户同时登陆:socketserver (完成)
2. 用户登陆,加密认证:md5加密 (完成)
3. 上传/下载文件,保证文件一致性:md5加密 (完成)
4. 传输过程中现实进度条 (完成)
5. 不同用户家目录不同,且只能访问自己的家目录,上传下载时,必须在自己目录 (完成)
6. 对用户进行磁盘配额、不同用户配额可不同: 上传、下载之前做文件夹大小的判断。(未完成)
7. 用户登陆server后,可在家目录权限下切换子目录 (完成)
8. 查看当前目录下文件,新建文件夹 (完成)
9. 删除文件和空文件夹 (完成)
10. 充分使用面向对象知识+反射 (完成)
11. 支持断点续传 (完成)
目录结构
bin
start.py 程序启动目录
conf
setting.py *服务端配置信息及用户密码信息*
core
ftp_server.py 服务端主程序
db
ftp_client
ftp_client1.py 客户端程序1
ftp_client2.py 客户端程序2
setting.py 客户端配置信息
使用方法:
运行bin目录下的start.py 启动服务端
运行ftp_client目录下的ftp_client1.py 登录用户一进行操作
运行ftp_client目录下的ftp_client2.py 登录用户二进行操作
用户名密码:(用户名密码都在conf/setting.py中,密码全部都是123456)
zhangjin 123456
Louis 123456
功能说明:
登录:支持多用户登录,禁止登录状态的用户再次登录,第一次登录用户会自动创建家目录
上传下载:支持断点续传
get filename 下载文件
put filename 上传文件
切换目录: 只能在家目录中切换
cd dirname 切换目录
cd .. 返回上层目录
查看当前路径下的目录信息:
ls
删除文件或文件夹:支持删除非空文件夹
rm filename
rm dirname
#创建文件夹
mkdir dirname