Python:從socket開始,搭建一個最基本功能的FTP服務器

jopen 12年前發布 | 35K 次閱讀 Python Python開發

摘要:這是一個對應客戶端為windows資源管理器的簡單FTP服務器,支持上傳,下載,新建文件夾,刪除,重命名,不支持用戶。

題外話:我們網絡設計實驗要求做的客戶端,題目一看錯,以為要寫服務端,結果辛辛苦苦寫 了大半之后才知道,后悔已經來不及……就只好硬著頭皮先把這個做完。當時寫這東西的時候找不到網上教怎么做的(目測大神們都覺得太簡單……),源碼倒是不 少,而自己水平太低,源碼基本沒法看(這真不是自謙,看pyftpdlib的時候覺得那是一個天書),只好自己邊研究源碼邊折騰。

最后做出來250行,基本功能倒也不難實現,但水平有限什么異常處理,什么庫,根本不會,更不用說什么框架……

同樣完成一個功能,具體下來有各種各樣的實現方法,所謂提高不僅僅是會實現某個功能,還包括以更快的實現它,更成熟的代碼風格,更有效的實現思路,更合理的利用已有的庫和架構,這些才是一個高手和碼農的差異所在。

FTP協議簡介

FTP協議,File Transfer Protocol,就是有關文件傳輸的協議,除了傳輸文件(上傳、下載),協議還支持在服務器進行簡單的文件修改操作,如,刪除,重命名,新建文件夾。使得客戶訪問服務器上的文件就像訪問本地文件一樣。同時支持用戶機制,可以給不同用戶不同權限。

基本流程及框架

FTP服務器中,為了保證多用戶登入,以及用戶操縱不因傳輸數據被打斷,所采用的多線程機制如下圖所示

Python:從socket開始,搭建一個最基本功能的FTP服務器

關于PORT模式和PASV模式。

這兩種模式是關于傳輸數據時新開端口的一個約定

PORT模式約定,由客戶端打開一個端口,然后在控制連接上告知服務器該端口號,服務器連接上。

PASV模式,也就是本文中所實現的模式。

1、控制連接上,客戶端發送PASV命令給服務器

2、服務器開啟一個端口,監聽,并把該端口號返回給客戶端

3、客戶端連接該端口

一次完整的流程,以LIST命令為例

</tr>

</tr>

</tr>

</tr>

客戶端

</td>


服務端

</td> </tr>



21號端口監聽

</td> </tr>

發起連接請求,輸入用戶名

</td>

——》

</td>



《——

</td>

返回331,用戶名正確

</td> </tr>

輸入密碼

</td>

——》

</td>



《——

</td>

返回230,密碼正確

</td> </tr>

PASV命令

</td>

——》

</td>



《——

</td>

227,開啟新端口,并返回端口號K

</td> </tr>

此時客戶端與服務器的端口K建立了數據連接

</td> </tr>

LSIT命令

</td>

——》

</td>



《——

</td>

125,數據連接已經開啟

</td> </tr>

《——

</td>

K端口,返回對應目錄文件列表

</td> </tr>


《——

</td>

226,數據傳輸完畢

</td> </tr>

.......

</td>

...............

</td>

....................

</td> </tr> </tbody> </table>

Socketthreadostime簡單介紹和使用

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()</pre>

來自:http://blog.csdn.net/whu_ksp/article/details/8562477

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色