题目:IO多路复用版FTP
需求:
实现文件上传及下载功能
支持多连接并发传文件
使用select or selectors
前言:
实现多并发的方式,可以使用多进程,多线程,协程。
多进程启动慢,耗资源;多线程存在并发修改同一份数据死锁问题;协程实际上是单线程,无法利用多核资源,一遇到阻塞会将整个任务都阻塞,但并发数最高。
由于IO任务不耗CPU资源,CPU密集型任务适合使用多进程,IO密集型任务适合使用多进程,也可使用协程,没有多线程的死锁问题,但是代码会更复杂。
select、selectors是网络编程中的I/O multiplexing模块,实现了协程机制,通过轮询调度任务,可以达到单线程多并发操作。
selectors基于select,这里使用selectors来构建FTP服务器
设计说明:
1、server工作在non-blocking模式,client工作在blocking模式。
2、socket默认是blocking模式,send和recv操作在数据发送或者接收完成前会一直处于等待状态。而在non-blocking模式下,程序一遇到堵塞直接抛出BlockingIOError错误,server利用这个exception退出当前任务,再通过selectors调度执行其他任务,实现伪多并发操作。
3、selectors轮询调度任务需要先注册事件,命令是register(sock,EVENT,method),当sock激活时,自动执行method。若要改变sock对应事件,使用modify(sock,EVENT,new_method),不能使用同一个sock注册不同的事件。
4、EVENT分selectors.EVENT_READ和selectors.EVENT_WRITE,EVENT_READ需要client端有请求过来才会被激活,EVENT_WRITE是server端在轮询到该任务时会主动去探测client端口是否可写,需要区分正确。
5、register(sock,EVENT,method)如果方法中需要传入参数,将参数存放在一个字典里,要用的时候从里面取出来,用完后删除。(我想应该还可以使用(sock,EVENT,method(sock,args*))方式,但未验证)
6、non-blocking模式下,无法保证send的数据完整全面,send的过程中遇到堵塞,在缓存中被发送出去的数据可能只是一部分,切换到其他任务时缓存中的数据被清空。为保证传输文件的完整全面,利用conn.send()会返回成功发送字节数,通过累计这个值,定位下次文件读取的cursor位置。
server端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import selectors
import socket
import json,os
class FtpServer(object):
def __init__(self,sock):
'''
:param upload_dict 用于存放上传文件状态信息参数,在read()中有详细说明
:param download_dict 用于存放下载文件状态信息参数,在read()中有详细说明
'''
self.sel = selectors.DefaultSelector()
self.sock = sock
self.upload_dict = {}
self.download_dict = {}
def start(self):
'''
:param key.data 指的是调用的方法
:param key.fileobj 指的是传入的sock
:param mask 无引用或指向,无实际意义。
'''
self.sock.listen(10)
self.sock.setblocking(False)
self.sel.register(sock, selectors.EVENT_READ, self.accept)
while True:
events = self.sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj)
def accept(self,sock):
conn, addr = sock.accept()
print('accepted', conn, 'from', addr)
conn.setblocking(False)
self.sel.register(conn, selectors.EVENT_READ, self.read)
def read(self,conn):
'''
判断输入的命令,然后将sock状态设置为读或写,直到任务结束再由任务自己调整回来
将文件读写open操作放在这里,是为了避免因BlockingIOError多次打开和关闭文件
:param upload_dict = {conn:{ "motion": "get",
"filename": filename,
"filesize":0,
"md5":md5,
"recv_size":recv_size, 已接收的字节数
"wfd":fd 打开用于写入的文件句柄
}}
:param download_dict = {conn:{ "motion": "get",
"filename": filename,
"filesize":0,
"md5":md5,
"send_size":send_size, 作为传输中断后再次传输时,cursor的锚位
"rfd":fd 打开用于读的文件句柄
}}
'''
try:
data = conn.recv(1024)
print("read:", conn,"data:",data)
if data:
try:
msg = json.loads(data.decode("utf-8"))
#print("msg:",msg,"from conn:",conn)
motion = msg["motion"]
filename = msg["filename"]
if "put" == motion:
msg["recv_size"] = 0
wfd = open(filename, "wb")
msg["wfd"] = wfd
self.upload_dict[conn]=msg
conn.send(b'ready to receive')
self.sel.modify(conn, selectors.EVENT_READ, self.upload)
elif "get" == motion:
filesize = os.path.getsize(filename)
filemd5 = os.popen('md5sum %s' % filename).read().split()[0]
msg["filesize"] = filesize
msg["md5"] = filemd5
print(msg)
conn.send(json.dumps(msg).encode("utf-8"))
msg["send_size"] = 0
rfd = open(filename, "rb")
msg["rfd"] = rfd
self.download_dict[conn] = msg
self.sel.modify(conn, selectors.EVENT_WRITE, self.download)
else:
print('func matching miss:',msg)
except Exception as e:
print('error:',e)
conn.send(data)
else:
print('closing:', conn,'data:',data)
self.sel.unregister(conn)
conn.close()
except ConnectionResetError:
print('client error,closing.', conn)
self.sel.unregister(conn)
conn.close()
def upload(self,conn):
'''
非阻塞模式下,server在遇到等待,就会直接报出BlockingIOError: [Errno 11] Resource temporarily unavailable,利用该报错退出当前任务
文件在writing的过程中可能会因BlockingIOError中断多次,每次中断时都将当期状态信息保存在upload_dict中
'''
filename = self.upload_dict[conn]["filename"]
filesize = self.upload_dict[conn]["filesize"]
f = self.upload_dict[conn]["wfd"]
recv_size = self.upload_dict[conn]["recv_size"]
while filesize > recv_size:
if filesize - recv_size > 1024:
n = 1024
else:
n = filesize - recv_size
try:
r_data = conn.recv(n)
f.write(r_data)
recv_size += len(r_data)
self.upload_dict[conn]["recv_size"] = recv_size
if filesize == recv_size:
f.close()
filemd5 = os.popen('md5sum %s' % filename).read().split()[0]
print("receive complete! filename:",filename,"filesize", filesize,"recv_filemd5:",filemd5,"original_filemd5:",self.upload_dict[conn]["md5"])
del self.upload_dict[conn]
self.sel.modify(conn, selectors.EVENT_READ, self.read)
except BlockingIOError:
break
def download(self,conn):
'''
non-blocking模式下,发送一旦遇到exception,send操作可能只发送部分数据出去,不能用len(line)来统计发送的字节数,即使是放在send之后统计
:param sendsize 发送出去的数据字节
:return 遇到IO等待事件,让出队列,让其他任务执行
:exception BlockingIOError IO遇到堵塞报的错,退出当前任务
:exception ConnectionResetError 客户端断开连接报的错,执行收尾工作
'''
f = self.download_dict[conn]["rfd"]
f.seek(self.download_dict[conn]["send_size"])
try:
while self.download_dict[conn]["send_size"] < self.download_dict[conn]["filesize"]:
for line in f:
sendsize=conn.send(line) ####这步非常重要,sendsize不一定等于len(line)
self.download_dict[conn]["send_size"] += sendsize
if self.download_dict[conn]["send_size"] == self.download_dict[conn]["filesize"]:
f.close()
del self.download_dict[conn]
print("sending complete!")
self.sel.modify(conn, selectors.EVENT_READ, self.read)
break
except BlockingIOError:
return
# except ConnectionResetError:
# print("lose connection:",conn)
# f.close()
# del self.download_dict[conn]
# self.sel.modify(conn, selectors.EVENT_READ, self.read)
if __name__ =="__main__":
sock = socket.socket()
sock.bind(('0.0.0.0',9999))
s = FtpServer(sock)
s.start()
client端
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import sys
import os
import json
def ProcessBar(part, total): ####进度条模块,只能在Linux下使用,并且窗口宽度要足够宽,否则会变成多行显示
if total != 0:
i = round(part * 100 / total)
sys.stdout.write(
'[' + '>' * i + '-' * (100 - i) + ']' + str(i) + '%' + ' ' * 3 + str(part) + '/' + str(total) + '\r')
sys.stdout.flush()
class FtpClient(object):
def __init__(self,conn):
self.conn = conn
def get(self,filename):
msg_send = {"motion": "get",
"filename": filename,
"filesize":0,
"md5":''}
self.conn.send(json.dumps(msg_send).encode("utf-8"))
msg_recv = self.conn.recv(1024)
msg=json.loads(msg_recv.decode("utf-8"))
filesize = msg["filesize"]
filemd5 = msg["md5"]
recv_size = 0
f = open(filename,"wb")
while filesize > recv_size:
if filesize - recv_size > 1024:
n = 1024
else:
n = filesize - recv_size
r_data = self.conn.recv(n)
f.write(r_data)
recv_size += len(r_data)
ProcessBar(recv_size, filesize)
f.close()
recv_filemd5 = os.popen('md5sum %s' % filename).read().split()[0]
print("filename:", filename, "filesize", filesize, "recv_size:", recv_size,"recv_filemd5:",recv_filemd5,"original_filemd5:",filemd5)
def put(self,filename):
filesize = os.path.getsize(filename)
filemd5 = os.popen('md5sum %s' % filename).read().split()[0]
msg = {"motion":"put",
"filename":filename,
"filesize":filesize,
"md5":filemd5}
self.conn.send(json.dumps(msg).encode("utf-8"))
self.conn.recv(1024) ####防止粘包信号
sendsize = 0
f = open(filename, "rb")
for line in f:
self.conn.send(line)
sendsize += len(line)
ProcessBar(sendsize, filesize)
f.close()
print(filename," transfer complete!")
def interactive(self):
while True:
cmd = input(">>>:").strip()
if len(cmd) == 0:
continue
elif cmd == "exit":
exit()
else:
motion = cmd.split()[0]
if hasattr(self,motion):
filename = cmd.split()[1]
func = getattr(self,motion)
func(filename)
else:
self.conn.send(json.dumps(cmd).encode("utf-8"))
msg = self.conn.recv(1024)
print("Server feedback,command error:",json.loads(msg.decode("utf-8")))
if __name__ == "__main__":
sock = socket.socket()
sock.connect(('127.0.0.1',9999))
c = FtpClient(sock)
c.interactive()
转载于:https://blog.51cto.com/tryagaintry/2060444