开发一个支持多用户在线的FTP程序-------------------主要是学习思路
实现功能点
1:用户登陆验证(用户名、密码)
2:实现多用户登陆
3:实现简单的cmd命令操作
4:文件的上传(断点续传)
程序文件结构
说明:
客户端文件夹为TFTP_Client, 服务端文件夹为TFTP_Server,bin目录下的文件为启动文件。核心代码在core文件夹中,服务端home文件夹为每个账号的家目录,已登陆名为文件夹名,conf文件夹为配置文件,logger为日志文件夹(未实现)
一:启动服务端。启动文件为ftp_server.py 文件
首先将编译器定位到启动文件目录中 cd demo/tftp_server/bin(根据创建文件路径)
启动服务:python ftp_server.py start
代码:
# -*- coding: utf- -*- # 声明字符编码
# coding:utf- import os, sys # 手动添加环境变量(找到TFTP_Server这层)
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PATH) # 引入core层中main模块
from core import main if __name__ == "__main__":
# main模块调用AravHandler
main.AravHandler()
二:启动客户端。启动文件为ftp_Client.py 文件
首先定位到bin目录:cd demo/tftp_client/bin
连接服务器:python ftp_client.py -s 127.0.0.1 -P 8888 -u root -p root
看看客户端反应
客户端启动代码
# -*- coding: utf- -*- # 声明字符编码
# coding:utf-
import os, sys # 手动添加环境变量(找到TFTP_Server这层)
PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PATH) # 引入core层中main模块
from core import main if __name__ == "__main__":
# main模块调用AravHandler
main.ClientHandler()
三:服务端main.py 文件和 客户端的main.py 文件-------------(核心代码)
服务端:
# -*- coding: utf- -*- # 声明字符编码
# coding:utf-
import sys # 解析命令行参数
import optparse
import socketserver
from conf import settings
from core import MySocketServer class AravHandler(object): def __init__(self):
self.opt = optparse.OptionParser()
# options返回的是对象 args:命令参数
options, args = self.opt.parse_args()
self.verify_args(options, args) def verify_args(self, options, args):
cmd = args[] # 通过反射处理指令
if hasattr(self, cmd):
func = getattr(self, cmd)
func()
else:
print("系统暂无【%s】指令" % cmd) def start(self):
print("服务器开始启动....")
server = socketserver.ThreadingTCPServer((settings.IP, settings.PORT), MySocketServer.ServerHandler)
server.serve_forever()
服务端:MySocketServer.py 文件
# -*- coding: utf- -*- # 声明字符编码
# coding:utf- import socketserver
import json
from conf import settings
import subprocess
import configparser
import os
import struct BUFFER_SIZE = STATUS_CODE = { : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
: "Invalid cmd ",
: "Invalid auth data",
: "Wrong username or password",
: "Passed authentication",
: "Filename doesn't provided",
: "File doesn't exist on server",
: "ready to send file",
: "md5 verification", : "the file exist,but not enough ,is continue? ",
: "the file exist !",
: " ready to receive datas", : "md5 valdate success" } class ServerHandler(socketserver.BaseRequestHandler): # 读取账号配置文件进行验证
def authenticate(self, user, pwd):
conf = configparser.ConfigParser()
print("账号配置文件路径:", settings.ACCOUNT_PATH)
conf.read(settings.ACCOUNT_PATH)
# 判断当前用户是否存在
if user in conf.sections():
if conf[user]["Password"] == pwd:
self.user = user
self.file_write_path = os.path.join(settings.BASE_DIR, "home", user)
return user
# 不满足条件,函数返回None # 验证方法
def auth(self, **kwargs):
print("服务器准备验证用户信息.....")
user_name = kwargs["user"]
user_pwd = kwargs["pwd"]
print("用户输入的用户名:%s 密码:%s " % (user_name, user_pwd)) user = self.authenticate(user_name, user_pwd)
print("验证后用户名为:%s " % user)
if user:
self.send_response()
else:
self.send_response() # 响应客户端
def send_response(self, status_code):
response = {"status_code": status_code}
self.request.sendall(json.dumps(response).encode("utf-8")) def handle(self):
self.ip, self.port = self.client_address
print("客户端[%s:%s]已连接到服务器" % (self.ip, self.port))
# 处理用户发送的信息
while True:
try:
client_msg = self.request.recv(BUFFER_SIZE)
if not client_msg:
break print("客户端【%s】>>%s" % (self.client_address, client_msg))
data = json.loads(client_msg.decode('utf-8')) """
客户端与服务端通讯格式
{
"action":"执行的方法",
"user":"用户名",
"pwd":"密码”
} """
if data.get('action'): # 方法分发调用
if hasattr(self, data.get('action')):
func = getattr(self, data.get('action'))
func(**data)
else:
print("'%s' 不是内部或外部命令,也不是可运行的程序或批处理文件。" % data.get('action'))
else:
print("Invalid cmd") except Exception as e:
print(e)
break # 解析写入数据
def put(self, **kwargs):
file_name = kwargs.get("file_name")
file_size = kwargs.get("file_size")
target_path = kwargs.get("target_path")
abs_path = os.path.join(self.file_write_path, target_path, file_name)
print("文件写入路径:", abs_path) # 判断当前上传的文件服务器是否有
write_size =
if os.path.exists(abs_path):
# ===================文件在服务器存在的情况=====================
server_file_size = os.stat(abs_path).st_size
if server_file_size < file_size:
# 进行断点续传
self.request.sendall("".encode('utf-8'))
yorn = self.request.recv(BUFFER_SIZE).decode('utf-8')
if yorn == "Y":
# 继续上传
self.request.sendall(str(server_file_size).encode('utf-8'))
write_size += server_file_size
f = open(abs_path, "ab")
elif yorn == "N":
# 不续传,重新上传
f = open(abs_path, "wb") else:
# 文件存在并且大小相等提示用户即可
self.request.sendall("".encode("utf-8"))
return
else:
# ==================文件为空直接写入=========================
self.request.sendall("".encode("utf-8"))
f = open(abs_path, "wb") while write_size < file_size:
try:
data = self.request.recv(BUFFER_SIZE)
except Exception as e:
print(e)
break
f.write(data)
write_size += len(data) f.close()
print("===========文件上传完成===========") def ls(self, **kwargs):
print("接收客户端[%s:%s]命令[%s]" % (self.ip, self.port, "ls"))
# 处理执行的命令
res = subprocess.Popen("dir", shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
err = res.stderr.read()
if err:
cmd_err = err
else:
cmd_err = res.stdout.read()
# # 第一种方式:解决粘包问题
# msg_len = len(cmd_err)
# print("数据长度为:", msg_len)
# client_socket.send(str(msg_len).encode('utf-8'))
# # 马上等待回复
# is_ok = client_socket.recv(BUFFER_SIZE)
# if is_ok == b"OK":
# client_socket.send(cmd_err)
# 第二种方式:解决粘包问题
msg_len = len(cmd_err)
msg_len = struct.pack('i', msg_len)
# 下面两次发送,在客户端会当成一次接收
self.request.send(msg_len)
self.request.send(cmd_err)
# print(msg_len)
# print(cmd_err)
客户端:
# -*- coding: utf- -*- # 声明字符编码
# coding:utf- import optparse
import socket
import configparser
import json
import os
import sys
import struct # 服务队与客户端交互状态码
STATUS_CODE = { : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
: "Invalid cmd ",
: "Invalid auth data",
: "Wrong username or password",
: "Passed authentication",
: "Filename doesn't provided",
: "File doesn't exist on server",
: "ready to send file",
: "md5 verification", : "the file exist,but not enough ,is continue? ",
: "the file exist !",
: " ready to receive datas", : "md5 valdate success" } class ClientHandler(object): def __init__(self):
self.opt = optparse.OptionParser()
# # 这里有两种方式可以获取启动文件后面跟的参数 :通过索引获取。:通过optparse构建对象。
# # 第一种 获取命令列表
# print(sys.argv)
# # 第二种
self.opt.add_option("-s", "--s", dest="server")
self.opt.add_option("-P", "--P", dest="port")
self.opt.add_option("-u", "--u", dest="user")
self.opt.add_option("-p", "--p", dest="pwd")
self.options, self.args = self.opt.parse_args()
self.port_verification()
self.client_connect()
self.upload_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# print(options)
# print(args)
# cmd = sys.argv[]
# print(cmd) # 服务端应答处理
def server_answer(self):
data = self.sock.recv().decode('utf-8')
if data is not None:
data = json.loads(data)
return data # 账号发送至服务端(服务器验证账号密码)
def account_verification(self, user, pwd):
"""
客户端与服务端通讯格式
{
"action":"执行的方法",
"user":"用户名",
"pwd":"密码”
} """
data = {"action": "auth", "user": user, "pwd": pwd} self.sock.send(json.dumps(data).encode('utf-8'))
# 等待服务端回消息
response = self.server_answer()
print("服务器<<:", response)
if response["status_code"] == :
self.user = user
print("status_code<<:", STATUS_CODE[])
return True
else:
print(STATUS_CODE[response["status_code"]]) # 账号参数验证
def user_info_verification(self):
if self.options.user is None or self.options.pwd is None:
user_name = input("user: ")
user_pwd = input("pwd: ")
return self.account_verification(user_name, user_pwd)
else:
return self.account_verification(self.options.user, self.options.pwd) # 端口号校验
def port_verification(self): if int(self.options.port) > :
if int(self.options.port) < :
return True
else:
exit("端口号的取值范围因该在0-65535")
else:
exit("端口号的取值范围因该在0-65535") # 客户端连接服务器
def client_connect(self):
print("正在连接服务器....")
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.options.server, int(self.options.port))) # 交互
def interactive(self):
# 账号参数验证
if self.user_info_verification():
while True:
print("begin to interactive.......")
cmd_info = input("[%s]" % self.user).strip() # put txt.png images
cmd_list = cmd_info.split()
print("cmd 命令:", cmd_list)
if hasattr(self, cmd_list[]):
func = getattr(self, cmd_list[])
func(*cmd_list)
else:
print("'%s' 不是内部或外部命令,也不是可运行的程序或批处理文件。" % cmd_list[]) # 打印进度条 # 上传功能
def put(self, *args):
action, local_path, target_path = args
# 读取本地路径资源(默认读取TFTP_Client/files)
local_path = os.path.join(self.upload_path, "files", local_path)
print("文件读取路径:", local_path)
upload_file_size = os.stat(local_path).st_size
print("上传文件:[%s][%d]" % (os.path.basename(local_path), upload_file_size))
data = {
"action": "put",
"file_name": os.path.basename(local_path),
"file_size": upload_file_size,
"target_path": target_path } self.sock.send(json.dumps(data).encode("utf-8")) is_exit = self.sock.recv().decode('utf-8')
client_size =
if is_exit == "":
# 文件不完整
yorn = input("文件有未完成记录是否继续上传【y/n】").strip().upper()
if yorn == "Y":
# 继续上传
self.sock.sendall(yorn.encode("utf-8"))
seck_size = self.sock.recv().decode("utf-8")
client_size += int(seck_size)
elif yorn == "N":
# 不续传,重新上传
self.sock.sendall(yorn.encode("utf-8"))
elif is_exit == "":
# 文件完全存在
print("文件[%s]已存在" % os.path.basename(local_path))
return
else:
pass f = open(local_path, "rb")
f.seek(client_size)
while client_size < upload_file_size:
data = f.read()
self.sock.sendall(data)
client_size += len(data)
self.show_progress(client_size, upload_file_size) # 打印进度条
def show_progress(self, number, total):
rate = float(number) / float(total)
rate_num = int(rate * )
sys.stdout.write("%s%% %s\r" % (rate_num, "#" * rate_num)) def ls(self, *args):
data = {
"action": "ls"
}
self.sock.sendall(json.dumps(data).encode('utf-8'))
# 第二种方式:解决粘包问题
# 先接收四个字节
length_data = self.sock.recv()
content_length = struct.unpack('i', length_data)[]
print("准备接收%d大小的数据" % content_length)
recv_size =
recv_msg = b''
# 循环获取数据
while recv_size < content_length:
recv_msg += self.sock.recv()
recv_size = len(recv_msg)
print("<<%s" % (recv_msg.decode('gbk'))) client = ClientHandler()
client.interactive()
四:服务端配置文件 accounts.cfg 和 settings.py
[DEFAULT] [admin]
Password = 123
Quotation = 100 [root]
Password = root
Quatation = 100
# -*- coding: utf- -*- # 声明字符编码
# coding:utf- import os, sys
# 项目根目录
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 账号文件路径
ACCOUNT_PATH = os.path.join(BASE_DIR, "conf", "accounts.cfg") IP = "127.0.0.1"
PORT =
五:简单演示
上传文件:
断点续传:
六 总结:
整个程序就是一个服务端和客户端之间的简单通讯,通过约定好的内容来做相应事情(调用哪个方法),当客户端向服务端发送一同指令,服务端接收后通过反射来判断当前服务中又没有对应指令的方法,有则获取调用,没有就提示客户端。断点续传则是,客户端先发送这次上传的文件信息(约定格式为JSON内容 data = { "action": "put", "file_name": os.path.basename(local_path), "file_size": upload_file_size,"target_path": target_path},服务端收到后解析内容,然后判断文件在服务器这边的状态(文件已存在、文件不存在、文件存在并且大小不相等提示用户是否继续上传等)返回给客户端。客户端根据服务器返回的状态码经行相应的读取文件发送给服务端。