今天來講講ftp文件下載,感覺挺有趣的,知道吧,就那種看到新文件生成,而自己寫的代碼也不多,那種成就感!
一、需求:
客戶端發送指令給服務端,服務端根據指令找到相應文件,發送給客戶端
分析:
PS:encode() decode()默認是utf-8
Ftp server
1.讀取文件名
2.檢測文件是否存在
3.打開文件
4.檢測文件大小
5.發送文件大小給客戶端
6.等客戶端確認 #防止粘包
7.開始邊讀邊發數據
8.發送md5
客戶端的md5與服務器端的md5對比,相同即文件傳輸過程沒有改變
二、知識鋪墊
舊的知識不用就會忘記,Come On! 正式講ftp前先看下面的兩個點:
1. os.path.isfile(path)
如果path是一個存在的文件,則返回True, 否則返回False
注:運行文件是test.py, test_bb.py與test.py在同一個目錄下
import os a = os.path.isfile("E:\\hello.py") print(a) b = os.path.isfile("hello.py") print(b) c = os.path.isfile("test_bb.py") print(c)
運行結果:
True
False
True
上面是我自己測試的,可以總結一下:
os.path.isfile(path) 中path是一個路徑下的文件;也可以是與測試文件在同一目錄下的文件名
2.md5
做了下面的測試,m是md5對象,最后輸出結果是一樣的,意味着一點一點加密與一起加密最后的結果是一樣的,為什么要這么做??因為如果要傳輸的文件幾G,那肯定是一行一行傳輸的,一行一行加密的。
三、開始打碼
服務端:
1 import socket 2 import os 3 import hashlib 4
5 server = socket.socket() 6 server.bind(("localhost", 9998)) 7
8 server.listen(5) 9
10 while True: 11 conn,addr = server.accept() 12 print("new conn:", addr) 13
14 while True: 15 print("等待新指令") 16 data = conn.recv(1024) 17 if not data: 18 print("客戶端已端開") 19 break
20 cmd, filename = data.decode().split() 21 if os.path.isfile(filename): #如果是文件
22 f = open(filename, "rb") 23 m = hashlib.md5() # 創建md5對象
24 file_size = os.stat(filename).st_size #獲取文件大小
25 conn.send(str(file_size).encode()) #發送文件大小
26 conn.recv(1024) #接收客戶端確認
27 for line in f: 28 conn.send(line) #發送數據
29 m.update(line) 30 print(cmd, filename) 31 print("file md5", m.hexdigest()) 32 f.close() 33 conn.send(m.hexdigest().encode()) #發送md5
34
35 server.close()
客戶端:
1 import socket 2 import hashlib 3
4 client = socket.socket() 5
6 client.connect(("localhost", 9998)) 7
8 while True: 9 cmd = input(">>>:").strip() 10 if len(cmd) == 0: 11 continue
12 if cmd.startswith("get"): 13 client.send(cmd.encode()) #客戶端發送指令
14 receive_file_size = client.recv(1024) 15 print("server file size",receive_file_size.decode()) 16 client.send("准備好接收文件了".encode()) #客戶端發送確認
17
18 receive_size = 0 19 file_total_size = int(receive_file_size.decode()) 20 filename = cmd.split()[1] 21 f = open(filename + ".new", "wb") #新文件,沒有的話會創建
22 m = hashlib.md5() #生成md5對象
23
24 while receive_size < file_total_size: 25 data = client.recv(1024) 26 receive_size += len(data) 27 m.update(data) 28 f.write(data) #寫到文件
29 else: 30 new_file_md5 = m.hexdigest() #根據收到文件生成的md5
31 print("file recv done") 32 print("receive_size:", receive_size) 33 print("total_file_size:", file_total_size) 34 f.close() 35 receive_file_md5 = client.recv(1024) 36 print("server file md5:", receive_file_md5) 37 print("client file md5:", new_file_md5) 38
39
40 client.close()
如果看不大懂,可以先看我上一篇博文socket-ssh。
OK, 這樣就可以了,可以了嗎?嗎?廢話不多說,測試一下:
客戶端:
1 C:\Python34\python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/day9/ftp_client.py 2 >>>:get test.py 3 server file size 477 4 file recv done 5 receive_size: 477 6 total_file_size: 477 7 server file md5: b'18e84dd5d7b345db59526b1a35d07ef2' 8 client file md5: 18e84dd5d7b345db59526b1a35d07ef2 9 >>>:get tes 10 server file size 39 11 file recv done 12 receive_size: 71 13 total_file_size: 39 #天吶,客戶端卡住了!!
服務端:

C:\Python34\python3.exe C:/Users/Administrator/PycharmProjects/laonanhai/day9/ftp_server.py new conn: ('127.0.0.1', 62944) 等待新指令 get test.py file md5 18e84dd5d7b345db59526b1a35d07ef2 等待新指令 get tes file md5 c21eff88569c5ca26d8019bd891c27e9 等待新指令
四、問題分析
看了上面的例子,覺得你們應該給我一個贊,我可是搞了很久,才終於搞出一個有粘包的測試。
OK,分析一下上面的問題。
客戶端執行第一個命令是正常的,但是執行get tes就卡住了。為什么呢??再分析一下,服務端發給客戶端的文件大小是39個字節,但是客戶端收到的卻是71個字節,My God,這意味着什么?很有可能粘包不是么!啥,你們不信,好。爸爸證明給你們看:
首先,打開原tes文件:
1 rgioknjt 2 bjfoikvj 3 bkitjmbvki 4 gvbtj
再打開運行后生成的新文件tes.new:
1 rgioknjt 2 bjfoikvj 3 bkitjmbvki 4 gvbtj 5 c21eff88569c5ca26d8019bd891c27e9
什么情況?tes.new多了一行,怎么那么像md5? OK,打開服務端看下,My God!多了的一行就是服務器端(因為粘包)發給客戶端的md5,而客戶端沒有收到服務端的md5,就一直卡在 receive_file_md5 = client.recv(1024) 這里。
服務端conn.send(line),接着又send(m.hexdigest())有可能產生粘包
五、ftp優化
知道BUG所在后,怎么優化改進呢?客戶端再給服務器一次響應??但你不會覺得這樣來回交互太麻煩嗎?
接下來提供一個方法:
客戶端已經知道接收多少數據,那讓客戶端接收文件時正好接收這些數據就可以了。EG:原本是收5M,但服務端發了5.1M,多的0.1M是md5,那在循環收文件時,收到5M就不再收,循環之后再recv就是md5了。
嗯,很好,具體怎么實現呢?
對客戶端來說,只有最后一次可能粘包,EG:服務端傳文件大小是50000KB,客戶端收文件倒數直到第二次共收到49800,還剩下200,客戶端最后一次還是收1024,這時若服務器(粘包)有發多余的數據就超了,就產生粘包了!
解決方法:客戶端最后一次判斷還剩多少未接收,直接收剩下的,不再收1024!
優化代碼:
1 while receive_size < file_total_size: 2
3 if file_total_size - receive_size > 1024: #要收不止一次
4 size = 1024
5 else: #最后一次,剩多少收多少
6 size = file_total_size - receive_size 7 print("最后一次收:", size) 8 data = client.recv(size)
測試:
>>>:get tes server file size 39 最后一次收: 39 file recv done 39 39 server file md5: b'c21eff88569c5ca26d8019bd891c27e9' client file md5: c21eff88569c5ca26d8019bd891c27e9 >>>:
OK,無粘包,成功!
五、源碼:
server:

import socket import os import hashlib server = socket.socket() server.bind(("localhost", 9999)) server.listen(5) while True: conn,addr = server.accept() print("new conn:", addr) while True: print("等待新指令") data = conn.recv(1024) if not data: print("客戶端已端開") break cmd, filename = data.decode().split() if os.path.isfile(filename): #如果是文件
f = open(filename, "rb") m = hashlib.md5() # 創建md5對象
file_size = os.stat(filename).st_size #獲取文件大小
conn.send(str(file_size).encode()) #發送文件大小
conn.recv(1024) #接收客戶端確認
for line in f: conn.send(line) #發送數據
m.update(line) print("file md5", m.hexdigest()) f.close() conn.send(m.hexdigest().encode()) #發送md5
print(cmd,filename) server.close()
client:

import socket import hashlib client = socket.socket() client.connect(("localhost", 9999)) while True: cmd = input(">>>:").strip() if len(cmd) == 0: continue
if cmd.startswith("get"): client.send(cmd.encode()) #客戶端發送指令
receive_file_size = client.recv(1024) print("server file size",receive_file_size.decode()) client.send("准備好接收文件了".encode()) #客戶端發送確認
receive_size = 0 file_total_size = int(receive_file_size.decode()) filename = cmd.split()[1] f = open(filename + ".new", "wb") #新文件,沒有的話會創建
m = hashlib.md5() #生成md5對象
while receive_size < file_total_size: if file_total_size - receive_size > 1024: #要收不止一次
size = 1024
else: #最后一次,剩多少收多少
size = file_total_size - receive_size print("最后一次收:", size) data = client.recv(size) receive_size += len(data) m.update(data) f.write(data) #寫到文件
else: new_file_md5 = m.hexdigest() #根據收到文件生成的md5
print("file recv done") print(receive_size, file_total_size) f.close() receive_file_md5 = client.recv(1024) print("server file md5:", receive_file_md5) print("client file md5:", new_file_md5) client.close()