您的位置:首页 > 编程语言 > Python开发

Python:从socket开始,搭建一个最基本功能的FTP服务器(附源码)

2013-02-01 11:47 906 查看
摘要:这是一个对应客户端为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()
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐