Python实现基于多线程、多用户的FTP服务器与客户端功能完整实例
本文实例讲述了Python实现基于多线程、多用户的FTP服务器与客户端功能。分享给大家供大家参考,具体如下:
项目介绍:
1.用户加密认证
2.允许同时多用户登录
3.每个用户有自己的家目录,且只能访问自己的家目录
4.对用户进行磁盘配额,每个用户的可用空间不同
5.允许用户在ftpserver上随意切换目录
6.允许用户查看当前目录下文件
7.允许上传和下载文件,保证文件一致性
8.文件传输过程中显示进度条
实现的原理:
服务器端启用端口监听,并对每一连接启用一个线程,对用户登陆密码采用SHA512进行加密并进行匹配,当用户登陆成功后,实例化FTPS,并引导客户端进入主命令模式,
然后实现FTP的上传功能、下载功能、新建目录、删除文件或目录、切换目录等实例化操作,同时对相关上传下载进行进度条显示,服务器端显示下载或上传文件的大小等
客户端与服务器协商建立连接后,进行用户身份登陆,登陆成功接收服务器指令,转入命令输入窗口,同时对put与get命令进行判断,实现特定的上传与下载功能
核心代码实现如下:
服务器端
main.py
#!/usr/bin/envpython3.5
#-*-coding:utf8-*-
importos,sys,socket,pickle
BASEDIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASEDIR)
fromconfimportsetting
fromcoreimportfile_handler
fromcoreimportdb_handler
importselect,hashlib
importthreading
deflogin(username,password):
"""
FTP登陆验证函数
:paramusername:
:parampassword:
:return:
#testDict={"username":"jjb","password":"123456","file_dir":"E:\python","file_size":500}
#file='jjb.pkl'
#fp=open(file,'wb')
#pickle.dump(testDict,fp)
#fp.close()
f=open("jjb.pkl","rb")
data=pickle.loads(f.read())
f.close()
print(data)
"""
#实例化加密函数
hash=hashlib.sha512()
db=db_handler.handler(setting.DATABASE,username)
ifos.path.isfile(db):
f=open(db,"rb")
data=pickle.loads(f.read())
f.close()
ifusername==data["name"]:
hash.update(bytes(data["password"],"utf8"))
hash_pwd=hash.hexdigest()
ifhash_pwd==password:
filedir=data["file_dir"]
filesize=data["file_size"]
return"True|%s|%s"%(filedir,filesize)
else:
return"False||"
else:
return"False||"
else:
return"False||"
defprocess(conn,addr):
flage="False"
#接收客户端连接请求信息
info=conn.recv(1000)
ifinfo.decode()=="connect":
conn.send(bytes("login","utf8"))
#接收用户及密码信息
whileflage=="False":
user_check=conn.recv(8000)
#分割用户名及密码
username,password=str(user_check.decode()).split("|")
#调用登陆验证函数
login_ack=login(username,password)
flage,home,size=str(login_ack).split("|")
#print(flage,home,size)
#print("user_input:",username,"user_pass:",password)
ifflage=="True":
#登陆成功发送登陆确认信息给客户端
conn.send(bytes("login_ack","utf8"))
#实例化FTPserver
ftp=file_handler.FTPs(username,conn,home,size)#登陆用户,数据连接,工作目录,磁盘配额
ftp.run()
break
else:
#登陆失败,发送给客户端重新验证
conn.send(bytes("登陆失败!","utf8"))
defftp_server():
'''
启动FTP服务器端,开启线程监听
:return:
'''
server=socket.socket()
server.bind((setting.IP_PORT["host"],setting.IP_PORT["port"]))
server.listen(10)
whileTrue:
r,w,e=select.select([server,],[],[],1)
fori,serverinenumerate(r):
conn,addr=server.accept()
#创建线程
t=threading.Thread(target=process,args=(conn,addr))
#启动线程
t.start()
server.close()
defrun():
ftp_server()
if__name__=="__main__":
run()
file_handler.py:
#!/usr/bin/envpython3.5
#-*-coding:utf8-*-
importos,sys
BASEDIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASEDIR)
importre
fromcoreimportdb_handler
fromconfimportsetting
importpickle
classFTPs(object):
'''
ftp操作命令方法:
'''
def__init__(self,username,conn,home,total_size):
'''
初始化参数
:paramusername:操作用户名
:paramconn:sock连接
:paramhome:用户根目录
:paramtotal_size:磁盘配额
:return:
'''
self.username=username
self.conn=conn
self.root=home
self.home=self.root
self.total_size=int(total_size)
self.cmd_file=None#文件指令
self.psize=4096#文件分片
defgetdirsize(self,space):
'''
计算磁盘空间大小
:return:
'''
self.dirsize=0
forroot,dirs,filesinos.walk(space):
self.dirsize+=(sum([os.path.getsize(os.path.join(root,name))fornameinfiles])/1024)
returnint(self.dirsize)
defput(self):
'''
上传文件
:return:
'''
ifself.cmd_file:
self.user_space=int(self.getdirsize(self.root)/1024)
#组合接收字符串
self.file_root='%s\\%s'%(self.home,self.cmd_file)
##获取文件名
self.f=os.path.basename(self.file_root)
ifos.path.isdir(self.home):
os.chdir(self.home)
else:
os.makedirs(self.home)
os.chdir(self.home)
try:
self.conn.send(bytes("f_ack","utf8"))
self.size=str(self.conn.recv(1024).decode()).split("|")
ifself.size[0]=="fsize":
self.fss=int(self.size[1])
self.f_total_size=int(self.user_space+(self.fss/1024/1024))
ifself.f_total_size上传文件至服务器
get------>从服务器上下载文件
dir------>查看服务器文件列表
cd------->进入指定文件夹
delete--->删除文件
mkd----->创建目录
help----->帮助信息
q------->退出
""","utf8"))
defrun(self):
whileTrue:
#try:
##接收客户端发来的命令信息
self.cmd=self.conn.recv(1000)
self.cmd_action=str(self.cmd.decode())
#判断命令是否含有空格
self.fg=re.search("\s","%s"%self.cmd_action)
ifself.fg:
self.cmd_action,self.cmd_file=str(self.cmd_action).split("")
else:
self.cmd_file=None
#print("cmd_action:",self.cmd_action,"cmd_file:",self.cmd_file)
ifhasattr(FTPs,self.cmd_action):
func=getattr(self,self.cmd_action)
func()
continue
else:
self.conn.send(b'commandisnotfound!')
continue
#exceptExceptionasex:
#print("系统异常:%s"%ex)
客户端
client.py:
#!/usr/bin/envpython3.5
#-*-coding:utf8-*-
importsys,os,re
importsocket,hashlib
BASEDIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASEDIR)
fromcoreimportfile_handler
fromconfimportsetting
deflogin():
hash=hashlib.sha512()
whileTrue:
user_input=input("请输入用户名:").strip()
pass_input=input("请输入密码:").strip()
iflen(user_input)!=0andlen(pass_input)!=0:
hash.update(bytes(pass_input,"utf8"))
sha_pwd=hash.hexdigest()
user="%s|%s"%(user_input,sha_pwd)
returnuser
break
defftp_client():
sk=socket.socket()
sk.connect((setting.IP_PORT["host"],setting.IP_PORT["port"]))
whileTrue:
flage=False
sk.send(bytes("connect","utf8"))
msg=sk.recv(100)
print("欢迎访问FTP服务器,请根据提示进行操作")
ifmsg.decode()=="login":
whileflage==False:
login_user=login()
username,password=str(login_user).split("|")
sk.send(bytes(login_user,"utf8"))
user_info=sk.recv(1000)
ifuser_info.decode()=="login_ack":
print("登陆成功!")
flage=True
break
print(user_info.decode())
whileflage:
cmd_action=input("请输入操作命令如:getfy.pyorhelp:").strip()
iflen(cmd_action)==0:continue
ifcmd_action=="q":
sys.exit()
#判断命令是否含有空格
fg=re.search("\s","%s"%cmd_action)
iffg:
cmd,cmd_file=str(cmd_action).split("")
ftp=file_handler.ftpc(sk,username,cmd_action,setting.DATABASE["local"])
ifhasattr(ftp,cmd):
func=getattr(ftp,cmd)
func()
continue
else:
cmd_file=None
sk.send(bytes(cmd_action,"utf8"))
rec_msg=sk.recv(8000)
print(rec_msg.decode())
ifflage=="False":
sk.send(bytes("connect","utf8"))
sk.close()
defrun():
ftp_client()
if__name__=="__main__":
run()
file_handler.py:
#!/usr/bin/envpython3.5
#-*-coding:utf8-*-
importsys,os,re
importsocket
BASEDIR=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASEDIR)
classftpc(object):
def__init__(self,sk,username,cmd_action,home):
self.sk=sk
self.username=username
self.cmd_action=cmd_action
self.home=home
defput(self):
'''
上传文件
:return:
'''
try:
os.chdir(self.home)
except:
os.makedirs(self.home)
os.chdir(self.home)
#判断命令是否含有空格
fg=re.search("\s","%s"%self.cmd_action)
iffg:
self.cmd,self.cmd_file=str(self.cmd_action).split("")
ifos.path.isfile(os.getcwd()+"\\"+self.cmd_file):
self.sk.send(bytes(self.cmd_action,"utf8"))#发送动作命令
rec_msg=self.sk.recv(8000)
ifrec_msg.decode()=="f_ack":
f=open(self.cmd_file,'rb')
self.fsize=os.path.getsize(self.cmd_file)#获取要发送文件的大小
self.sk.send(bytes("fsize|%s"%self.fsize,"utf8"))#发送文件大小
self.ack=self.sk.recv(1000)
ifself.ack.decode()=="f_ack_ready":
self.fsize=int(self.fsize)
self.size=0
ack=""
whileself.size",(count),"%")
ack=self.sk.recv(1000).decode()
ifack=="ok":
print("上传成功")
else:
print("上传失败")
else:
print(self.ack.decode())
else:
print("上传文件失败:%s"%rec_msg.decode())
else:
print("上传文件失败,请输入正确的文件名!")
else:
print("上传文件失败,请输入正确的文件名!")
defget(self):
'''
下载文件
:return:
'''
try:
os.chdir(self.home)
except:
os.makedirs(self.home)
os.chdir(self.home)
#判断命令是否含有空格
fg=re.search("\s","%s"%self.cmd_action)
iffg:
self.cmd,self.cmd_file=str(self.cmd_action).split("")
else:
self.cmd_file=None
self.sk.send(bytes(self.cmd_action,"utf8"))
rec_msg=self.sk.recv(8000)
ifrec_msg.decode()=="f_ack_read":
self.rec=self.sk.send(bytes("ok","utf8"))
self.rec_size=self.sk.recv(2048)
self.ack_rec=str(self.rec_size.decode()).split("|")
self.sk.send(bytes("f_ack","utf8"))
self.ack_s=int(self.ack_rec[1])
print(self.ack_s)
self.re_s=0
f=open(self.cmd_file,"wb")
whileself.re_s",(count+1),"%")
self.sk.send(bytes("ok","utf8"))
print(self.re_s)
self.ack_ok=self.sk.recv(1024)
print("接收文件:%s"%self.ack_ok.decode())
else:
print("接收文件失败:%s"%rec_msg.decode())
如下是重要模块进行收藏:
OS模块
os.getcwd()获取当前工作目录,即当前python脚本工作的目录路径
os.chdir("dirname") 改变当前脚本工作目录;相当于shell下cd
os.curdir 返回当前目录:('.')
os.pardir 获取当前目录的父目录字符串名:('..')
os.makedirs('dirname1/dirname2') 可生成多层递归目录
os.removedirs('dirname1') 若目录为空,则删除,并递归到上一级目录,如若也为空,则删除,依此类推
os.mkdir('dirname') 生成单级目录;相当于shell中mkdirdirname
os.rmdir('dirname') 删除单级空目录,若目录不为空则无法删除,报错;相当于shell中rmdirdirname
os.listdir('dirname') 列出指定目录下的所有文件和子目录,包括隐藏文件,并以列表方式打印
os.remove() 删除一个文件
os.rename("oldname","newname") 重命名文件/目录
os.stat('path/filename') 获取文件/目录信息
os.sep 输出操作系统特定的路径分隔符,win下为"\\",Linux下为"/"
os.linesep 输出当前平台使用的行终止符,win下为"\t\n",Linux下为"\n"
os.pathsep 输出用于分割文件路径的字符串
os.name 输出字符串指示当前使用平台。win->'nt';Linux->'posix'
os.system("bashcommand") 运行shell命令,直接显示
os.environ 获取系统环境变量
os.path.abspath(path) 返回path规范化的绝对路径
os.path.split(path) 将path分割成目录和文件名二元组返回
os.path.dirname(path) 返回path的目录。其实就是os.path.split(path)的第一个元素
os.path.basename(path) 返回path最后的文件名。如何path以/或\结尾,那么就会返回空值。即os.path.split(path)的第二个元素
os.path.exists(path) 如果path存在,返回True;如果path不存在,返回False
os.path.isabs(path) 如果path是绝对路径,返回True
os.path.isfile(path) 如果path是一个存在的文件,返回True。否则返回False
os.path.isdir(path) 如果path是一个存在的目录,则返回True。否则返回False
os.path.join(path1[,path2[,...]]) 将多个路径组合后返回,第一个绝对路径之前的参数将被忽略
os.path.getatime(path) 返回path所指向的文件或者目录的最后存取时间
os.path.getmtime(path) 返回path所指向的文件或者目录的最后修改时间
sys模块
sys.argv 命令行参数List,第一个元素是程序本身路径
sys.exit(n) 退出程序,正常退出时exit(0)
sys.version 获取Python解释程序的版本信息
sys.maxint 最大的Int值
sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
sys.platform 返回操作系统平台名称
sys.stdout.write('please:')
val=sys.stdin.readline()[:-1]
re模块
匹配格式
模式
描述
^
匹配字符串的开头
$
匹配字符串的末尾。
.
匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符。
[...]
用来表示一组字符,单独列出:[amk]匹配'a','m'或'k
[^...]
不在[]中的字符:[^abc]匹配除了a,b,c之外的字符。
re*
匹配0个或多个的表达式。
re+
匹配1个或多个的表达式。
re?
匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
re{n}
re{n,}
精确匹配n个前面表达式。
re{n,m}
匹配n到m次由前面的正则表达式定义的片段,贪婪方式
a|b
匹配a或b
(re)
G匹配括号内的表达式,也表示一个组
(?imx)
正则表达式包含三种可选标志:i,m,或x。只影响括号中的区域。
(?-imx)
正则表达式关闭i,m,或x可选标志。只影响括号中的区域。
(?:re)
类似(...),但是不表示一个组
(?imx:re)
在括号中使用i,m,或x可选标志
(?-imx:re)
在括号中不使用i,m,或x可选标志
(?#...)
注释.
(?=re)
前向肯定界定符。如果所含正则表达式,以...表示,在当前位置成功匹配时成功,否则失败。但一旦所含表达式已经尝试,匹配引擎根本没有提高;模式的剩余部分还要尝试界定符的右边。
(?!re)
前向否定界定符。与肯定界定符相反;当所含表达式不能在字符串当前位置匹配时成功
(?>re)
匹配的独立模式,省去回溯。
\w
匹配字母数字
\W
匹配非字母数字
\s
匹配任意空白字符,等价于[\t\n\r\f].
\S
匹配任意非空字符
\d
匹配任意数字,等价于[0-9].
\D
匹配任意非数字
\A
匹配字符串开始
\Z
匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串。c
\z
匹配字符串结束
\G
匹配最后匹配完成的位置。
\b
匹配一个单词边界,也就是指单词和空格间的位置。例如,'er\b'可以匹配"never"中的'er',但不能匹配"verb"中的'er'。
\B
匹配非单词边界。'er\B'能匹配"verb"中的'er',但不能匹配"never"中的'er'。
\n,\t,等.
匹配一个换行符。匹配一个制表符。等
\1...\9
匹配第n个分组的子表达式。
\10
匹配第n个分组的子表达式,如果它经匹配。否则指的是八进制字符码的表达式。
正则表达式常用5种操作
re.match(pattern,string)#从头匹配
re.search(pattern,string)#匹配整个字符串,直到找到一个匹配
re.split()#将匹配到的格式当做分割点对字符串分割成列表
>>>m=re.split("[0-9]","alex1rain2jack3helenrachel8")
>>>print(m)
输出:['alex','rain','jack','helenrachel','']
re.findall()#找到所有要匹配的字符并返回列表格式
>>>m=re.findall("[0-9]","alex1rain2jack3helenrachel8")
>>>print(m)
输出:['1','2','3','8']
re.sub(pattern,repl,string,count,flag)#替换匹配到的字符
m=re.sub("[0-9]","|","alex1rain2jack3helenrachel8",count=2)
print(m)
输出:alex|rain|jack3helenrachel8
正则表达式实例
字符匹配
实例
描述
python
匹配"python".
字符类
实例
描述
[Pp]ython
匹配"Python"或"python
rub[ye]
匹配"ruby"或"rube
[aeiou]
匹配中括号内的任意一个字母
[0-9]
匹配任何数字。类似于[0123456789]
[a-z]
匹配任何小写字母
[A-Z]
匹配任何大写字母
[a-zA-Z0-9]
匹配任何字母及数字
[^aeiou]
除了aeiou字母以外的所有字符
[^0-9]
匹配除了数字外的字符
特殊字符类
实例
描述
.
匹配除"\n"之外的任何单个字符。要匹配包括'\n'在内的任何字符,请使用象'[.\n]'的模式。
\d
匹配一个数字字符。等价于[0-9]。
\D
匹配一个非数字字符。等价于[^0-9]。
\s
匹配任何空白字符,包括空格、制表符、换页符等等。等价于[\f\n\r\t\v]。
\S
匹配任何非空白字符。等价于[^\f\n\r\t\v]。
\w
匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
\W
匹配任何非单词字符。等价于'[^A-Za-z0-9_]'。
re.match与re.search的区别
re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。
PS:这里再为大家提供2款非常方便的正则表达式工具供大家参考使用:
JavaScript正则表达式在线测试工具:
http://tools.jb51.net/regex/javascript
正则表达式在线生成工具:
http://tools.jb51.net/regex/create_reg
更多关于Python相关内容可查看本站专题:《Python正则表达式用法总结》、《Python数据结构与算法教程》、《Python函数使用技巧总结》、《Python字符串操作技巧汇总》、《Python入门与进阶经典教程》及《Python文件与目录操作技巧汇总》
希望本文所述对大家Python程序设计有所帮助。