Python:从socket开始,搭建一个最基本功能的FTP服务器
摘要:这是一个对应客户端为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()来自:http://blog.csdn.net/whu_ksp/article/details/8562477