【轉】一小時學會用Python Socket 開發可併發的FTP服務器!!

一小時學會用Python Socket 開發可併發的FTP服務器!!

轉載自:http://3060674.blog.51cto.com/3050674/1687308


socket是什麼

什麼是socket所謂socket通常也稱作"套接字",用於描述IP地址和端口,是一個通信鏈的句柄。應用程序通常通過"套接字"向網絡發出請求或者應答網絡請求。說白了就是一種通信機制。它類似於銀行,電信啊這些部分的電話客服部門。你打電話的時候,那邊會分配置一個人回答你的問題,客服部門就相當於socket的服務器端了,你這邊呢就相當於客戶端了,在和你通話結束前,如果有人在想找和你通話的那個說話,是不可能的,因爲你在和他通信,當然客服部門的電話交換機也不會重複分配。我們天天用的http\smtp\ftp等網絡協議都是基於socket的上層實現,無論使用何種網絡協議,最本質上都是在進行數據的接收和發送,只不過發送的數據類型和內容不同罷了,“發送”和“接收”這兩個動作就是socket處理數據的主要方式。

socket起源於Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open–> 讀寫write/read–> 關閉close”模式來操作。Socket就是該模式的一個實現,socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/IO、打開、關閉),pythonsocket模塊是直接調用的unixsocket庫,接下來我們一起來看下,如何在python下實現socket

 

使用socket時需要指定Socket Family(地址簇),包括以下幾種:

socket.AF_UNIX       只能夠用於單一的Unix系統進程間通信

socket.AF_INET      用於主機之間的網絡通信

socket.AF_INET6    IPv6通信

若想實現主機之間的通信,我們就得使用socket.AF_INET

 

確認地址簇後,還需要指定socket 數據類型

socket.SOCK_STREAM     流式socket, for TCP

socket.SOCK_DGRAM       數據報式socket, for UDP

socket.SOCK_RAW    原始套接字,普通的套接字無法處理ICMPIGMP等網絡報文,而SOCK_RAW可以;其次,SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由用戶構造IP頭。

socket.SOCK_RDM   是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在需要執行某些特殊操作時使用,如發送ICMP報文。SOCK_RAM通常僅限於高級用戶或管理員運行的程序使用。

socket.SOCK_SEQPACKET      可靠的連續數據包服務

 

我們主要用的一般是SOCK_STREAM (for TCP)SOCK_DGRAMfor UDP.

 

 

進行socket調用時可能會用到的函數:

s = socket(family,type[,protocal])       使用給定的地址族、套接字類型、協議編號(默認爲0)來創建套接字。

 

套接字的實例具有以下方法:

  1. s.bind(address) 將套接字綁定到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。

  2. s.listen(backlog)   開始監聽傳入連接。backlog指定在拒絕連接之前,操作系統可以掛起的最大連接數量。該值至少爲1,大部分應用程序設爲5就可以了。

  3. s.connect(address)  連接到address處的套接字。一般,address的格式爲元組(hostname,port),如果連接同一臺機器上的服務器,可以將hostname設爲‘localhost’。如果連接出錯,返回socket.error錯誤。

  4. s.connect_ex(adddress)  功能與connect(address)相同,但是成功返回0,失敗返回errno的值。

  5. s.accept() 接受連接並返回(conn,address,其中conn是新的套接字對象,可以用來接收和發送數據。address是連接客戶端的地址。

  6. s.close()  關閉套接字。

  7. s.fileno()  返回套接字的文件描述符。

  8. s.getpeername() 返回連接套接字的遠程地址。返回值通常是元組(ipaddr,port)。

  9. s.getsockname()  返回套接字自己的地址。通常是一個元組(ipaddr,port)

  10. s.getsockopt(level,optname[.buflen]) 返回套接字選項的值。

  11. s.gettimeout() 返回當前超時期的值,單位是秒,如果沒有設置超時期,則返回None

  12. s.recv(bufsize[,flag])  接受套接字的數據。數據以字符串形式返回,bufsize指定要接收的最大數據量。flag提供有關消息的其他信息,通常可以忽略。

  13. s.recvfrom(bufsize[.flag])  recv()類似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。

  14. s.send(string[,flag])  string中的數據發送到連接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。

  15. s.sendall(string[,flag])  string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None,失敗則拋出異常。

  16. s.sendto(string[,flag],address)  將數據發送到套接字,address是形式爲(ipaddrport)的元組,指定遠程地址。返回值是發送的字節數。該函數主要用於UDP協議。

  17. s.setblocking(flag)  如果flag0,則將套接字設爲非阻塞模式,否則將套接字設爲阻塞模式(默認值)。非阻塞模式下,如果調用recv()沒有發現任何數據,或send()調用無法立即發送數據,那麼將引起socket.error異常。

  18. s.setsockopt(level,optname,value)   設置給定套接字選項的值。

  19. s.settimeout(timeout)   設置套接字操作的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因爲它們可能用於連接的操作(如connect())普通的非套接字實例的函數

  20. getdefaulttimeout()返回默認的套接字超時時間(以秒爲單位)。None表示不設置任何超時時間。

  21. gethostbyname(hostname)   將主機名(如“www.baidu.com”)轉換爲IPv4地址,IP地址將以字符串的形式返回,如“8.8.8.8”。不支持IPv6

  22. gethostname() 返回本地機器的主機名。

 


正式寫代碼了噢!!!


下面實現一下最簡單的socket 通信:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Server.py 服務端
 
#Echo server program
import
socket
 
HOST=‘’  #空代表0.0.0.0
PORT= 50007  #監聽端口
s= socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 生成socket tcp通信實例,
s.bind((HOST,PORT)) #綁定ip和端口,注意bind只接受一個參數,(HOST,PORT) 做成一個元祖傳進去
s.listen(1)  #開始監聽,裏面的數字是代表服務端在拒絕新連接之前可以最多掛起多少連接,不過實驗過了沒啥用,所以寫個1就好了
 
conn,addr= s.accept()  #接受連接,並返回兩個變量,conn代表每個新連接進入後服務端都會爲其生成一個新實例,後面可以用這個實例進行發送和接收,addr是連接進來的客戶端的地址,accept()方法在有新連接進入時就會返回conn,addr這兩個變量,但如果沒有連接時,此方法就會阻塞直至有新連接過來。
 
print'Connected by', addr
 
while  True:
    data = conn.recv(1024#接收1024字節數據
    if not data: break     #如果收不到客戶端數據了(代表客戶端斷開了),就斷開
    conn.sendall(data.upper())    #將收到的數據全變成大寫再發給客戶端
     
conn.close() #關閉連接


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Client.py 客戶端
 
import
socket
 
HOST= '192.168.3.1' # 遠程socket服務器ip
PORT= 50007         # 遠程socket服務器端口
 
= socket.socket(socket.AF_INET,socket.SOCK_STREAM) #實例化socket
s.connect((HOST,PORT)) #連接socket服務器
 
while True:
    msg = raw_input("Your msg::").strip() #讓用戶輸入消息,去除回車和空格
    if len(msg) == 0:continue 
     
    s.sendall(msg) #向服務器發送消息
    data= s.recv(1024)       #接收服務器的消息
     
    print 'Received:', data
s.close()


這樣我們就能實現服務端和客戶端1對1來通信了,但是你會發現,只要客戶端一中斷,服務器也跟着中斷了,這樣顯然是不合理的,一個客戶端斷開了,服務端應該能持續爲其它客戶端提供服務纔對。 那服務端爲啥會跟着客戶端一起關閉呢,因爲服務端代碼中

1
if not data: break

如果收不到客戶端的數據,就跳出循環,我們可以把代表調整成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#Echo server program
import socket
 
HOST=''  #空代表0.0.0.0
PORT= 50007  #監聽端口
s= socket.socket(socket.AF_INET,socket.SOCK_STREAM) 
s.bind((HOST,PORT)) 
s.listen(1)
 
 
while True:
    conn,addr= s.accept()  
 
    print'Connected by', addr
 
    while  True:
        data = conn.recv(1024#接收1024字節數據
        if not data: break     #如果收不到客戶端數據了(代表客戶端斷開了),就斷開
        conn.sendall(data.upper())    #將收到的數據全變成大寫再發給客戶端
 
    conn.close() #關閉此客戶端的連接實例

這樣如果一個客戶端連接斷開了,最裏面的那個循環會跳出,就又回到第一層的while循環,

1
conn,addr= s.accept()

上面的accept()方法會繼續等待一個新的連接進來,這樣,服務端就可以持續不斷的爲客戶端提供服務了。


解決了不能持續提供服務的問題後,新問題又來了,當你啓動服務端後,同時再啓動2個客戶端,你會發現,只能有一個客戶端跟服務端不斷的通信,另一個客戶端會一直處在掛起狀態,當你把可以通信的客戶端斷開後,你會發現第2個客戶端就可以跟服務端進行通信了。 這是爲什麼呢?哈哈,因爲你的服務端同時只能爲一個客戶提供服務呀。就像你跟一個人在說話的同時,是不能同時跟其它人說話的,對麼?

P_5964972_1__1802965651.jpg


其實想讓你的服務端口可以同時爲與多個客戶端進行通信也很簡單,直接用多線程併發就好了,什麼?你不會寫多線程?沒關係,Python已經幫你實現了,你只需要調用一個叫SocketServer的模塊就好了。下面咱們就把剛纔的單線程socket服務端變成多線程的:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import SocketServer
 
class MyTCPHandler(SocketServer.BaseRequestHandler):
    #繼承BaseRequestHandler基類,然後必須重寫handle方法,並且在handle方法裏實現與客戶端的所有交互
     
    def handle(self):
 
        while  True:
            data = self.request.recv(1024#接收1024字節數據
            if not data: break     
            self.request.sendall(data.upper())
 
if __name__ == "__main__":
    HOST, PORT = "localhost"50007
 
    # 把剛纔寫的類當作一個參數傳給ThreadingTCPServer這個類,下面的代碼就創建了一個多線程socket server
    server = SocketServer.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
 
    # 啓動這個server,這個server會一直運行,除非按ctrl-C停止
    server.serve_forever()


好了,此時再多啓動幾個客戶端,看看是不是所有的客戶端都能同時跟服務器端通信啦。


接下來我們看看怎麼通過socket模擬實現一個ftp server,允許客戶端上傳和下載文件。 


FTP客戶端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#_*_coding:utf-8_*_
__author__ = 'jieli'
 
 
import socket
import os
class FtpClient(object):
    def __init__(self,host,port ):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port)) #連接服務器
    def start(self): #實例化客戶端類後,需要調用此方法啓動客戶端
        self.interactive()  #跟用戶的交互都在這個方法裏
         
         
    def interactive(self):
        while True:
            user_input = raw_input(">>:").strip()
            if len(user_input) == 0:continue
            user_input = user_input.split() #用戶輸入的指令進行拆分,第一個參數是指要進行什麼動作,比如get remote_filename
 
            if hasattr(self,user_input[0]):#判斷類中是否有get或其它輸入的方法
                func = getattr(self,user_input[0]) #通過字符串獲取類中對應方法的內存對象
                func(user_input) #調用此內存對象
 
            else
                print "\033[31;1mWrong cmd usage\033[0m"
 
    def get(self,msg): #從服務器端下載文件
        print '--get func---',msg
        if len(msg) ==2 :
            file_name = msg[1]
            instruction = "FileTransfer|get|%s" % file_name #告訴服務器端要下載什麼文件
            self.sock.send(instruction)
            feedback = self.sock.recv(100#等待服務器端的消息確認
            print '-->',feedback
            if feedback.startswith("FileTransfer|get|ready"): #代表服務器上文件存在,並且服務器已經準備好了發送此文件到客戶端
                file_size = int(feedback.split("|")[-1]) # 服務器端發回來的確認消息中,最後面一個值是文件大小,必須知道文件大小才知道一共要收多少內容
                self.sock.send("FileTransfer|get|recv_ready"#告訴服務器端已經準備好了接收
                recv_size = 0 # 因爲文件可能會比較大,一次收不完,所以要循環收,每收到一次,就計個數
                = file('client_recv/%s' % os.path.basename(file_name),'wb'#在本地創建一個新文件來存這個要下載的文件內容
                print '--->',file_name
                while not file_size == recv_size:#只要文件總大小和已收到的大小不想等,就代表還沒收完
                    if file_size - recv_size>1024:#文件總大下減已收到的大小等於還剩下沒收到的大小,如果這個數大於1024,代表一次肯定收不完,那就還得多循環幾次
                        data = self.sock.recv(1024#這次收1024字節,但實際上收到的可能比1024小,所以需要以實際收到的數爲準
                        recv_size += len(data) # 已收到的大小加上這一次循環收到的實際大小
                    else:# 如果最後剩下的少於1024,那就一次性把剩下的都收過來
                        data = self.sock.recv(file_size - recv_size) 
                        #recv_size = file_size #不能這麼寫,因爲這一次依然不一定能一次性收完,因爲實際收到的數據可能比你規定的數據要少, 所以需要按下面這行的方式寫
                        recv_size += (file_size - recv_size) 
                    f.write(data) #收到的內容寫入文件
                    print file_size,recv_size
                else:
                    print '---recv file:%s---' % file_name
                    f.close() 
            else:
                print feedback
        else:
            print "\033[31;1mWrong cmd usage\033[0m"
 
    def put(self): 
        pass
    def ls(self):
        pass
    def cd(self):
        pass
    def delete(self):
        pass
 
 
if __name__ == "__main__":
    = FtpClient('localhost',9002)
    f.start()



服務器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#_*_coding:utf-8_*_
 
import SocketServer
 
import os
class MyTCPHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        while True:
            instruction = self.request.recv(1024).strip() #接收客戶端命令
            if not instruction :break
            instruction = instruction.split('|'#將客戶端發過來的消息按|來拆分,消息類似這種格式 “FileTransfer|get|file_name”
            if hasattr(self,instruction[0]): #判斷類中是否有這個方法
                func = getattr(self, instruction[0]) #獲取這個方法的內存對象
                func(instruction) #調用此方法
    def FileTransfer(self,msg): #負責文件的發送和接收
        print '---filetransfer---',msg
        if msg[1== 'get'#如果客戶端發來的指令是get,那就是下載文件
            print "client wants to download file:", msg[2]
            if os.path.isfile(msg[2]): #判斷客戶端發的文件名是否存在並是個文件
                file_size = os.path.getsize(msg[2]) # 獲取文件大小
                res = "ready|%s" % file_size # 把文件大小告訴客戶端
            else:
                res = "file doesn't exist" #文件也有可能不存在
            send_confirmation = "FileTransfer|get|%s" %res 
            self.request.send(send_confirmation) # 發送確認消息給客戶端
            feedback = self.request.recv(100#等待客戶端確認, 如果這時不等客戶端確認就立刻給客戶端發文件內容,因爲爲了減少IO操作,socket發送和接收是有緩衝區的,緩衝區滿了纔會發送,那上一條消息很有可能會和文件內容的一部分被合併成一條消息發給客戶端,這就行成了粘包,所以這裏等待客戶端的一個確認消息,就把兩次發送分開了,不會再有粘包
            if feedback == 'FileTransfer|get|recv_ready'#如果客戶端說準備好接收了
                = file(msg[2],'rb')
                send_size = 0 #發送的邏輯跟客戶端循環接收的邏輯是一樣的
                while not file_size == send_size :
                    if file_size - send_size > 1024:
                        data = f.read(1024)
                        send_size += 1024
                    else#left data less than 1024
                        data = f.read(file_size - send_size)
                        send_size +=(file_size - send_size)
                    self.request.send(data)
                    print file_size,send_size
                else:
                    print '---send file:%s done----' % msg[2]
                    f.close()
        elif msg[1== 'put':
            pass
 
 
if __name__ == '__main__':
 
    HOST, PORT = "", 9002
 
    server = SocketServer.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
 
    server.serve_forever()


好了, 這樣客戶端就能從服務器端上下載文件啦! 當然現在只要下載的功能,不過上傳、查看文件列表的業務邏輯基本是跟這個差不多的,大家可以自己進行擴展,當然,如果要真想模擬ftp的功能更全面些,還得加上用戶認證,權限認證,可切換目錄,可對用戶進行上傳空間配額,可允許傳目錄,可實現多用戶併發等更多細節功能,我的git裏有一個例子,包含了用戶認證,限制用戶只能在自己的家目錄活動,允許用戶自主切換目錄等,大家可以自己先寫,沒思路的話再參考我的代碼。 


完整代碼 :https://github.com/triaquae/py_training/tree/master/sample_code/ftp_sample 


* 注:有的同學問,這個多線程可支持多少用戶併發呢?回答是支持不了多少,當然我也沒實際測試過具體多少個,估計不會超過幾百個,因爲Python 的多線程是不能利用多核優勢的,所以我們看到的併發其實本質上還是串行的,只不是cpu不斷的在不同線程之間進行切換運行而已,但由於GIL要保證線程安全的原因,無論你生成多少個線程,Python的GIL只允許同一時間只有一個線程真正運行。如果真要實際多併發,採用異步的方式會是一個好方法,有機會我在跟大家分享下如何利用異步Select\Epoll來實際Socket的併發吧!


有興趣的同學可以加入我的Python自動化討論羣(29215534),共同學習呵呵。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章