一、功能说明:

1.本程序基于socket实现客户端与服务器端的单进程交互

2.用到的用户名:whw,密码abc123——服务器端密码的验证利用hashlib模块进行MD5的编码以确保通信安全。

3.客户端登陆成功后可以查看自己再服务器上的文件夹里文件的列表;可以在自己所在的目录随意切换;可以将服务器端自己文件夹中的文件下载到客户端;可以将自己本端的文件下载到服务器端自己的文件夹里去

4.客户端上传并下载文件有日志记录

  二、目录结构

WHW_FTP
├── client
│ ├── bin #客户端入口程序目录(客户端文件保存的目录)
│ │ └── whw_client.py #客户端的入口程序
│ │
│ └── logics #配置文件目录
│ └── ftp_client.py #客户端与服务器端交互的逻辑

├── server
│ ├── bin #服务器端入口程序目录
│ │ └── whw_server.py #服务器端的入口程序
│ │
│ ├── conf #存放的是用户信息与程序用到的参数
│ │ ├── accounts.ini #客户信息
│ │ └── settings.py #程序用到的其他固定参数
│ │
│ └── core #服务器端程序的主逻辑存放地
│ │ ├── ftp_server.py #服务器端与客户端交互的程序
│ │ └── logger.py #记录日志的逻辑
│ │ └── management.py #负责处理客户端命令行参数的逻辑
│ │
│ └── home #存放的是用户目录(用户在server端的文件存放于此)
│ │ └── whw #客户文件夹
│ └── log #存放的是日志信息
│ └── whw.log #日志文件
└── README.txt

  三、程序源代码

import os
import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR) if __name__ == '__main__':
from logics import ftp_client
client = ftp_client.Ftp_client()
client.interactive()
print(BASE_DIR)

whw_client.py

import optparse
import socket
import json
import os class Ftp_client:
'''ftp客户端'''
MSG_SIZE = 1024 #消息最长1024 def __init__(self):
#初始化
self.username = None
parser = optparse.OptionParser()
parser.add_option("-s", "--server", dest="server", help="ftp server ip_addr")
parser.add_option("-P", "--port", type="int", dest="port", help="ftp server port")
parser.add_option("-u", "--username", dest="username", help="username info")
parser.add_option("-p", "--password", dest="password", help="password info")
#传参
self.options, self.args = parser.parse_args()
#检查参数是否合法
self.argv_verification()
#建立连接
self.make_connection() def argv_verification(self):
'''检查参数合法性'''
#客户端-s跟-P后面不能跟空
if not self.options.server or not self.options.port:
exit('Error:must supply server and port parameters!') def interactive(self):
"""处理与Ftpserver的所有交互"""
if self.auth():
while 1:
user_input = input('[%s]>>:' % self.username).strip()
if not user_input:
continue
# 将命令分割切片~~~
cmd_list = user_input.split()
# 反射~~~
if hasattr(self, '_%s' % cmd_list[0]):
func = getattr(self, '_%s' % cmd_list[0])
func(cmd_list[1:]) def auth(self):
"""用户认证!!!"""
count = 0
while count < 3:
username = input("username:").strip()
if not username: continue
password = input("password:").strip()
cmd = {
'action_type': 'auth',
'username': username,
'password': password
}
#给server发送客户端用户的认证信息
self.whw_sock.send(json.dumps(cmd).encode("utf-8"))
#然后接收server的反馈
response = self.get_response()
if response.get('status_code') == 200: # pass auth
self.username = username
self.terminal_display = "[%s]>>:" % self.username
self.current_dir = "\\"
return True
else:
print(response.get("status_msg"))
count += 1 def get_response(self):
"""获取服务器端返回的信息"""
data = self.whw_sock.recv(self.MSG_SIZE)
return json.loads(data.decode('utf-8')) def make_connection(self):
'''建立socket链接'''
self.whw_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#注意optparse模块得到的是两个对象,而不是字典与列表!要用对象的操作取值
self.whw_sock.connect((self.options.server,self.options.port)) def parameter_check(self,args,min_args=None,max_args=None,exact_args=None):
'''命令参数个数合法性检查'''
if min_args:
if len(args) < min_args:
print('must provide at least %s parameters,but %s received!'%(min_args,len(args)))
return False
if max_args:
if len(args)>max_args:
print('must provide at most %s parameters,but %s received!' % (max_args, len(args)))
return False
if exact_args:
if len(args)!=exact_args:
print('need exactly %s parameters,but %s received!' % (exact_args, len(args)))
return False
return True def send_msg(self,action_type,**kwargs ):
"""打包消息并发送到远程"""
msg_data = {
'action_type': action_type,
'fill':''#做成定长的
}
#把两个字典合成一个,update方法
msg_data.update(kwargs)
bytes_msg = json.dumps(msg_data).encode('utf-8')
if len(bytes_msg) < self.MSG_SIZE:
msg_data['fill'] = msg_data['fill'].zfill( self.MSG_SIZE - len(bytes_msg))
bytes_msg = json.dumps(msg_data).encode()
self.whw_sock.send(bytes_msg) def _get(self,cmd_args):
'''从ftp server下载'''
if self.parameter_check(cmd_args,min_args=1):
filename = cmd_args[0]
self.send_msg(action_type='get',filename=filename)
response = self.get_response()
if response.get('status_code') == 301:
file_size = response.get('filesize')
receive_size = 0
#进度条功能
progress_generator = self.progress_bar(file_size)
progress_generator.__next__()
#注意打开方式为wb
f = open('%s.download'%filename,'wb')
#循环接收
while receive_size < file_size:
if file_size - receive_size <8192:#last recv
data = self.whw_sock.recv(file_size-receive_size)
else:
data = self.whw_sock.recv(8192)
receive_size += len(data)
f.write(data)
#打印进度条
progress_generator.send(receive_size)
else:
print('\n')
print('---file [%s] recv done,received size [%s]---'% (filename,file_size))
f.close()
os.replace('%s.download'%filename,filename) else:
print(response.get('status_msg')) def _ls(self,args):
'''显示当前目录的文件列表'''
self.send_msg(action_type='ls')
response = self.get_response() #定长的 1024
if response.get('status_code') == 302:
cmd_result_size = response.get('cmd_result_size')
received_size = 0
cmd_resule = b''
while received_size < cmd_result_size:
#最后一次接收 小于8192
if cmd_result_size - received_size < 8192:
data = self.whw_sock.recv(cmd_result_size - received_size)
else:
data = self.whw_sock.recv(8192)
cmd_resule += data
received_size += len(data)
else:
#windows上gbk解码
print(cmd_resule.decode('gbk')) def _cd(self,cmd_args):
"""切换目录"""
#只能跟一个参数
if self.parameter_check(cmd_args, exact_args=1):
target_dir = cmd_args[0]
self.send_msg('cd',target_dir=target_dir)
response = self.get_response()
if response.get("status_code") == 350:#dir changed
self.terminal_display = "[/%s]" % response.get('current_dir')
self.current_dir = response.get('current_dir') def progress_bar(self,total_size,current_percent=0,last_percent=0):
'''进度条功能'''
while 1:
received_size = yield current_percent
current_percent = int(received_size / total_size *100)
if current_percent > last_percent:
print("*" * int(current_percent / 2) + "{percent}%".format(percent=current_percent), end='\r',
flush=True)
last_percent = current_percent # 把本次循环的percent赋值给last def _put(self,cmd_args):
"""上传本地文件到服务器"""
#先检查命令的合法性
if self.parameter_check(cmd_args, exact_args=1):
local_file = cmd_args[0]
if os.path.isfile(local_file):
total_size = os.path.getsize(local_file)
self.send_msg('put',file_size=total_size,filename=local_file)
f = open(local_file,'rb')
uploaded_size = 0
progress_generator = self.progress_bar(total_size)
progress_generator.__next__()
for line in f:
self.whw_sock.send(line)
uploaded_size += len(line)
progress_generator.send(uploaded_size)
else:
print('\n')
print('file upload done'.center(50,'-'))
f.close()

ftp_client.py

import os
import sys BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR) if __name__ == '__main__':
from core import management
argv_parser = management.Management_tool(sys.argv)
# 解析并执行指令
argv_parser.execute()

whw_server.py

[whw]
name = WangHongWei
password = e99a18c428cb38d5f260853678922e03
expire = 2019-01-01

accounts.ini

import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HOST = '0.0.0.0'
PORT = 9090
#用户的家目录
USER_HOME_DIR = os.path.join(BASE_DIR,'home')
#账户信息
ACCOUNT_FILE = os.path.join(BASE_DIR,'conf','accounts.ini')
MAX_SOCKET_LISTEN = 5
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#log文件目录
LOGGING_FILE = os.path.join(BASE_DIR,'log','whw.log')

settings.py

import socket
import json
import configparser
import hashlib
import os
import subprocess
import time
from conf import settings
from core import logger class Ftp_server:
'''处理与客户端所有交互的socket server'''
#提前定义 交互信息的状态码
STATUS_CODE = {
200: "Passed authentication!",
201: "Wrong username or password!",
300: "File does not exist !",
301: "File exist , and this msg include the file size- !",
302: "This msg include the msg size!",
350: "Dir changed !",
351: "Dir doesn't exist !",
401: "File exist ,ready to re-send !",
402: "File exist ,but file size doesn't match!",
}
#消息最长定义为1024 大于的话另做处理
# 消息长度最长为1024
MSG_SIZE = 1024 def __init__(self,management_instance):
#可以调用management的实例对象
self.management_instance = management_instance
#server启动需要的参数——从settings中取
self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.sock.bind((settings.HOST,settings.PORT))
self.sock.listen(settings.MAX_SOCKET_LISTEN)
#用户信息
self.accounts = self.load_accounts()
#存放用户的登陆信息
self.user_obj = None
#用户当前目录信息
self.user_current_dir = None def run_forever(self):
'''启动socket server'''
print('Starting whw_server on %s:%s'.center(50,'*') % (settings.HOST,settings.PORT) )
while 1:
#accept()接收client发送的指令信息
self.request,self.addr = self.sock.accept()
print('got a new connection from %s...' % (self.addr,))
try:
#所有的交互放到handle方法里
self.handle()
except Exception as e:
print('Error happened with client,close connection',e)
self.request.close() def handle(self):
'''处理与用户的所有指令的交互'''
#循环接收数据
while 1:
#服务器收到的信息
raw_data = self.request.recv(self.MSG_SIZE)
#空信息...断开链接
if not raw_data:
print('connection %s is lost...' % (self.addr,))
#删除链接信息、
del self.request,self.addr
break
data = json.loads(raw_data.decode('utf-8'))#str
action_type = data.get('action_type')
#反射 根据指令类型调用相应的方法
if action_type:
if hasattr(self,'_%s'%action_type):
func = getattr(self,'_%s'%action_type)
func(data)
else:
print('invalid command!') def load_accounts(self):
'''加载所有账号信息'''
config_obj = configparser.ConfigParser()
config_obj.read(settings.ACCOUNT_FILE)
print('所有用户名:',config_obj.sections())
return config_obj def _auth(self, data):
"""处理用户认证请求"""
print("auth ", data)
if self.authenticate(data.get('username'), data.get('password')):
print('pass auth....')
self.send_response(status_code=200)
else:
self.send_response(status_code=201) def authenticate(self,username,password):
'''用户认证方法'''
if username in self.accounts:
_password = self.accounts[username]['password']
md5_obj = hashlib.md5()
md5_obj.update(password.encode())
md5_password = md5_obj.hexdigest()
if md5_password == _password:
# 认证成功后 把用户的信息存下来
self.user_obj = self.accounts[username]
#认证成功后 把用户文件的位置存下来
self.user_obj['home'] = os.path.join(settings.USER_HOME_DIR,username)
#ls方法用到,让用户觉得切换到用户的目录了
self.user_current_dir = self.user_obj['home']
return True
else:
print('wrong username or password~')
return False
else:
print('wrong username or password~~')
return False def send_response(self,status_code,*args,**kwargs):
"""打包发送消息给客户端#用户信息状态码与状态的对应关系"""
data = kwargs
data['status_code'] = status_code
data['status_msg'] = self.STATUS_CODE[status_code]
data['fill'] = ''
bytes_data = json.dumps(data).encode('utf-8')
#制作定长的报头
if len(bytes_data) < self.MSG_SIZE:
#zfill——返回指定长度字符串,原字符串右对齐,前面填充0
data['fill'] = data['fill'].zfill(self.MSG_SIZE - len(bytes_data))
bytes_data = json.dumps(data).encode('utf-8')
#将信息发送给客户端
self.request.send(bytes_data) def _get(self,data):
'''客户端下载文件需要的方法'''
file_name = data.get('filename')
#这里需要将用户的home路径与文件名拼接,注意这里home的调用方法##############
full_path = os.path.join(self.user_obj['home'],file_name)
if os.path.isfile(full_path):
file_size = os.stat(full_path).st_size
self.send_response(301,filesize=file_size)
print('ready to send file')
#开始发送文件
f = open(full_path,'rb')
for line in f:
self.request.send(line)
else:
print('file 【%s】 send done...'%file_name)
f.close()
logger.write_logger('客户<%s>从服务器下载文件【%s】' % (self.user_obj['name'],file_name))
else:
self.send_response(300) def _ls(self,data):
'''运行ls命令并将结果返回给client'''
#运行命令并拿到结果 注意dir命令需要后面加上用户当前目录
cmd_obj = subprocess.Popen('dir %s'%self.user_current_dir,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout = cmd_obj.stdout.read()
stderr = cmd_obj.stderr.read()
cmd_result = stdout + stderr
if not cmd_result:
cmd_result = b'current dir has no file at all'
#与client交互
self.send_response(302,cmd_result_size = len(cmd_result))
self.request.sendall(cmd_result) def _cd(self,data):
"""根据用户的target_dir改变self.user_current_dir 的值"""
target_dir = data.get('target_dir')
# abspath是为了解决../..的问题
full_path = os.path.abspath(os.path.join(self.user_current_dir,target_dir) )
print("full path:",full_path)#####################################################
#检测要切换的目录是否存在
if os.path.isdir(full_path):
if full_path.startswith(self.user_obj['home']):#has permission
self.user_current_dir = full_path
relative_current_dir = self.user_current_dir.replace(self.user_obj['home'], '')
self.send_response(350, current_dir=relative_current_dir)
else:
self.send_response(351)
else:
self.send_response(351) def _put(self,data):
"""client uploads file to server"""
#客户端发过来——filename
local_file = data.get("filename")
# 文件目录
full_path = os.path.join(self.user_current_dir,local_file)
# 代表文件已存在,不能覆盖,
if os.path.isfile(full_path):
#创建文件名+时间戳
filename = "%s.%s" %(full_path,time.time())
else:
filename = full_path f = open(filename,"wb")
total_size = data.get('file_size')
received_size = 0 while received_size < total_size:
if total_size - received_size < 8192: # last recv
data = self.request.recv(total_size - received_size)
else:
data = self.request.recv(8192)
received_size += len(data)
f.write(data)
#print(received_size, total_size)
else:
print('file %s recv done'% local_file)
f.close()
logger.write_logger('客户<%s>上传文件【%s】到服务器' % (self.user_obj['name'], local_file))

ftp_server.py

import logging
import os
from conf import settings def logger_file():
#生成logger对象
whw_logger = logging.getLogger('whw.log')
whw_logger.setLevel(logging.INFO)
#生成handler对象
whw_fh = logging.FileHandler(settings.LOGGING_FILE)
whw_fh.setLevel(logging.INFO)
#生成formatter对象
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
#formatter绑定到handler对象
whw_fh.setFormatter(file_formatter)
#handler对象绑定到logger对象中
whw_logger.addHandler(whw_fh)
#返回logger对象的内存地址——
return whw_logger def write_logger(msg):
log_obj = logger_file()
log_obj.info(msg)
log_obj.handlers.pop()

logger.py

from core import ftp_server

class Management_tool:
'''负责对用户输入的指令进行解析并调用相应的模块去处理''' def __init__(self,sys_argv):
self.sys_argv = sys_argv
self.verify_argv() def verify_argv(self):
'''验证指令是否合法'''
if len(self.sys_argv) < 2:
self.help_msg()
#sys_argv[0]默认是文件名,所以取第二个才是指令
cmd = self.sys_argv[1]
#用反射判断指令是否存在
if not hasattr(self,cmd):
print('invalid argument!')
self.help_msg() def help_msg(self):
msg = '''
start start FTP server
stop stop FTP server
restart restart FTP server
createuser username create a FTP user
'''
exit(msg) def execute(self):
'''解析并执行指令'''
cmd = self.sys_argv[1]
func = getattr(self,cmd)
func() def start(self):
''' start ftp server'''
#实例化对象
server = ftp_server.Ftp_server(self)
server.run_forever()

management.py

2018-05-23 13:36:13,853 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【wanghw.txt】
2018-05-23 13:36:24,069 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【rrr.mp4】
2018-05-23 13:36:39,862 - whw.log - INFO - 客户<WangHongWei>上传文件【whw.mp4】到服务器
2018-05-23 16:38:37,041 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【rrr.mp4】
2018-05-23 16:38:50,163 - whw.log - INFO - 客户<WangHongWei>上传文件【whw.mp4】到服务器
2018-05-23 16:39:16,313 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【wanghw.txt】
2018-05-23 16:47:21,678 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【rrr.mp4】
2018-05-23 16:47:31,895 - whw.log - INFO - 客户<WangHongWei>上传文件【whw.mp4】到服务器
2018-05-23 16:47:51,453 - whw.log - INFO - 客户<WangHongWei>上传文件【eee.txt】到服务器
2018-05-23 17:11:58,296 - whw.log - INFO - 客户<WangHongWei>从服务器下载文件【wanghw.txt】
2018-05-23 17:12:05,091 - whw.log - INFO - 客户<WangHongWei>上传文件【eee.txt】到服务器

whw.log

  四、简单演示:

简单的单进程FTP服务器的实现的更多相关文章

  1. 超简单——自己搭建ftp服务器

    自己搭建ftp服务器 之所以没选择serv-u,一是因为收费,虽说网上有破解版,但是使用过程中发现破解版很不稳定,经常异常死掉,随后改选用免费的filezilla. 1软件获取 从百度搜索 FileZ ...

  2. pure-ftpd搭建简单的Ubuntu FTP服务器

    Linux下的ftpd很多,Ubuntu下常用vsftpd, proftpd和pure-ftpd,当初使用的就是proftpd. 不过前两者有个致命的问题就是内码转换,它们默认使用UTF-8编码,而W ...

  3. Linux ftp服务器部署(最简单的ftp教程)

    之前在阿里云领了一个ECS服务器(顺便说一句,白嫖的,真香~),就想着做点什么,然后试着做个 ftp 站点,因为第一次尝试,结果走了不少弯路.最后终于完成了,研究了两天(哎~,脑壳笨没办法)就想着记录 ...

  4. FTP服务器常规操作

    导读 FTP协议是Internet文件传输的基础,它是由一系列规格说明文档组成,目标是提高文件的共享性,提供非直接使用远程计算机,使存储介质对用户透明和可靠高效地传送数据.下面就由我给大家简单介绍一下 ...

  5. Docker & pure-ftpd 快速加建 FTP 服务器

    项目需要进行升级服务,现在需要基于centos 7使用docker来快速打架一个FTP环境来方便本地文件上传. 本次使用的是 pure-ftpd docker镜像,有关镜像使用的详细信息,本人是从 h ...

  6. asp.net core 简单部署之FTP配置(CentOS 7.0安装配置Vsftp服务器)

    配置过程原文地址:http://www.osyunwei.com/archives/9006.html 坑和结果 正确的跟着这个内容走,是靠谱的. 我自己给自己踩了个坑,请参照文章的朋友注意第七条:七 ...

  7. ftp服务器搭建及简单操作

    ftp服务器搭建及简单操作 1. 添加一个新用户,使用名useradd testftp,然后使用passwd testftp对新添加的用户设置密码(这里设置为“1234567”). 2. 安装ftp服 ...

  8. 转:【专题十二】实现一个简单的FTP服务器

    引言: 休息一个国庆节后好久没有更新文章了,主要是刚开始休息完心态还没有调整过来的, 现在差不多进入状态了, 所以继续和大家分享下网络编程的知识,在本专题中将和大家分享如何自己实现一个简单的FTP服务 ...

  9. 专题十二:实现一个简单的FTP服务器

    引言: 在本专题中将和大家分享如何自己实现一个简单的FTP服务器.在我们平时的上网过程中,一般都是使用FTP的客户端来对商家提供的服务器进行访问(上传.下载文件),例如我们经常用到微软的SkyDriv ...

随机推荐

  1. python猜数字(多种实现方法)

    设定一个理想数字比如:66,让用户输入数字,如果比66⼤,则显示猜测的结果⼤了:如果比66⼩,则显示猜测的结果小了;只有等于66,显示猜测结果 第一种方式(最简单的方式实现) n = 66 # 理想数 ...

  2. java 8大数据类型

    第一类:逻辑型boolean 第二类:文本型char 1.JAVA中,char占2字节,16位.可在存放汉字 2.char赋值 char a='a';  //任意单个字符,加单引号. char a=' ...

  3. 【java编程-Javassist】秒懂Java动态编程(Javassist研究)

    作者:ShuSheng007 来源:CSDN 原文:https://blog.csdn.net/ShuSheng0007/article/details/81269295 版权声明:本文为博主原创文章 ...

  4. mysqldump-1045

    mysqldump: [Warning] Using a password on the command line interface can be insecure.mysqldump: Got e ...

  5. golang fatal error: concurrent map read and map write

    调试程序的时候,为了打印map中的内容 ,直接 使用seelog 的方法打印 map中的内容到日志,结果出现 “concurrent map read and map write”的错误,导致程序异常 ...

  6. MySQL在登陆时出现ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)错误

    错误显示:ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) 解决方案: 1.找到配 ...

  7. lamp安装总结

    1.安装准备   建一个目录用于存放各软件包的压缩文件, 如我把我的源码文件都放在了 /software目录下 切换到/software目录下,执行 wget http://dev.mysql.com ...

  8. 数学的语言 化无形为可见 (Keith Devlin 著)

    第一章 数字为何靠的住 (已看) 第二章 心智的模式 (已看) 第三章 动静有数 (已看) 第四章 当数学成型 (已看) 第五章 数学揭开美之本质 (已看) 第六章 当数学到位 (已看) 第七章 数学 ...

  9. JSON字符串互相转换的三种方式和性能比较

    C# 对象与JSON字符串互相转换的三种方式 JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式. 关于内存对象和JSON字符串的相互转换, ...

  10. linux中和salt中的fqdn测试小节

    设置hosts文件和hostname文件 [root@dawn-hnyd-yd-1 ~]# cat /etc/hosts 127.0.0.1 localhost localhost.localdoma ...