FTP Server
import socket import struct from concurrent.futures import ThreadPoolExecutor import json import hashlib import os import time from demo import common_utils PUT_FILE_DIR = r'C:xLuffyFTPsharefileserverput' GET_FILE_DIR = r'C:xLuffyFTPsharefileserverget' IP_PORT = ('127.0.0.1', 9999) def run_forever(): """ 启动socket :return: """ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(IP_PORT) server_socket.listen(5) print('Server Start,IP:%s, LISTENING PORT: %s.' % IP_PORT) pool = ThreadPoolExecutor(10) while True: conn, client_addr = server_socket.accept() print('创建一个新的线程,和客户端{}通信'.format(client_addr)) pool.submit(take_over_connection, conn, client_addr) def take_over_connection(conn, client_addr): """ 用来接管socket链接,每个线程接管一个链接 :param conn: :param client_address: :return: """ print('MyServer') server = MyServer(conn, client_addr) server.handle_cmd() class MyServer(object): """ 处理客户端所有的交互socket server """ STATUS = { 300: 'File not exist !', 301: 'File exist , and the msg include the file size!', 302: 'File not exist !!!' } def __init__(self, conn, client_addr): self.conn = conn self.client_addr = client_addr def handle_cmd(self): """ 处理用户命令交互 :return: """ print('handle_cmd') while True: try: # 收到报头长度 recv_pack = self.conn.recv(4) if not recv_pack: print( 'connect {} is lost ……'.format( self.client_addr)) break # 解析报头 recv_length = struct.unpack('i', recv_pack)[0] header_data = self.conn.recv(recv_length) # json_data json_data = json.loads(header_data.decode('utf-8')) print('recv data >>> {}'.format(json_data)) action_type = json_data.get('action_type') if action_type: # 使用反射 if hasattr(self, '_{}'.format(action_type)): func = getattr(self, '_{}'.format(action_type)) func(json_data) else: print('invalid command') except ConnectionResetError: # 适用于windows操作系统 break def send_response(self, status_code, **kwargs): """ 向客户端发送响应吗 :param status: :return: """ # 构造消息头 message = { 'status': status_code, 'status_message': self.STATUS.get(status_code) } message.update(kwargs) # 更新消息 message_json = json.dumps(message) # 为防止粘包,封装消息包 header_byte = message_json.encode('utf-8') # 先发送报头的长度 self.conn.send(struct.pack('i', len(message_json))) print('发送response报头的长度: {}'.format(len(message_json))) print('发送response报头内容:{}'.format(message)) # 发送报头 self.conn.send(header_byte) def _get(self, data): """ 下载文件,如果文件存在,发送状态码+文件大小+md5,发送文件 不存在,发送状态码 :param data: :return: """ print('_get {}'.format(data)) file_path = os.path.join( GET_FILE_DIR, data.get('file_name')) if os.path.isfile(file_path): file_size = os.path.getsize(file_path) print( 'file_path: {} file_size: {} '.format( file_path, file_size)) self.send_response(301, file_size=file_size, md5=common_utils.get_md5( file_path), server_file_dir=os.path.dirname(file_path)) print('read to send file >>>', data.get('file_name')) with open(file_path, 'rb') as f: for line in f: self.conn.send(line) else: print('send file {} done'.format(file_path)) else: self.send_response(302) def _put(self, data): """ 拿到文件名和大小,检测本地是否存在相同文件 如果存在,创建新文件local_file_name+timestamp 如果存在,创建新文件local_file_name :param data: :return: """ print('_put {}'.format(data)) file_size = data.get('file_size') file_name = data.get('file_name') file_path = os.path.join( PUT_FILE_DIR, file_name) client_md5 = data.get('md5') if os.path.isfile(file_path): print('file is exist') file_path = '{}.{}'.format(file_path, str(int(time.time()))) tmp_file = '{}.down'.format(file_path) print('tmp_file:', tmp_file) f = open(tmp_file, 'wb') recv_size = 0 print('put file {} start >>> '.format(file_path)) while recv_size < file_size: data = self.conn.recv(8192) # 接收文件内容 f.write(data) recv_size += len(data) else: print(" ") print( '-- file [{}] put done, received size [{}]'.format(file_name, common_utils.bytes2human( os.path.getsize(tmp_file)))) f.close() os.rename(tmp_file, file_path) server_md5 = common_utils.get_md5(file_path) if server_md5 == client_md5: print('文件上传完整与客户端一致') if __name__ == '__main__': run_forever()
FTP Client
import argparse import socket import json import struct import sys import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) from demo import common_utils PUT_FILE_PATH = r'C:xLuffyFTPsharefileclientput' GET_FILE_PATH = r'C:xLuffyFTPsharefileclientget' IP_PORT = ('127.0.0.1', 9999) class FtpClient(): """ ftp客户端 """ def __init__(self): self.client_sock = None self.make_connect() def make_connect(self): """ 连接服务器 :return: """ try: self.client_sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM) print('连接服务器') self.client_sock.connect(IP_PORT) # 连接服务器 except Exception as e: print('连接服务器异常', e) def interactive(self): """ 交互 :return: """ menu = """ 1. 下载文件 get 1.txt 2. 上传文件 put 1.txt 3. 退出 bye """ print(menu) while True: user_input = input('请输入 >>> ').strip() if not user_input: continue cmd_list = user_input.split() if hasattr(self, '_{}'.format(cmd_list[0])): func = getattr(self, '_{}'.format(cmd_list[0])) func(cmd_list) # get def send_msg(self, action_type, **kwargs): """ 打包消息,发送到服务器 :param action_type: :param kwargs: :return: """ cmd = { 'action_type': action_type, } cmd.update(kwargs) # 更新字典 cmd_json = json.dumps(cmd) # 为防止粘包,封装包 header_byte = cmd_json.encode('utf-8') # 先发送报头的长度 self.client_sock.send(struct.pack('i', len(cmd_json))) print('发送auth报头的长度: {}'.format(len(cmd_json))) print('发送auth报头内容:{}'.format(cmd_json)) # 发送报头 self.client_sock.send(header_byte) def arg_check(self, cmd_args, len_args): if len(cmd_args) != len_args: print( 'must provide {} parameters but received {}'.format(len_args, len(cmd_args))) return False else: return True def get_response(self): """ 收到服务器向客户端发送的响应 :return: """ # 收到报头长度 recv_pack = self.client_sock.recv(4) if recv_pack: # 解析报头 recv_length = struct.unpack('i', recv_pack)[0] header_data = self.client_sock.recv(recv_length) # json_data json_data = json.loads(header_data.decode('utf-8')) print('recv response >>> {}'.format(json_data)) return json_data else: print('recv_pack is null !!!') return None def _get(self, cmd_args): """ 得到文件,发送到远程,等待返回消息, 等待文件,循环收文件 :param cmd_args: :return: """ if self.arg_check(cmd_args, 2): file_name = cmd_args[1] # get filename self.send_msg('get', file_name=file_name) response_data = self.get_response() if response_data.get('status') == 301: file_size = response_data.get('file_size') server_md5 = response_data.get('md5') file_path = os.path.join( GET_FILE_PATH, file_name) recv_size = 0 p = self.progress_bar(file_size) # 进度条 p.send(None) print('get file {} start >>> '.format(file_name)) tmp_file = '{}.down'.format(file_path) with open(tmp_file, 'wb') as f: # 写下载文件 # 序列化保存数据 while recv_size < file_size: data = self.client_sock.recv(8192) f.write(data) recv_size += len(data) p.send(recv_size) else: print(" ") print( '-- file [{}] recv done, received size [{}]'.format(file_name, file_size)) if os.path.isfile(file_path): # 如果文件存在,删除后覆盖文件 os.remove(file_path) os.rename(tmp_file, file_path) client_md5 = common_utils.get_md5(file_path) if server_md5 == client_md5: print('文件下载完整与服务端一致') else: print(response_data.get('status_message')) def _put(self, cmd_args): """ 1.上传本地文件到服务器 2.确保本地文件存在 3.把文件名和文件大小发送到远程 4.发送文件内容 :return: """ if self.arg_check(cmd_args, 2): local_file_name = cmd_args[1] # put filename full_path = os.path.join(PUT_FILE_PATH, local_file_name) if os.path.isfile(full_path): total_size = os.path.getsize(full_path) self.send_msg( 'put', file_name=local_file_name, file_size=total_size, md5=common_utils.get_md5(full_path)) p = self.progress_bar(total_size) p.send(None) upload_size = 0 with open(full_path, 'rb') as f: # 发送文件 for line in f: self.client_sock.send(line) upload_size += len(line) p.send(upload_size) else: print(" ") print('file upload done'.center(50, '-')) else: print( 'file [{}] is not exist !!!'.format(local_file_name)) def _bye(self, cmd_args): """ 退出 :return: """ print("bye") self.client_sock.close() exit(0) @staticmethod def progress_bar(total_size): """ 显示进度条 :param total_size: :return: """ current_percent = 0 last_percent = 0 while True: recv_size = yield current_percent current_percent = int(recv_size / total_size * 100) print("#" * int(current_percent / 4) + '{percent}%'.format(percent=int(current_percent)), end=" ", flush=True) if __name__ == '__main__': c = FtpClient() c.interactive()
common_util
import logging from logging import handlers import os from tkinter import Tk, filedialog import os import hashlib def bytes2human(n): # 文件大小字节单位转换 symbols = ('K', 'M', 'G', 'T', 'P', 'E') prefix = {} for i, s in enumerate(symbols): # << 左移” 左移一位表示乘2 即1 << 1=2,二位就表示4 即1 << 2=4, # 10位就表示1024 即1 << 10=1024 就是2的n次方 prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] return '%.2f%s' % (value, s) return "%sB" % n def get_md5(file_path): """ 得到文件MD5 :param file_path: :return: """ if os.path.isfile(file_path): file_size = os.stat(file_path).st_size md5_obj = hashlib.md5() # hashlib f = open(file_path, 'rb') # 打开文件 read_size = 0 while read_size < file_size: read_byte = f.read(8192) md5_obj.update(read_byte) # update md5 read_size += len(read_byte) hash_code = md5_obj.hexdigest() # get md5 hexdigest f.close() print('file: [{}] size: [{}] md5: [{}]'.format( file_path, bytes2human(read_size), hash_code)) return str(hash_code) def get_dir_size_count(dir): """ 获得文件夹中所有文件大小和文件个数 :param dir: :return: """ size = 0 count = 0 for root, dirs, files in os.walk(dir): size_li = [os.path.getsize(os.path.join(root, name)) for name in files] size += sum(size_li) count += len(size_li) print('目录{} 文件个数{}, 总共大小约{}'.format(dir, count, bytes2human(size))) return count, size def brows_local_filename(title='choose a file', force=False): """ Select a local file by filedialog of tkinter. Return an exist file path. :param title: :param force: If force is True user must choose a file. :return: """ tk = Tk() tk.withdraw() tk.wm_attributes('-topmost', 1) while True: filename = filedialog.askopenfilename(title=title) if not force or filename: break tk.destroy() return filename def brows_save_filename(title='save as'): """ Select a local path to save a file by filedialog of tkinter.Return a path for saving file. :param title: :return: """ tk = Tk() tk.withdraw() tk.wm_attributes('-topmost', 1) filename = filedialog.asksaveasfilename(title=title) tk.destroy() return filename