一,项目题目:基于线程开发一个FTP服务器

二,项目要求:

基本要求:

1.用户加密认证
 
2.允许同时多用户登录
 
3.每个用户有自己的家目录 ,且只能访问自己的家目录
 
4.对用户进行磁盘配额,每个用户的可用空间不同
 
5.允许用户在ftp server上随意切换目录
 
6.允许用户查看当前目录下文件
 
7.允许上传和下载文件,保证文件一致性(md5)
 
8.文件传输过程中显示进度条
 
9.附加功能:支持文件的断点续传

  

扩展需求:

作业需求:
 
1.在之前开发的FTP基础上,开发支持多并发的功能
 
2.不能使用SocketServer模块,必须自己实现多线程
 
3.必须用到队列Queue模块,实现线程池
 
4.允许配置最大并发数,比如允许只有10个并发用户

三,注意事项: 

基本操作:满足需求1,2,3,4
大神要求:代码写的健壮、清晰

  

四,项目分析:

1,用户加密认证

  这个肯定需要用到configparser 和hashlib模块,用md5进行加密,服务端与用户端
进行交互前,肯定需要进行认证,在服务端进行认证,客户端需要发送用户名及密码,但
是为了安全起见,服务端数据库中的密码应该是加密后的密文,客户端登陆认证时也应该
发送密文到服务端,服务端接受到密文与数据库中对应的密文进行比较。

  

2,查看自己的当前目录下的文件

  这个只需要写一个dir就ok
简单的说,使用configparse模块就可以完成

  

3,文件传输中显示进度条

  下载的进度条比较好实现,我们可以从服务端受到将要下载的文件的大小,

  上传的进度条,我们可以利用文件操作的tell()方法,获取当前指针位置(字节)

4,在之前的基础上实现多并发的功能

    并发,是伪并行,,即看起来是同时运行的,单个CPU+多道技术就可以实现并发

    多道技术概念回顾:内存中同时存入多道(多个)程序,cpu从一个进程快速切换
到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu
只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,
即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)

  

5,不能使用SocketServer模块,必须自己实现多线程

        多线程(即多个控制线程)的概念是,在一个进程中存在多个线程,多个线程共享该进程的地址空间,
相当于一个车间内有多个流水线,都共有一个车间的资源。例如,北京地铁与上海地铁是不同的进程,而北京
地铁里的1号线是一个线程,北京地铁所有的线路共享该地铁的所有资源。 为什么使用多线程?
1,同一个进程内可以共享该进程内的地址资源 2,创建线程的开销远小于创建进程的开销(创建一个进程,就是创建一个车间,涉及到申请空间
,而且在该空间内至少创建一个流水线,但是创建线程,就只是在一个车间内造一条流水线,无需申请
空间,所以创建开销小)

  

6,必须用到队列Queue模块,实现线程池

  使用线程池即可,如果不会可以参考此博客的练习题:http://www.cnblogs.com/wj-1314/p/9039970.html

7,允许配置最大并发数,比如允许只有10个并发用户

8,小编的主要思路

- 1 对于此项目,最初的想法是写出上传,和下载文件的程序,包括客户端和服务端。

-  2 在此基础上扩展程序,包括提出开始程序到bin里面,配置文件在config里面

-  3 然后把上传文件和下载文件的程序进行断点续传的程序重构

-   4 在此基础上,对文件进行加密

-   5 增加功能,包括设置进度条,增加查看功能,增加目录功能,删除文件功能,切换目录功能等

-   6 然后再设置磁盘分配功能,完善内容

-   7 然后添加用户登陆,包括对用户的密码加密等功能

-   8 写完后检查程序

 

五,README文件

## 作者:zhanzhengrecheng
## 版本:示例版本 v0.1
## 程序介绍:
- 实现了基于线程开发一个FTP服务器的常用功能
- 基本功能全部用python的基础知识实现,用到了socket\hashlib\configparse\os\sys\pickle\函数\模块\类知识
- 在保证了支持多并发的功能上,不使用SocketServer模块,自己实现了多线程,而且使用了队列 ## 概述
本次作业文件夹一共包含了以下4个文件:
- 程序结构图:整个Thread_based_FTP_homework的程序文件结构
- 程序结构文件:整个Thread_based_FTP_homework的程序文件结构
- 程序文件: Thread_based_FTP_homework
- 程序说明文件:README.md ## 程序要求
- 1.用户加密认证
- 2.允许同时多用户登录
- 3.每个用户有自己的家目录 ,且只能访问自己的家目录
- 4.对用户进行磁盘配额,每个用户的可用空间不同
- 5.允许用户在ftp server上随意切换目录
- 6.允许用户查看当前目录下文件
- 7.允许上传和下载文件,保证文件一致性(md5)
- 8.文件传输过程中显示进度条
- 9.附加功能:支持文件的断点续传 - 10.在之前开发的FTP基础上,开发支持多并发的功能 
- 11.不能使用SocketServer模块,必须自己实现多线程 
- 12.必须用到队列Queue模块,实现线程池 
- 13.允许配置最大并发数,比如允许只有10个并发用户 ## 本项目思路
- 1 对于此次项目,在上次作业的基础上完成
- 2 本次首要任务,为了降低程序的耦合性,将把server端和client端的许多东西分出来,保证一个函数只做一件事情
- 3 发现了上次作业里面出现的小问题,进行了解决
- 4 使用队列Queue模块,实现多线程
- 5 设置配置最大的并发数,此处设置在settings里面,最大并发用户设置为3 ##### 备注(程序结构)
> 目前还不会把程序树放在README.md里面,所以做出程序结构的txt版本和图片版本,放在文件外面方便查看 ## 对几个实例文件的说明
### 几个实例文件全是为了上传和下载使用,自己随便找的素材,没有把视频,照片上传 ## 不足及其改进的方面
### 每次程序从用户登陆到使用只能完成一次功能,不能重复使用 ## 程序结构 │ Thread_based_FTP_homework
│ __init__.py
│
├─client # 客户端程序入口
│ │ __init__.py
│ ├─bin # 可执行程序入口目录
│ │ run.py
│ │ __init__.py
│ ├─config # 配置文件目录
│ │ │ settings.py # 配置文件
│ │ │ __init__.py
│ ├─core # 主要逻辑程序目录
│ │ │ file_func.py # client端文件操作功能模块
│ │ │ ftp_client.py # client端主程序模块
│ │ │ md5_func.py # client端对文件加密操作功能模块
│ │ │ progress_bar_func_func.py # client端文件下载进度条操作功能模块
│ │ │ __init__.py
│ ├─download # 下载内容模块
│ │ a.txt
│ │ b.txt
│ │ c.txt
│ └─upload # 上传内容模块
│ a.txt
│ b.txt
└─server # 服务端程序入口
├─bin
│ run.py # 可执行程序入口目录
│ __init__.py
├─config # 配置文件目录
│ │ accounts.ini # 账号密码配置文件
│ │ settings.py # 配置文件
│ │ __init__.py
├─core # 主要逻辑程序目录
│ │ ftp_server.py # server端主程序模块
│ │ main.py # 主程序模块
│ │ user_handle.py # 用户注册登录模块
│ │ file_func.py # server端文件操作功能模块
│ │ auth_func.py # server端用户认证功能模块
│ │ md5_func.py # server端对文件加密操作功能模块
└─home # 家目录
│ __init__.py
├─curry # curry用户的家目录
│ │ a.txt
│ │ b.txt
│ │ c.txt
├─durant # durant用户的家目录
│ └─test3
│ └─test4
└─james # james用户的家目录
│ a.txt
│ b.txt
│ c.txt
│ test1
│ test2
└─test3

  

六,程序结构图

八,程序代码

1,server

1.1 bin

run.py

# _*_ 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 ftp_server
from core import main
from config import settings if __name__ == '__main__':
a = main.Manager()
a.interactive()

  

1.2config

settings.py

# _*_ coding: utf-8 _*_
import os
import sys
import socket
import logging
 
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
 
ACCOUNTS_FILE = os.path.join(BASE_DIR,'config','accounts.ini')
 
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
 
BIND_HOST = '127.0.0.1'
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
coding = 'utf-8'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False
# 允许配置最大并发数,这里设置为3
MAX_CONCURRENT_COUNT =3

  

1.3core

auth.py

# _*_ coding: utf-8 _*_
import pickle
import hashlib
import os
import struct from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func class Auth_func(): def auth(self, conn):
'''
处理用户的认证请求
1,根据username读取accounts.ini文件,然后查看用户是否存在
2,将程序运行的目录从bin.user_auth修改到用户home/username方便之后查询
3,把客户端返回用户的详细信息
:return:
'''
while True:
user_dic = self.get_recv(conn)
username = user_dic['username']
password = user_dic['password']
md5_obj = hashlib.md5(password.encode('utf-8'))
check_password = md5_obj.hexdigest()
user_handle = UserHandle(username)
# 判断用户是否存在 返回列表,
user_data = user_handle.judge_user()
if user_data:
if user_data[0][1] == check_password:
conn.send(struct.pack('i', 1)) # 登录成功返回 1
self.homedir_path = os.path.join(settings.BASE_DIR, 'home', username)
# 将程序运行的目录名修改到 用户home目录下
os.chdir(self.homedir_path)
# 将用户配额的大小从M 改到字节
self.quota_bytes = int(user_data[2][1]) * 1024 * 1024
user_info_dic = {
'username': username,
'homedir': user_data[1][1],
'quota': user_data[2][1]
}
# 用户的详细信息发送到客户端
conn.send(pickle.dumps(user_info_dic))
return True
else:
conn.send(struct.pack('i', 0)) # 登录失败返回 0
else:
conn.send(struct.pack('i', 0)) # 登录失败返回 0 def get_recv(self, conn):
'''从client端接收发来的数据'''
return pickle.loads(conn.recv(settings.max_recv_bytes)) def current_home_size(self):
"""得到当前用户目录的大小,字节/M"""
self.home_bytes_size = 0
File_func().recursion_file(self.homedir_path, self.home_bytes_size)
home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)

  

file_func.py

# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle from config import settings
from core import ftp_server
from core import md5_func class File_func(object):
def readfile(self,file_path):
'''读取文件,得到文件内容的bytes类型'''
with open(file_path, 'rb') as f:
filedata = f.read()
return filedata def send_filedata(self,file_path,conn,exist_file_size=0):
"""下载时,将文件打开,send(data)"""
with open(file_path, 'rb') as f:
f.seek(exist_file_size)
while True:
data = f.read(1024)
if data:
conn.send(data)
else:
break def write_file(self,conn,f,recv_size,file_size):
'''上传文件时,将文件内容写入到文件中'''
while recv_size < file_size:
res = conn.recv(settings.max_recv_bytes)
f.write(res)
recv_size += len(res)
conn.send(struct.pack('i', recv_size)) # 为了进度条的显示 def recursion_file(self, homedir_path,home_bytes_size):
"""递归查询用户目录下的所有文件,算出文件的大小"""
res = os.listdir(homedir_path)
for i in res:
path = os.path.join(homedir_path,i)
if os.path.isdir(path):
self.recursion_file(path,home_bytes_size)
elif os.path.isfile(path):
home_bytes_size += os.path.getsize(path) def current_home_size(self,homedir_path):
"""得到当前用户目录的大小,字节/M"""
self.home_bytes_size =0
self.recursion_file(self.home_bytes_size,homedir_path)
home_m_size = round(self.home_bytes_size / 1024 / 1024, 1)

  

ftp_server.py

# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import pickle
import subprocess
import hashlib
import queue
from threading import Thread
from threading import currentThread from config import settings
from core.user_handle import UserHandle
from core.file_func import File_func
from core.md5_func import Md5_func
from core.auth_func import Auth_func
class FTPServer(): def __init__(self,server_address,bind_and_listen = True):
self.server_address = server_address
self.socket = socket.socket(settings.address_family,settings.socket_type)
self.q = queue.Queue(settings.MAX_CONCURRENT_COUNT)
if bind_and_listen:
try:
self.server_bind()
self.server_listen()
except Exception:
self.server_close() def server_bind(self):
allow_reuse_address = False
if allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address) def server_listen(self):
self.socket.listen(settings.listen_count) def server_close(self):
self.socket.close() def server_accept(self):
return self.socket.accept() def conn_close(self, conn):
conn.close() def server_link(self):
print("\033[31;1mwaiting client .....\033[0m")
while True: # 链接循环
conn,self.client_addr = self.server_accept()
print('客户端地址:', self.client_addr)
# while True: # 通信循环
try:
t = Thread(target=self.server_handle,args=(conn,))
self.q.put(t)
t.start()
except Exception as e:
print(e)
self.conn_close(conn)
self.q.get() def server_handle(self,conn):
'''处理与用户的交互指令'''
if Auth_func().auth(conn):
print("\033[32;1m-------user authentication successfully-------\033[0m")
try:
res = conn.recv(settings.max_recv_bytes)
if not res:
self.conn_close(conn)
self.q.get()
# 解析命令,提取相应的参数
self.cmds = res.decode(settings.coding).split()
if hasattr(self, self.cmds[0]):
func = getattr(self, self.cmds[0])
func(conn)
except Exception as e:
print(e)
self.conn_close(conn)
self.q.get() def get(self, conn):
'''
下载,首先查看文件是否存在,然后上传文件的报头大小,上传文件,以读的方式发开文件
找到下载的文件
    发送 header_size
    发送 header_bytes file_size
    读文件 rb 发送 send(line)
    若文件不存在,发送0 client提示:文件不存在
:param cmds:
:return:
'''
if len(self.cmds) > 1:
filename = self.cmds[1]
self.file_path = os.path.join(os.getcwd(), filename)
if os.path.isfile(self.file_path):
file_size = os.path.getsize(self.file_path)
obj = conn.recv(4)
exist_file_size = struct.unpack('i', obj)[0]
header = {
'filename': filename,
'filemd5': Md5_func().getfile_md5(self.file_path),
'file_size': file_size
}
header_bytes = pickle.dumps(header)
conn.send(struct.pack('i', len(header_bytes)))
conn.send(header_bytes)
if exist_file_size: # 表示之前被下载过 一部分
if exist_file_size != file_size:
File_func().send_filedata(self.file_path, exist_file_size)
else:
print('\033[31;1mbreakpoint and file size are the same\033[0m')
else: # 文件第一次下载
File_func().send_filedata(self.file_path, conn)
else:
print('\033[31;1merror\033[0m')
conn.send(struct.pack('i', 0)) else:
print("\033[31;1muser does not enter file name\033[0m") def put(self, conn):
"""从client上传文件到server当前工作目录下
"""
if len(self.cmds) > 1:
obj = conn.recv(4)
state_size = struct.unpack('i', obj)[0]
if state_size:
# 算出了home下已被占用的大小self.home_bytes_size
self.current_home_size()
header_bytes = conn.recv(struct.unpack('i', conn.recv(4))[0])
header_dic = pickle.loads(header_bytes)
filename = header_dic.get('filename')
file_size = header_dic.get('file_size')
file_md5 = header_dic.get('file_md5')
self.file_path = os.path.join(os.getcwd(), filename)
if os.path.exists(self.file_path):
conn.send(struct.pack('i', 1))
has_size = os.path.getsize(self.file_path)
if has_size == file_size:
print("\033[31;1mfile already does exist!\033[0m")
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mLast time file not finished,this time continue\033[0m')
conn.send(struct.pack('i', 1))
if self.home_bytes_size + int(file_size - has_size) > self.quota_bytes:
print('\033[31;1mSorry exceeding user quotas\033[0m')
conn.send(struct.pack('i', 0))
else:
conn.send(struct.pack('i', 1))
conn.send(struct.pack('i', has_size))
with open(self.file_path, 'ab') as f:
f.seek(has_size)
File_func().write_file(conn, f, has_size, file_size)
Md5_func().verification_filemd5(self.file_path, conn, file_md5) else:
conn.send(struct.pack('i', 0))
print('\033[31;1mfile does not exist, now first put\033[0m')
if self.home_bytes_size + int(file_size) > self.quota_bytes:
print('\033[31;1mSorry exceeding user quotas\033[0m')
conn.send(struct.pack('i', 0))
else:
conn.send(struct.pack('i', 1))
with open(self.file_path, 'wb') as f:
recv_size = 0
File_func().write_file(conn, f, recv_size, file_size)
Md5_func().verification_filemd5(self.file_path,conn, file_md5) else:
print("\033[31;1mfile does not exist!\033[0m")
else:
print("\033[31;1muser does not enter file name\033[0m") def ls(self, conn):
'''查看当前工作目录下,先返回文件列表的大小,在返回查询的结果'''
print("\033[34;1mview current working directory\033[0m")
subpro_obj = subprocess.Popen('dir', shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = subpro_obj.stdout.read()
stderr = subpro_obj.stderr.read()
conn.send(struct.pack('i', len(stdout + stderr)))
conn.send(stdout)
conn.send(stderr)
print('\033[31;1mCongratulations view directory success\033[0m') def mkdir(self, conn):
'''增加目录
在当前目录下,增加目录
1.查看目录名是否已经存在
2.增加目录成功,返回 1
2.增加目录失败,返回 0'''
print("\033[34;1madd working directory\033[0m")
if len(self.cmds) > 1:
mkdir_path = os.path.join(os.getcwd(), self.cmds[1])
if not os.path.exists(mkdir_path):
os.mkdir(mkdir_path)
print('\033[31;1mCongratulations add directory success\033[0m')
conn.send(struct.pack('i', 1))
else:
print("\033[31;1muser directory already does exist\033[0m")
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m") def cd(self, conn):
'''切换目录
1.查看是否是目录名
2.拿到当前目录,拿到目标目录,
3.判断homedir是否在目标目录内,防止用户越过自己的home目录 eg: ../../....
4.切换成功,返回 1
5.切换失败,返回 0'''
print("\033[34;1mSwitch working directory\033[0m")
if len(self.cmds) > 1:
dir_path = os.path.join(os.getcwd(), self.cmds[1])
if os.path.isdir(dir_path):
# os.getcwd 获取当前工作目录
previous_path = os.getcwd()
# os.chdir改变当前脚本目录
os.chdir(dir_path)
target_dir = os.getcwd()
if self.homedir_path in target_dir:
print('\033[31;1mCongratulations switch directory success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry switch directory failed\033[0m')
# 切换失败后,返回到之前的目录下
os.chdir(previous_path)
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mSorry switch directory failed,the directory is not current directory\033[0m')
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m") def remove(self, conn):
"""删除指定的文件,或者空文件夹
1.删除成功,返回 1
2.删除失败,返回 0
"""
print("\033[34;1mRemove working directory\033[0m")
if len(self.cmds) > 1:
file_name = self.cmds[1]
file_path = os.path.join(os.getcwd(), file_name)
if os.path.isfile(file_path):
os.remove(file_path)
conn.send(struct.pack('i', 1))
elif os.path.isdir(file_path): # 删除空目录
if not len(os.listdir(file_path)):
os.removedirs(file_path)
print('\033[31;1mCongratulations remove success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry remove directory failed\033[0m')
conn.send(struct.pack('i', 0))
else:
print('\033[31;1mSorry remove directory failed\033[0m')
conn.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m")

  

main.py

# _*_ coding: utf-8 _*_
from core.user_handle import UserHandle
from core.ftp_server import FTPServer
from config import settings class Manager():
'''
主程序,包括启动server,创建用户,退出
:return:
'''
def __init__(self):
pass def start_ftp(self):
'''启动server端'''
server = FTPServer(settings.ip_port)
server.server_link()
server.close() def create_user(self):
'''创建用户,执行创建用户的类'''
username = input("\033[32;1mplease input your username>>>\033[0m").strip()
UserHandle(username).add_user() def logout(self):
'''
退出登陆
:return:
'''
print("\033[32;1m-------Looking forward to your next login-------\033[0m")
exit() def interactive(self):
'''交互函数'''
msg = '''\033[32;1m
1 启动ftp服务端
2 创建用户
3 退出
\033[0m'''
menu_dic = {
"1": 'start_ftp',
"2": 'create_user',
"3": 'logout',
}
exit_flag = False
while not exit_flag:
print(msg)
user_choice = input("Please input a command>>>").strip()
if user_choice in menu_dic:
getattr(self,menu_dic[user_choice])()
else:
print("\033[31;1myou choice doesn't exist\033[0m")

  

md5_func.py

# _*_ coding: utf-8 _*_
import hashlib
import struct from core import ftp_server
from core import file_func class Md5_func(object): def getfile_md5(self,file_path):
'''获取文件的md5'''
md5 = hashlib.md5(file_func.File_func().readfile(file_path))
print("md5是:\n",md5.hexdigest())
return md5.hexdigest() def handle_data(self):
'''处理接收到的数据,主要是将密码转化为md5的形式'''
user_dic = ftp_server.FTPServer().get_recv()
username = user_dic['username']
password = user_dic['password']
md5_obj = hashlib.md5()
md5_obj.update(password)
check_password = md5_obj.hexdigest() def verification_filemd5(self,file_path,conn,filemd5):
# 判断文件内容的md5
if self.getfile_md5(file_path) == filemd5:
print('\033[31;1mCongratulations download success\033[0m')
conn.send(struct.pack('i', 1))
else:
print('\033[31;1mSorry download failed\033[0m')
conn.send(struct.pack('i', 0))

  

user_handle.py

#_*_coding:utf-8_*_
import configparser
import hashlib
import os from config import settings class UserHandle():
'''
创建用户名称,密码
如果用户存在,则返回,如果用户不存在,则注册成功
'''
def __init__(self,username):
self.username = username
self.config = configparser.ConfigParser()
self.config.read(settings.ACCOUNTS_FILE) def password(self):
'''生成用户的密码,然后加密'''
password_inp = input("\033[32;1mplease input your password>>>\033[0m").strip()
md5_obj = hashlib.md5()
md5_obj.update(password_inp.encode('utf-8'))
md5_password = md5_obj.hexdigest()
return md5_password def disk_quota(self):
'''生成每个用户的磁盘配额'''
quota = input('\033[32;1mplease input Disk quotas>>>:\033[0m').strip()
if quota.isdigit():
return quota
else:
exit('\033[31;1mdisk quotas must be integer\033[0m') def add_user(self):
'''创建用户,存到accounts.ini'''
if not self.config.has_section(self.username):
print('\033[31;1mcreating username is :%s \033[0m' % self.username)
self.config.add_section(self.username)
self.config.set(self.username,'password',self.password())
self.config.set(self.username, 'homedir', 'home/' + self.username)
self.config.set(self.username, 'quota', self.disk_quota())
self.write_config()
self.create_userhome()
print('\033[1;32msuccessfully create userdata\033[0m')
else:
print('\033[1;31musername already existing\033[0m') def create_userhome(self):
'''创建用户的home文件夹'''
os.mkdir(os.path.join(settings.BASE_DIR, 'home', self.username)) def write_config(self):
'''写入文档'''
with open(settings.ACCOUNTS_FILE,'w') as f:
self.config.write(f) def judge_user(self):
'''判断用户是否存在'''
if self.config.has_section(self.username):
return self.config.items(self.username)

  

2,client

2.1bin

run.py

# _*_ 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 ftp_client
from config import settings
if __name__ == '__main__':
run = ftp_client.FTPClient(settings.ip_port)
run.execute()

  

2.2config

settings.py

# _*_ coding: utf-8 _*_
import os
import sys
import socket BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
# 下载的文件存放路径
down_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'download')
# 上传的文件存放路径
upload_filepath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'upload')
#绑定的IP地址
BIND_HOST = '127.0.0.1'
#绑定的端口号
BIND_PORT = 9999
ip_port = (BIND_HOST,BIND_PORT)
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM coding = 'utf-8'
listen_count = 5
max_recv_bytes = 8192
allow_reuser_address = False

  

2.3core

file_func.py

# _*_ coding: utf-8 _*_
import os
import sys
import struct
import pickle
from config import settings
from core import ftp_client
from core import md5_func
from core import progress_bar_func class File_func(object): def readfile(self,file_path):
'''读取文件'''
with open(file_path,'rb') as f:
filedata = f.read()
return filedata def appendfile_content(self,socket,file_path,temp_file_size,file_size):
'''追加文件内容'''
with open(file_path,'ab') as f:
f.seek(temp_file_size)
get_size = temp_file_size
while get_size < file_size:
res = socket.recv(settings.max_recv_bytes)
f.write(res)
get_size += len(res)
progress_bar_func.progress_bar(1,get_size,file_size) #1表示下载 def write_file(self,socket,f,get_size,file_size):
'''下载文件,将内容写入文件中'''
while get_size < file_size:
res = socket.recv(settings.max_recv_bytes)
f.write(res)
get_size += len(res)
progress_bar_func.progress_bar(1,get_size,file_size) #1表示下载 def recv_file_header(self,socket,header_size):
"""接收文件的header, filename file_size file_md5"""
header_types = socket.recv(header_size)
header_dic = pickle.loads(header_types)
print(header_dic, type(header_dic))
total_size = header_dic['file_size']
filename = header_dic['filename']
filemd5 = header_dic['filemd5']
return (filename,total_size,filemd5) def open_sendfile(self,file_size,file_path,socket,recv_size =0):
'''打开要上传的文件(由于本程序上传文件的原理是先读取本地文件,再写到上传地址的文件)'''
with open(file_path, 'rb') as f:
f.seek(recv_size)
while True:
data = f.read(1024)
if data:
socket.send(data)
obj = socket.recv(4)
recv_size = struct.unpack('i', obj)[0]
progress_bar_func.progress_bar(2, recv_size, file_size)
else:
break
success_state = struct.unpack('i', socket.recv(4))[0]
if success_state:
print('\033[31;1mCongratulations upload success\033[0m')
else:
print('\033[31;1mSorry upload directory failed\033[0m')

  

ftp_client.py

# _*_ coding: utf-8 _*_
import socket
import struct
import json
import os
import sys
import pickle
import hashlib from config import settings
from core.md5_func import Md5_func
from core.file_func import File_func
from core import progress_bar_func
class FTPClient: def __init__(self,server_address,connect = True):
self.server_address = server_address
self.socket = socket.socket(settings.address_family,settings.socket_type)
if connect:
try:
self.client_connect()
except Exception:
self.client_close() def client_connect(self):
try:
self.socket.connect(self.server_address)
except Exception as e:
print("\033[31;1merror:%s\033[0m"%e)
exit("\033[31;1m\nThe server is not activated \033[0m") def client_close(self):
self.socket.close() def get(self,cmds):
"""从server下载文件到client
"""
if len(cmds) >1:
filename = cmds[1]
self.file_path = os.path.join(settings.down_filepath, filename)
if os.path.isfile(self.file_path): #如果文件存在,支持断电续传
temp_file_size = os.path.getsize(self.file_path)
self.socket.send(struct.pack('i',temp_file_size))
header_size = struct.unpack('i',self.socket.recv(4))[0]
if header_size:
filename,file_size,filemd5 = File_func().recv_file_header(self.socket,header_size)
if temp_file_size == file_size:
print('\033[34;1mFile already does exist\033[0m')
else:
print('\033[34;1mFile now is breakpoint continuation\033[0m')
File_func().appendfile_content(self.socket,self.file_path,temp_file_size,file_size)
Md5_func().verification_filemd5(self.file_path,filemd5)
else:
print("\033[34;1mFile was downloaded before,but now server's file is not exist\033[0m")
else:#如果文件不存在,则是直接下载
self.socket.send(struct.pack('i',0))
obj = self.socket.recv(1024)
header_size = struct.unpack('i', obj)[0]
if header_size==0:
print("\033[31;1mfile does not exist!\033[0m")
else:
filename, file_size, filemd5 = File_func().recv_file_header(self.socket,header_size)
download_filepath = os.path.join(settings.down_filepath, filename)
with open(download_filepath, 'wb') as f:
get_size = 0
File_func().write_file(self.socket,f, get_size, file_size)
Md5_func().verification_filemd5(self.file_path,filemd5)
else:
print("\033[31;1muser does not enter file name\033[0m") def ls(self,cmds):
'''查看当前工作目录,文件列表'''
print("\033[34;1mview current working directory\033[0m")
obj = self.socket.recv(4)
dir_size = struct.unpack('i',obj)[0]
recv_size = 0
recv_bytes = b''
while recv_size <dir_size:
temp_bytes = self.socket.recv(settings.max_recv_bytes)
recv_bytes +=temp_bytes
recv_size += len(temp_bytes)
print(recv_bytes.decode('gbk'))
print('\033[31;1mCongratulations view directory success\033[0m') def mkdir(self,cmds):
'''增加目录
1,server返回1 增加成功
2,server返回2 增加失败'''
print("\033[34;1madd working directory\033[0m")
obj = self.socket.recv(4)
res = struct.unpack('i',obj)[0]
if res:
print('\033[31;1mCongratulations add directory success\033[0m')
else:
print('\033[31;1mSorry add directory failed\033[0m') def cd(self,cmds):
'''切换目录'''
print("\033[34;1mSwitch working directory\033[0m")
if len(cmds) >1:
obj = self.socket.recv(4)
res = struct.unpack('i', obj)[0]
if res:
print('\033[31;1mCongratulations switch directory success\033[0m')
else:
print('\033[31;1mSorry switch directory failed\033[0m')
else:
print("\033[31;1muser does not enter file name\033[0m") def remove(self,cmds):
'''表示删除文件或空文件夹'''
print("\033[34;1mRemove working directory\033[0m")
obj = self.socket.recv(4)
res = struct.unpack('i', obj)[0]
if res:
print('\033[31;1mCongratulations remove success\033[0m')
else:
print('\033[31;1mSorry remove directory failed\033[0m') def put_situation(self,file_size,condition=0):
'''上传的时候有两种情况,文件已经存在,文件不存在'''
quota_state= struct.unpack('i', self.socket.recv(4))[0]
if quota_state:
if condition:
obj = self.socket.recv(4)
recv_size = struct.unpack('i', obj)[0]
File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
else:
File_func().open_sendfile(file_size, self.file_path, self.socket, recv_size=0)
else:
print('\033[31;1mSorry exceeding user quotas\033[0m') def put(self,cmds):
"""往server端登录的用户目录下上传文件
"""
if len(cmds) > 1:
filename = cmds[1]
file_path = os.path.join(settings.upload_filepath, filename)
if os.path.isfile(file_path): # 如果文件存在,支持断电续传
self.socket.send(struct.pack('i', 1))
self.file_path = file_path
file_size = os.path.getsize(self.file_path)
header_dic = {
'filename': os.path.basename(filename),
'file_md5': Md5_func().getfile_md5(self.file_path),
'file_size': file_size
}
header_bytes = pickle.dumps(header_dic)
self.socket.send(struct.pack('i', len(header_bytes)))
self.socket.send(header_bytes)
state = struct.unpack('i', self.socket.recv(4))[0]
if state: #已经存在
has_state = struct.unpack('i', self.socket.recv(4))[0]
if has_state:
self.put_situation(file_size, 1)
else: # 存在的大小 和文件大小一致 不必再传
print("\033[31;1mfile already does exist!\033[0m")
else: # 第一次传
self.put_situation(file_size)
else: # 文件不存在
print("\033[31;1mfile does not exist!\033[0m")
self.socket.send(struct.pack('i', 0))
else:
print("\033[31;1muser does not enter file name\033[0m") def get_recv(self):
'''从client端接受发来的数据'''
return pickle.loads(self.socket.recv(settings.max_recv_bytes)) def login(self):
'''
登陆函数,当登陆失败超过三次,则退出
用户密码发送到server短
接受server端返回的信息,如果成功返回1,失败返回0
:return: 如果用户账号密码正确,则返回用户数据的字典
'''
retry_count = 0
while retry_count <3:
username = input('\033[34;1mplease input Username:\033[0m').strip()
if not username:
continue
password = input('\033[34;1mplease input Password:\033[0m').strip()
user_dic = {
'username':username,
'password':password
}
#将用户信息发送到客户端,然后接受客户端的数据
data = pickle.dumps(user_dic)
self.socket.send(pickle.dumps(user_dic))
#为了防止出现黏包问题,所以先解压报头,读取报头,再读数据
obj = self.socket.recv(4)
res = struct.unpack('i',obj)[0]
#此处,如果返回的是代码4001,则成功 4002则失败
if res:
print("\033[32;1m-----------------welcome to ftp client-------------------\033[0m")
user_info_dic = self.get_recv()
recv_username = user_info_dic['username']
return True
else:
print("\033[31;1mAccount or Passwordoes not correct!\033[0m")
retry_count +=1 def execute(self):
'''
执行,或者实施
:return:
'''
if self.login():
while True:
try:
self.help_info()
inp = input("Please input a command>>>").strip()
if not inp:
continue
self.socket.send(inp.encode(settings.coding))
cmds = inp.split()
if hasattr(self, cmds[0]):
func = getattr(self, cmds[0])
func(cmds)
break
else:
print('\033[31;1mNo such command ,please try again\033[0m')
except Exception as e: # server关闭了
print('\033[31;1m%s\033[0m'%e)
break def help_info(self):
print ('''\033[34;1m
get + (文件名) 表示下载文件
put + (文件名) 表示上传文件
ls 表示查询当前目录下的文件列表(只能访问自己的文件列表)
mkdir + (文件名) 表示创建文件夹
cd + (文件名) 表示切换目录(只能在自己的文件列表中切换)
remove + (文件名) 表示删除文件或空文件夹
\033[0m''')

  

md5_func.py

# _*_ coding: utf-8 _*_
import hashlib
from core import ftp_client
from core.file_func import File_func class Md5_func(object):
def getfile_md5(self,file_path):
'''对文件内容进行加密,也就是保持文件的一致性'''
md5 = hashlib.md5(File_func().readfile(file_path))
print("md5是:\n",md5.hexdigest())
return md5.hexdigest() def verification_filemd5(self,file_path,filemd5):
# 判断下载下来的文件MD5值和server传过来的MD5值是否一致
if self.getfile_md5(file_path) == filemd5:
print('\033[31;1mCongratulations download success\033[0m')
else:
print('\033[31;1mSorry download failed,download again support breakpoint continuation\033[0m')

  

progress_bar_fun.py

# _*_ coding: utf-8 _*_
'''进度条的表示功能'''
import sys def progress_bar(num, get_size, file_size):
float_rate = float(get_size) / float(file_size)
rate_num = round(float_rate * 100, 2)
if num == 1: # 1表示下载
sys.stdout.write('\033[31;1m\rfinish downloaded perentage:{0}%\033[0m'.format(rate_num))
elif num == 2: # 2表示上传
sys.stdout.write('\033[31;1m\rfinish uploaded perentage:{0}%\033[0m'.format(rate_num))
sys.stdout.flush()

  

基于线程开发一个FTP服务器的更多相关文章

  1. 一款基于Netty开发的WebSocket服务器

    代码地址如下:http://www.demodashi.com/demo/13577.html 一款基于Netty开发的WebSocket服务器 这是一款基于Netty框架开发的服务端,通信协议为We ...

  2. 在Linux中搭建一个FTP服务器

    在Linux中搭建一个ftp服务器,以供两个工作小组保管文件使用.禁用匿名.第一个小组使用ftp账号:ftp1,工作目录在:/var/ftp/ftp1:第二个小组使用ftp2,工作目录在:/var/f ...

  3. 基于SpringBoot开发一个Restful服务,实现增删改查功能

    前言 在去年的时候,在各种渠道中略微的了解了SpringBoot,在开发web项目的时候是如何的方便.快捷.但是当时并没有认真的去学习下,毕竟感觉自己在Struts和SpringMVC都用得不太熟练. ...

  4. Linux中搭建一个ftp服务器详解

    来源:Linux社区  作者:luzhi1024 详解Linux中搭建一个ftp服务器. ftp工作是会启动两个通道:控制通道 , 数据通道在ftp协议中,控制连接均是由客户端发起的,而数据连接有两种 ...

  5. 基于多线程多用户的FTP服务器与客户端功能实现

    项目介绍: 用户加密认证 允许同时多用户登录 每个用户有自己的家目录 ,且只能访问自己的家目录 对用户进行磁盘配额,每个用户的可用空间不同 允许用户在ftp server上随意切换目录 允许用户查看当 ...

  6. 开发一个FTP软件

    一.开发一个多并发的FTP server 需求: .允许同时支持多用户在线 .用户认证 .用户空间配额 .权限限制 .可上传下载.上传下载过程中显示进度条 .用户可远程切换目录.查看服务端文件列表等 ...

  7. 【Java TCP/IP Socket】基于线程池的TCP服务器(含代码)

    了解线程池 在http://blog.csdn.net/ns_code/article/details/14105457(读书笔记一:TCP Socket)这篇博文中,服务器端采用的实现方式是:一个客 ...

  8. Linux搭建一个FTP服务器

    1.安装vsftp 2.配置vsftpd.conf, vim /etc/vsftpd.conf 下面说说里面比较重要的选项 1 anonymous_enable=NO #不允许匿名用户 2 3 loc ...

  9. 基于SOUI开发一个简单的小工具

    基于DriectUI有很多库,比如 Duilib (免费) soui (免费) DuiVision (免费) 炫彩 (界面库免费,UI设计器付费,不提供源码) skinui (免费使用,但不开放源码, ...

随机推荐

  1. PAT乙级1003

    1003 我要通过! (20 point(s)) “答案正确”是自动判题系统给出的最令人欢喜的回复.本题属于 PAT 的“答案正确”大派送 —— 只要读入的字符串满足下列条件,系统就输出“答案正确”, ...

  2. Unity编辑器扩展-Custom List, displaying data your way

    本文转自http://catlikecoding.com/unity/tutorials/editor/custom-list/ Custom List, displaying data your w ...

  3. Python开发——13.操作系统、进程和线程

    一.操作系统 1.定义 操作系统是用来协调.管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间.操作系统运行在内核态,拥有对所有硬件的完全访问权,可以执行机器能够运行的任何指令.软件的 ...

  4. IE兼容问题 动态生成的节点IE浏览器无法触发

    ie下click()不能操作文档中没有的节点,所以你可以在click()前添加下面的语句 document.body.appendChild( input ); input.style.display ...

  5. SqlServer 连接 相关。

    sqlserver数据库连接池是一个客户端的东西.和sql server服务器无关. 各种provider默认的连接池大小不同. 比如:Ado.NET 中sqlserver 连接池默认的值是100. ...

  6. Python核心团队计划2020年停止支持Python2,NumPy宣布停止支持计划表

    Python核心团队计划在2020年停止支持Python 2.NumPy项目自2010年以来一直支持Python 2和Python 3,并且发现支持Python 2对我们有限的资源增加了负担:因此,我 ...

  7. maven项目打包成war包发布到tomcat下...

    分为两种情况: 1.当你的项目中没有前端页面时,直接右键项目--Export--选择web下的warFile--选择目录到tomcat安装目录下的webapps下即可访问. 2.当项目中有前端页面时 ...

  8. JavaScript 中的正则表达式

    1.正则表达式概述 ECMAScript 3 开始支持正则表达式,其语法和 Perl 语法很类似,一个完整的正则表达式结构如下: 1 var expression = / pattern / flag ...

  9. mysql5.6版本的优化

    1. 目标 l 了解什么是优化 l 掌握优化查询的方法 l 掌握优化数据库结构的方法 l 掌握优化MySQL服务器的方法 2. 什么是优化? l 合理安排资源.调整系统参数使MySQL运行更快.更节省 ...

  10. 《JAVASCRIPT 高级程序设计》读书笔记六 面向对象的程序设计

    一   对象属性 a.对象定义: 无序属性的集合,其属性可以包含基本值.对象或者函数: b.两种创建方式: 1.构造函数: var person = new Object(); person.name ...