Python模拟FTP服务
项目结构:
server bin ftp_start.py # 启动程序 conf setting.py # 主配置文件 user_info.conf # 用户信息文件 core server.p # 代码核心文件 HOME # 用户家目录 ftp1 # 用户目录 file ftp2 # 用户目录 file lib check_file_exist.py # 检查文件是否存在 encrypt.py # 信息加密 get_use_quota.py # 检查用户上传磁盘空间 make_header.py # 自制报头
FTP: Server端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- import sys import os PACKAGE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(PACKAGE_PATH) from core import server if __name__ == '__main__': server.ftp_server.user_auth()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- import os # config path BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # user file USER_FILE_PATH = '%s/conf/user_info.conf' % BASE_PATH # home dir HOME_DIR = '%s\\HOME' % BASE_PATH # server bind address BIND_ADDRESS = '127.0.0.1' # listen PORT = 8888
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
[ftp1] PASSWORD = 123456 QUOTA = 100 [ftp2] PASSWORD = abcdef QUOTA = 200
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- from conf import setting from lib import get_use_quota,encrypt,check_file_exist,make_header import configparser import socket,struct,pickle,os,hashlib class MyService: # 地址家族 address_family = socket.AF_INET # 传输协议 socket_type = socket.SOCK_STREAM # 字符编码 code = 'utf-8' # 请求队列数 request_queue = 5 # 请求包的大小 request_packet_size = 1024 # 用户文件路径 user_file_path = setting.USER_FILE_PATH # socket初始化 def socket_init(self,): self.server = socket.socket(self.address_family,self.socket_type) self.server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) self.server.bind((setting.BIND_ADDRESS,setting.PORT)) self.server.listen(self.request_queue) # 用户认证 def user_auth(self): self.socket_init() tag = True while tag: # 拿到客户端监听地址和监听端口 self.conn,self.address = self.server.accept() tag2 = True while tag2: # 接收用户信息字典 bytes_header = self.conn.recv(self.request_packet_size) if not bytes_header:break # 序列化加载报头 header_dic = pickle.loads(bytes_header) # 客户端登录用户密码 client_user = header_dic['user'] client_passwd = header_dic['passwd'] # 拿到文件对象 conf_obj = configparser.ConfigParser() # 读取文件内容 conf_obj.read(self.user_file_path) # 文件所有标题(section) db_all_user = conf_obj.sections() # 用户不存在文件中 if client_user not in db_all_user: self.conn.send('False'.encode(self.code)) # 用户存在 else: # 拿到用户密码(options) db_passwd = conf_obj.get(client_user,'PASSWORD') # 密码(options)加密 db_encrypt_passswd = encrypt.encrypt_passwd(db_passwd) # 密码对比 if client_passwd != db_encrypt_passswd: self.conn.send('False'.encode(self.code)) else: self.conn.send('True'.encode(self.code)) while tag2: # 接收客户端发送的命令 get file,put file bytes_cmd = self.conn.recv(self.request_packet_size) str_command = bytes_cmd.decode(self.code) # 命令 command = str_command.split()[0] # 文件名 file_name = str_command.split()[1] # quit 退出 if command == 'quit': tag2 = False elif command == 'get': self.get(client_user,file_name) elif command == 'put': self.put(client_user) self.conn.close() self.server.close() def get(self,client_user,file_name): # 获取文件路径,文件不存在则返回False file_path = check_file_exist.check_exist(client_user,file_name) # 下载文件的大小 get_file_size = os.path.getsize(file_path) # 转换为Mb mb_size = get_file_size / (1024*1024) # 文件不存在 if not file_path: self.conn.send('False'.encode(self.code)) return # 文件存在 else: self.conn.send('True'.encode(self.code)) # 序列化报头,报头长度 pick_header,header_length = make_header.my_header(get_file_size,file_path) # 发送报头长度 self.conn.send(header_length) # 发送报头 self.conn.send(pick_header) # 发送文件 with open(file_path,'rb') as read_f: for line in read_f: self.conn.send(line) def put(self,client_user): # 拿到配置文件对象 conf_obj = configparser.ConfigParser() # 读取配置文件内容 conf_obj.read(self.user_file_path) # 用户磁盘最大配额 max_quota = conf_obj.get(client_user,'QUOTA') # 已用配额 use_quota_mb = get_use_quota.get_quota(client_user) # 接受报头长度(struct格式) struct_header_length = self.conn.recv(4) # 将struct格式转换为整数 header_length = struct.unpack('i',struct_header_length)[0] # (int,)元组格式 # 接受报头 pick_header = self.conn.recv(header_length) header_dic = pickle.loads(pick_header) # 获取上传文件名称 put_file_name = header_dic['filename'] # 获取上传文件大小 put_file_size = header_dic['filesize'] # 转换成mb大小 put_file_size_mb = put_file_size / (1024*1024) # 获取上传文件md5校验值 put_file_md5 = header_dic['md5'] # 已用配额+本次文件大小 > 最大用户配额 if use_quota_mb + put_file_size_mb > int(max_quota): self.conn.send('False'.encode(self.code)) return else: self.conn.send('True'.encode(self.code)) # 接受上传文件到用户的家目录 HOME_DIR = os.path.join(setting.HOME_DIR,client_user) file_abs_path = os.path.join(HOME_DIR,put_file_name) with open(file_abs_path,'wb') as write_file: put_size = 0 while put_size < put_file_size: # 接收文件一行内容 recv_line = self.conn.recv(self.request_packet_size) # 写入文件 write_file.write(recv_line) write_file.flush() # 接受内容大小 put_size += len(recv_line) # 文件MD5校验 with open(file_abs_path,'rb') as read_file: for line in read_file: md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8')) md5.update(line) local_md5_data = md5.hexdigest() if local_md5_data != put_file_md5: self.conn.send('False'.encode(self.code)) else: self.conn.send('True'.encode(self.code)) ftp_server = MyService()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- from conf import setting import os def check_exist(client_user,file): # 家目录 home = os.path.join(setting.HOME_DIR,client_user) # 如果文件名不带目录,则为当前目录 if file[0] != '/' or file[0] != '\\': file = os.path.join(home,file) # 文件绝对路径列表 file_list = [] # 遍历目录、文件 for root,dirs,files in os.walk(home): for f in files: # 文件名字不为空 if f: # 添加到文件列表 file_list.append(os.path.join(root,f)) if file not in file_list:return False # 文件存在 return file
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python import hashlib # 加密密码 def encrypt_passwd(passwd): # md5加密 my_md5 = hashlib.md5('猪肉炖粉条'.encode('utf-8')) my_md5.update(passwd.encode('utf-8')) # 获取加密值 encrypt_var = my_md5.hexdigest() # 返回加密后的值 return encrypt_var
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- from conf import setting import os # 获取已用磁盘配额 def get_quota(user): # 家目录 home_dir = os.path.join(setting.HOME_DIR,user) # 遍历所有文件 for root,dirs,files in os.walk(home_dir): # 获取所有文件大小总和,并换算为MB total_size = sum([os.path.getsize(os.path.join(root,filename)) for filename in files]) / (1024*1024) return total_size
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- import struct,hashlib,os,pickle def my_header(file_size,file_path): # md5校验文件 with open(file_path,'rb') as read_file: for line in read_file: md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8')) md5.update(line) md5_data = md5.hexdigest() # 报头 header_dic = { 'filename': os.path.basename(file_path), 'filesize': file_size, 'md5': md5_data } # 序列化报头 pick_header = pickle.dumps(header_dic) # 报头长度 header_length = struct.pack('i',len(pick_header)) return pick_header,header_length
FTP: Client端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding: utf-8 -*- import socket,struct,pickle import hashlib,sys,os import getpass class Myclient: # 地址家族 address_family = socket.AF_INET # 协议类型 socket_type = socket.SOCK_STREAM # 字符编码 code = 'utf-8' # 请求包的大小 request_packet_size = 1024 def __init__(self,bind_address,bind_port): self.bind_address = bind_address self.bind_port = bind_port # socket初始化连接 def socket_init(self): self.client = socket.socket(self.address_family,self.socket_type) self.client.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) self.client.connect((self.bind_address,self.bind_port)) # 登录 def login(self): self.socket_init() count = 0 tag = True while tag: user = input('user >>: ').strip() passwd = getpass.getpass('password >>: ').strip() # 用户或密码有一个为空 if not user or not passwd: count += 1 continue # 密码加密 md5_passwd = hashlib.md5('猪肉炖粉条'.encode(self.code)) md5_passwd.update(passwd.encode(self.code)) passwd_str = md5_passwd.hexdigest() # 用户信息字典 header_dic = { 'user': user, 'passwd': passwd_str } # 序列化信息字典 pick_dic = pickle.dumps(header_dic) # 发送用户信息字典 self.client.send(pick_dic) # 接收登录信息 log_info_byets = self.client.recv(self.request_packet_size) log_info = log_info_byets.decode(self.code) if log_info == 'True': print('login success') while tag: # 输入命令 self.client_cmd = input('>>: ').strip() # 命令列表 cmd_list = self.client_cmd.split() # 退出 if self.client_cmd == 'quit': # 发送退出信号 self.client.send('quit server'.encode(self.code)) tag = False break # 命令语法错误 if len(cmd_list) != 2: print('Syntax error') continue # 命令 cmd = cmd_list[0] file_name = cmd_list[1] # 执行命令 if cmd == 'get': self.client.send(self.client_cmd.encode(self.code)) self.get() elif cmd == 'put': # 上传的文件是否存在 file_exist = self.check_file_exist(file_name) # 存在则发送命令 if file_exist: self.client.send(self.client_cmd.encode(self.code)) self.put(file_name) else: print('file not does exist') else: print('user or passwd error') count += 1 if count == 3: tag = False self.client.close() # 检查上传文件是否存在 def check_file_exist(self,file_path): # 如果发送文件不带目录,则为当前目录 if file_path[0] != '/' or file_path[0] != '\\': file_path = os.path.join(os.getcwd(),file_path) if os.path.exists(file_path): return True return False # md5校验 def md5_check(self,file_path): with open(file_path, 'rb') as read_file: for line in read_file: md5 = hashlib.md5('小鸡炖蘑菇'.encode('utf-8')) md5.update(line) md5_data = md5.hexdigest() return md5_data # get命令 def get(self): # 接受文件是否存在信号 bytes_file_exist = self.client.recv(self.request_packet_size) file_exist = bytes_file_exist.decode(self.code) # 文件存在 if file_exist == 'True': # 报头长度(struct格式) struct_header_length = self.client.recv(4) # 将struct格式转换为整数 header_length = struct.unpack('i',struct_header_length)[0] # (int,)元组格式 # 接受报头信息 pick_header = self.client.recv(header_length) header_dic = pickle.loads(pick_header) # 获取下载文件名 get_file_name = header_dic['filename'] # 获取下载文件大小 get_file_size = header_dic['filesize'] # 获取文件下载MD5校验值 get_file_md5 = header_dic['md5'] # 写入文件 with open(get_file_name,'wb') as write_file: down_size = 0 while down_size < get_file_size: # 接收文件内容 recv_line = self.client.recv(self.request_packet_size) # 写入文件 write_file.write(recv_line) # 接受内容大小 down_size += len(recv_line) # 下载文件进度百分比 down_percent = down_size / get_file_size # 进度条输出格式 my_str = ('[%%-%ds]' % 50) % (int(down_percent*50)*'#') # 打印进度条 print('\r%s %s%%' % (my_str,int(down_percent*100)),end='',file=sys.stdout,flush=True) print('') # 文件MD5校验 md5_data = self.md5_check(get_file_name) if md5_data != get_file_md5: print('File md5 check failed') # 文件不存在 else: print('file not does exist') # put命令 def put(self,file_path): # 发送文件大小 file_size = os.path.getsize(file_path) # md5校验文件 md5_data = self.md5_check(file_path) # 报头信息 header_dic = { 'filename': os.path.basename(file_path), 'filesize': file_size, 'md5': md5_data, } # 序列化报头 pick_header = pickle.dumps(header_dic) # 报头长度 header_length = struct.pack('i',len(pick_header)) # 发送报头长度 self.client.send(header_length) # 发送报头 self.client.send(pick_header) # 接受磁盘配额是否足够信息 quota_info = self.client.recv(1024) if quota_info.decode(self.code) == 'False': print('insufficient disk quota') return # 上传文件 send_total_size = 0 with open(file_path,'rb') as read_file: for line in read_file: # 发送文件一行内容 self.client.send(line) # 发送总内容 send_total_size += len(line) # 发送文件内容百分比 put_percnet = send_total_size / file_size # 进度条输出格式 my_str = ('[%%-%ds]' % 50) % (int(put_percnet*50)*'#') # 打印进度条 print('\r%s %s%%' % (my_str,int(put_percnet*100)),end='',file=sys.stdout,flush=True) print('') # 接收文件MD5校验信息 md5_data_byets = self.client.recv(self.request_packet_size) md5_data = md5_data_byets.decode(self.code) # 校验失败 if md5_data == 'False': print('File md5 check failed') if __name__ == '__main__': ftp_client = Myclient('127.0.0.1',8888) ftp_client.login()
模拟结果:
PUT
GET