前言
因为想要做一个能够满足多用户同时上传/下载文件的功能,并且带有权限功能,网上查了一些资料,尝试了用HTTP协议传输文件,用socket异步IO,多进程传输等方案,从中也学到了很多的东西,这里所述就是使用selectors
和socket
模拟FTP协议实现文件的传输,最多可以同时满足10个传输链接。
1.HTTP传输
因为用Django,Flask,Tornado
等web框架可以轻松实现文件的上传/下载,这种方法无疑是最容易实现的,我是使用Django
做过尝试,但这种方案的问题在于:
- 大文件传输过程中可能链接中断
- 文件续传的实现比较复杂
- 大文件一个链接占用的时间过长
对于大文件上传时可以通过分包的方式上传,让每个包的大小适中,但是HTTP协议毕竟无状态,如果分包,后台任然需要对其进行整合,所以需要频繁的记录传输进度。尝试中文件大小超过20M就可能再传输过程中出现异常(尽管超过20M的可能性很小),从这里引发了思考,想通过别的方式来实现。
2.FTP传输
FTP专为传输文件而生,与HTTP不同,一次传输需要两个TCP链接,一个是命令链接,一个是传输链接,主动模式下,在命令链接进行权限确认等工作,完成后打开传输链接端口并告诉客户端传输链接所在端口,客户端链接传输端口,开始数据传输,这里最大的问题就在与并发问题,命令链接可以用异步IO的方式实现并发,但是传输过程是一对一的,所以要实现多个链接同时上传/下载就需要开启多个端口,小应用还好,大应用就头疼了。被动模式下传输端口由客户端决定,服务器去链接客户端规定的端口,切不说客户端防火墙的问题,这种客户端监听服务器的模式本来就存在很大的问题,但是解决了并发问题。
还有一点我觉得安全问题,感觉打开数据连接时可能会被别的链接窃取,当然也可以在数据链路加入一些认证方法。
3.多线程,多进程的异步方式和异步IO
多线程/多进程 简单粗暴,每个链接fork一个子进程或者开启一个子线程来处理,但是这种方法会浪费很多资源在监听链接请求上,并且进程或线程过多,CPU切换线程/进程开销就会加大很多。
异步IO 使用select,poll,epoll来实现异步IO,这种方法通过事件驱动的模式确实能够很好处理并发问题,但是:
比如使用epoll模型,那么每一次传输或者接收到数据后得记录当前传输进度并重新注册事件,实现起来很麻烦。
4.实现
经过思考后,我选择模拟FTP协议,命令链接使用异步IO进行验证和部分信息的获取,获取后开一个线程开启一个数据传输链路进行连接传输。逻辑框图如下:
命令链路和传输链路分别占用一个进程,进程间通信使用两个消息队列Queue
一个队列中存放端口值(9000,9001……)另外一个存放相关的数据字典,包括文件名,文件大小等,下文简称端口队列,数据队列
命令端口获得连接后,查询端口队列是否为空,不为空则表示任有空闲的传输端口,之后将获得数据放入数据队列,此时传输端口进程监听到数据队列有数据,开启一个线程,线程中套接字绑定到对应传输端口等待客户端的链接,之后进行文件的传输,传输完成后,将本次链接端口号放入端口队列,退出线程。
代码如下:
class FtpServer:
def __init__(self,**kwds):
self.IpAddr=kwds['ipaddr']
self.Port=kwds['port']
self.QueuePorts=kwds['QueuePorts']
self.QueueInfors=kwds['QueueInfors']
self.sel=DefaultSelector()
self.Socket=socket.socket()
def connect(self):
self.Socket.bind((self.IpAddr,self.Port))
self.Socket.listen(100)
self.Socket.setblocking(False)
self.sel.register(self.Socket,EVENT_READ,self.accpet)
while True:
events=self.sel.select()
for key,mask in events:
callback = key.data
callback(key.fileobj, mask)
def accpet(self,sock,mask):
conn,addr=sock.accept()
conn.setblocking(False)
self.sel.register(conn,EVENT_READ,self.interface)
def Task_Upload(self,conn,size,name):
Error={}
if not self.QueuePorts.empty():
tmp = {}
tmp['port'] = self.QueuePorts.get()
tmp['action'] = 'Upload'
tmp['file_size']=size
tmp['extra']=name
self.QueueInfors.put(tmp) # 将信息传入消息队列 本次连接结束
conn.send(json.dumps({
'status': 'True', 'port': tmp['port'],'size':size}).encode('utf-8')) # 将本次连接结果返回客户端
else:
Error['status'] = False
Error['reason'] = '服务器资源已占满,请稍后再试'
package = json.dumps(Error)
conn.send(package.encode('utf-8'))
def Task_Download(self,conn,path):
Error = {}
if os.path.isfile(path)==