今天来讲讲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 #天呐,客户端卡住了!!
服务端:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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()