摘要:这是一个对应客户端为windows资源管理器的简单FTP服务器,支持上传,下载,新建文件夹,删除,重命名,不支持用户。
题外话:我们网络设计实验要求做的客户端,题目一看错,以为要写服务端,结果辛辛苦苦写了大半之后才知道,后悔已经来不及……就只好硬着头皮先把这个做完。当时写这东西的时候找不到网上教怎么做的(目测大神们都觉得太简单……),源码倒是不少,而自己水平太低,源码基本没法看(这真不是自谦,看pyftpdlib的时候觉得那是一个天书),只好自己边研究源码边折腾。
最后做出来250行,基本功能倒也不难实现,但水平有限什么异常处理,什么库,根本不会,更不用说什么框架……
同样完成一个功能,具体下来有各种各样的实现方法,所谓提高不仅仅是会实现某个功能,还包括以更快的实现它,更成熟的代码风格,更有效的实现思路,更合理的利用已有的库和架构,这些才是一个高手和码农的差异所在。
FTP协议简介
FTP协议,File Transfer Protocol,就是有关文件传输的协议,除了传输文件(上传、下载),协议还支持在服务器进行简单的文件修改操作,如,删除,重命名,新建文件夹。使得客户访问服务器上的文件就像访问本地文件一样。同时支持用户机制,可以给不同用户不同权限。
基本流程及框架
在FTP服务器中,为了保证多用户登入,以及用户操纵不因传输数据被打断,所采用的多线程机制如下图所示
关于PORT模式和PASV模式。
这两种模式是关于传输数据时新开端口的一个约定
PORT模式约定,由客户端打开一个端口,然后在控制连接上告知服务器该端口号,服务器连接上。
PASV模式,也就是本文中所实现的模式。
1、控制连接上,客户端发送PASV命令给服务器
2、服务器开启一个端口,监听,并把该端口号返回给客户端
3、客户端连接该端口
一次完整的流程,以LIST命令为例
客户端 |
服务端 |
|
21号端口监听 |
||
发起连接请求,输入用户名 |
——》 |
|
《—— |
返回331,用户名正确 |
|
输入密码 |
——》 |
|
《—— |
返回230,密码正确 |
|
PASV命令 |
——》 |
|
《—— |
227,开启新端口,并返回端口号K |
|
此时客户端与服务器的端口K建立了数据连接 |
||
LSIT命令 |
——》 |
|
《—— |
125,数据连接已经开启 |
|
《—— |
从K端口,返回对应目录文件列表 |
|
《—— |
226,数据传输完毕 |
|
....... |
............... |
.................... |
Socket、thread、os、time简单介绍和使用
Socket:
本程序中用到的socket功能很简单,包括创建socket,监听,接受连接,连接文件化,发送接收数据,关闭连接。详看主要部件的介绍部分,或参考其他资料,这里不多说。
Thread:
本程序中只用到start_new_thread(func,(args)),就像看到的,该函数接收两个参数,一个是希望新线程中执行的函数,还有就是希望给该函数传入的参数。
这是一个轻量级的开启线程的方法,更好的做法是继承线程类,把一个类做成线程,有自己的资源可以访问。
Os:
本文中主要涉及到的是,os.chdir(),os.getcwd(),os.mkdir()等等,这些关于变更目录的操作
Time:
参考了pyftpdlib中对时间的处理,不多,建议参考介绍时间库的文章详看。
实现思路
首先创建一个主socket绑定到21端口。
然后以一个while循环接受用户的连接,每接受一个连接就开启一个新线程与该用户交互。
在线程中又以一个while循环来接收用户命令,每接到一条命令,就对用户的命令和传递参数进行解析,并调用对应的handler函数处理。
如遇到pasv操作,则在handler_pas中建立新的socket并返回给客户端。
大致思路如此。详看代码部分。
代码部分
#-*-coding:utf8-*-
import time
import socket,sys
from thread import *
import os
__author__ = 'ksp'
class FTPs():
def __init__(self,localip='127.0.0.1',path='c:/'):#接受本机ip以绑定socket,接受开放的目录
self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
#这是传输时分片的大小
self.PSIZE=4096
self.lip=localip
#绑定到FTP专用端口,21
try:
self.s.bind((self.lip,21))
except:
print 'ip error'
raise
self.path=path
#将工作目录改变到所设目录下
try:
os.chdir(path)
except ValueError:
print 'path Invalid'
raise
self.close=False
#文件属性中的日期时会用到
self._months_map = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul',
8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
def Run(self):#主函数,用于循环监听用户的登入请求
self.s.listen(1)
#self.s.settimeout(60)
print 'socket created server running...'
#设置之后创建的socket都只存活60秒,防止异常时卡死
socket.setdefaulttimeout(60)
while 1:
try:
conn,addr=self.s.accept()
#停止服务器
except KeyboardInterrupt:
self.close=True
conn.close()
self.s.close()
print 'KeyboardInterrupt'
break
print 'connect with '+addr[0]+':'+str(addr[1])
#开启新线程用于与该用户交互
start_new_thread(self.cftpcmd,(conn,))
def cftpcmd(self,cnn,):#与用户交互的主函数
cpath=self.path.replace('\\','//')
os.chdir(cpath)
#连接文件化,以访问文件的方式访问socket
cf=cnn.makefile('rw',0)
cf.write('220 ready for transfer\r\n')
print 'thread open and connected...'
#无用户验证机制,在此接受用户
print cf.readline().strip()
cf.write('331 name ok\r\n')
print cf.readline().strip()
cf.write('230 log in ok\r\n')
#保存用于数据传输的连接
dsocket=None
#用于处理用户请求关闭连接
selfclose=False
while 1:
#获取用户提交的命令和参数
try:
gets=cf.readline().strip()
if self.close or selfclose:
break
except:
print '\r\ntimeout exit thread'
cnn.close()
break
print 'receive command: "%s"'% gets
cmd=gets[:3].lower()
args=gets[3:]
#解析命令,使用对应的函数处理。以eval方式是为了在多个命令需要的处理函数相似的情况下简化
try:
if cmd in ['lis',]:
ev='self.handle_%s(dsocket,cf)' % (cmd)
print ev
eval(ev)
elif cmd=='qui':
selfclose=self.handle_qui(cf)
elif cmd=='ret':
cf.write('125 dataconnection open\r\n')
start_new_thread(self.handle_ret,(args,cf,dsocket))
elif cmd=='sto':
cf.write('150 file status ok\r\n')
start_new_thread(self.handle_sto,(args,cf,dsocket))
elif cmd=='pas':
ev='self.handle_%s("%s",cf)' % (cmd,args)
print ev
dsocket,psocket=eval(ev)
elif cmd=='rnf':
cf.write('350 ready for destination name\r\n')
oldename=args[2:]
elif cmd=='rnt':
cf.write('250 rename ok\r\n')
newname=args[2:]
try:
os.rename(oldename,newname)
except:
print 'rename error'
elif hasattr(self,'handle_%s'% cmd):
ev='self.handle_%s("%s",cf)' % (cmd,args)
print ev
eval(ev)
else:
cf.write('501 Invaild command\r\n')
print 'no handler for this command..'+'self.handle_%s("%s",cf)' % (cmd,args)
except:
print 'error...closing thread and conn'
if dsocket != None:
dsocket.close()
psocket.close()
cf.write('221 goodbye..\r\n')
cf.close()
cnn.close()
exit_thread()
print 'main thread exit'
cnn.close()
def handle_user(self,args,cf):
cf.write('331 username ok\r\n')
print '331 ok'
def handle_pass(self,args,cf):
cf.write('230 log in ok\r\n')
print '230 ok'
def handle_cwd(self,args,cf):#CWD函数,还包含了当目录不存在时创建目录的功能
try:
os.chdir(args[1:])
except:
print 'dir does not exit,make it'
os.mkdir(args[1:])
os.chdir(args[1:])
cf.write('250 "%s" is current directory\r\n'% os.getcwd())
print 'cwd'
def handle_pwd(self,args,cf):
cf.write('257 "%s" is current directory\r\n'% os.getcwd()[len(self.path)-1:].replace('\\','/'))
print 'pwd'
def handle_lis(self,ppsock,cf):#LIST函数,用于返回用户请求的目录下的文件列表
cf.write('125 Data connection already open \r\n')
res=''
for afile in os.listdir(os.getcwd()):
fpath=os.getcwd()+'\\'+afile
#文件的修改时间需要进行相应的格式化
tstr=self.format_time(fpath)
if os.path.isfile(fpath):
#获取文件大小
size=os.path.getsize(fpath)
res+= '-rw-rw-rw- 1 owner group %s %s %s\r\n' % (size,tstr,afile)
else:
res+= 'drwxrwxrwx 1 owner group 0 %s %s\r\n' % (tstr,afile)
print res
ppsock.send(res)
cf.write('226 transfer complete\r\n')
ppsock.close()
def handle_pas(self,args,cf):#进入PASV模式,返回一个用于传输数据的socket
psock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
psock.bind((self.lip,0))
pport=psock.getsockname()[1]
psock.listen(1)
cf.write('227 entering pasv mode (%s,%s,%s).\r\n' % (psock.getsockname()[0],pport//256,pport%256))
ppsock,addr=psock.accept()
print 'enter pasv mode port %s...'%pport
return [ppsock,psock]
def handle_typ(self,args,cf):
cf.write('200 \r\n')
print 'type a'
def handle_qui(self,cf):
cf.write('200 \r\n')
print 'quit...'
return True
def handle_noo(self,args,cf):
args=args[2:]
cf.write('200 \r\n')
print 'noop'
def handle_siz(self,args,cf):
filename=args[2:]
print filename
size=os.path.getsize(os.getcwd()+'\\'+filename)
cf.write('%s %s\r\n'%(213,size))
def handle_por(self,args,cf):#port mode pass
args=args[2:]
cf.write('200 \r\n')
print 'enter port mode'
def handle_ret(self,args,cf,psock):#RET命令,用于下载文件
try:
tpath=os.getcwd()+'\\'+args[2:]
print 'ret transfering now...path:%s'%tpath
f=open(tpath,'rb')
#对文件进行分片传输
while True:
data=f.read(self.PSIZE)
if not data:
break
psock.send(data)
cf.write('226 ok\r\n')
print 'transport completed..'
psock.close()
except:
print 'ret error...'
cf.write('226 ok\r\n')
psock.close()
exit_thread()
def handle_sto(self,args,cf,psock):#STO命令,用于上传文件
try:
fname=os.getcwd()+'\\'+args[2:]
f=open(fname,'wb')
print 'make file ok'
buf=psock.recv(self.PSIZE)
while len(buf)==self.PSIZE:
f.write(buf)
buf=psock.recv(self.PSIZE)
cf.write('226 transfer complete\r\n')
f.write(buf)
f.close()
psock.close()
except:
print 'error in sto'
psock.close()
exit_thread()
def handle_mkd(self,args,cf):
cf.write('257 %s dir created\r\n'%args)
try:
os.mkdir(args[1:])
except:
print 'mkdir error'
def handle_del(self,args,cf):
cf.write('250 file removed\r\n')
fname=os.getcwd()+'\\'+args[2:]
try:
os.remove(fname)
except:
print 'dele error'
def handle_rmd(self,args,cf):
cf.write('250 dir remove\r\n')
try:
os.rmdir(args[1:])
except:
print 'remove dir error'
def format_time(self,file):#时间格式化
raw_ftime=os.stat(file).st_mtime
mtime=time.localtime(raw_ftime)
now=time.time()
if now-raw_ftime>180*24*60*60:
tstr='%d %Y'
else:
tstr='%d %H:%M'
res='%s %s'%(self._months_map[mtime.tm_mon],time.strftime(tstr,mtime))
return res
def handle_sys(self,args,cf):
cf.write('215 UNIX Type:L8\r\n')
print 'syst'
if __name__=='__main__':
abc=FTPs('192.168.8.100','e:/')
abc.Run()