今天来讲讲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()