aiohttp_cors 和 aiohttp文件上传
- aiohttp_cors
- 跨域共享
- aiohttp_cors的使用方法
- 其他使用方法
- aiohttp的post和文件上传
- 文件的下载和上传
- 前端网页部分
- 前端Javascript部分
- 服务器端aiohttp部分
- 完整程序代码
- 说明
- 前端完整代码
- 服务端完整代码
aiohttp_cors
跨域共享
在浏览器中,如果使用与当前网页不是同协议(例如http)、同IP地址(或网址)、同端口的http资源,可能会出现跨域共享问题,浏览器控制台会报“No Access-Control-Allow-Origin header presence”错误,这是一种安全策略。有些例外情况跨域共享不会被禁止,详情可见:链接: http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html。
但我们的程序中,使用RestFUL网络服务,这些服务有时要用在不同网页中。为规避这一问题,就涉及到“跨域资源共享”(CORS)的问题。python的aiohttp是不具备这一功能(tornado内部具有此功能)的,为此,就必须使用与之配套的aiohttp_cors模块来补充这一功能。使用pip install aiohttp_cors命令,可以获取和安装这一模块,程序中使用import aiohttp_cors就可引入,作为对aiohttp CORS功能的补充。
aiohttp_cors的使用方法
aiohttp_cors的文档比较简单,使用方法在其文档中用例子来描述,在网页https://pypi.org/project/aiohttp_cors/0.7.0/中,其中没找到每个类和方法的详细说明。从编程的角度,我们关注的aiohttp_cors的关键有两点:
- 它是aiohttp的附加机制,配合其运行,配合方式有两种,一种是先定义好所有aiohttp
route,然后将cors功能加上,另一种是先定义cors功能,再逐个加入aiohttp route; - 它可以为aiohttp的每个route设置cors,也可以将一个cors用于所有route
我们采用的是将一个cors用于全部route并且先定义cors的方式,所依据的是其官网的如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | cors = aiohttp_cors.setup(app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*", ) }) # Add all resources to `CorsConfig`. resource = cors.add(app.router.add_resource("/hello")) cors.add(resource.add_route("GET", handler_get)) cors.add(resource.add_route("PUT", handler_put)) cors.add(resource.add_route("POST", handler_put)) cors.add(resource.add_route("DELETE", handler_delete)) |
据此,我们相应的程序段落如下:
1 2 3 4 5 6 7 8 9 10 | cors = aiohttp_cors.setup(app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*", allow_methods="*", ) }) resource = cors.add(app.router.add_resource("/post/file/")) cors.add(resource.add_route("POST", post_file)) |
其他使用方法
官网中为route逐个设置cors的例子:
1 2 3 4 5 6 7 8 9 | cors.add( app.router.add_route("GET", "/hello", handler), { "http://client.example.org": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers=("X-Custom-Server-Header",), allow_headers=("X-Requested-With", "Content-Type"), max_age=3600, ) }) |
还有先定义所有route,再施加cors的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # Setup application routes. app.router.add_route("GET", "/hello", handler_get) app.router.add_route("PUT", "/hello", handler_put) app.router.add_route("POST", "/hello", handler_put) app.router.add_route("DELETE", "/hello", handler_delete) # Configure default CORS settings. cors = aiohttp_cors.setup(app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*", ) }) # Configure CORS on all routes. for route in list(app.router.routes()): cors.add(route) |
编程者可以根据自己不同的情况选用,例如,后一个例子对于已经有了aiohttp程序之后要增加cors功能的情况就十分方便。
aiohttp的post和文件上传
文件的下载和上传
文件下载只要一个超文本引用(href)就可解决问题,无论是HTML客户端还是服务器端,都不需要额外编程,例如下面的HTML程序可以下载服务器网页目录下share子目录中的buffe.txt和modbus_therm_hum_01.xlsx两个文件:
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <a href="shared/buffer.txt" download="buffer.txt">文件:buffer.txt</a> <br/> <a href="shared/modbus_therm_hum_01.xlsx" download="modbus_therm_hum_01.xlsx">文件:modbus_therm_hum_01.xlsx</a> </body> </html> |
文件的上传却相对麻烦,不但需要网页客户端的(Javascript)程序,还需要服务器端的(jsp等)程序才能实现。由于我们使用aiohttp写的RestFul形式的Web Service,所以,将文件上传的服务也纳入其中。
当前,在文件上传方面,前台是JavaScript/CSS/HTML,后台是JSP、PHP等方式的参考资料较多,但前台是JavaScript/CSS/HTML,后台是aiohttp并且支持CORS的材料则相对较少。我们找了一些资料结合自己的情况,进行了测试和编程。本章介绍的是相关情况。解决了文件上传的问题,一般的POST编程也没有问题了。
完成文件上传一般以POST方式操作,需要以下组成部分:
- 浏览器端的网页程序部分(form);
- 浏览器端的文件处理程序部分(JavaScript);
- 服务器端的文件处理程序部分(aiohttp)。
这些部分可以用不同的方式实现,括号中是我们使用的方式。无论哪种方式,网上都有很多例子,有些例子比较完整,有些例子不完整;有些例子可以正确工作,有些例子运行起来有问题。下面几节以一些有参考价值的例子为基础,具体描述一下它们的实现,以及在实现中发现和解决的问题。
HTML和JavaScript部分主要参考了以下两个网页中的内容:
- 网页_1:https://www.ibm.com/developerworks/cn/web/1101_hanbf_fileupload/index.html
- 网页_2:http://www.jq22.com/webqd18520
aiphttp部分主要参考了以下两个网页中的内容:
- 网页_3:https://blog.csdn.net/u010080628/article/details/84975272
- 网页_4:https://wizardforcel.gitbooks.io/aiohttp-chinese-documentation/content/aiohttp%E6%96%87%E6%A1%A3/ServerUsage.html#%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E7%AE%B1,见其中的“文件上传”一节
前端网页部分
多数网页部分的描述都比较相似,关键有二:
- form具有enctype="multipart/form-data"属性;
- form中有一个文件输入的元素。
例如网页_2中相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <form id="form1" enctype="multipart/form-data" method="post" action="Upload.aspx"> <div class="row"> <label for="fileToUpload">Select a File to Upload</label><br> <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected();"> </div> <div id="fileName"></div> <div id="fileSize"></div> <div id="fileType"></div> <div class="row"> <input type="button" onclick="uploadFile()" value="Upload"> </div> <div id="progressNumber"></div> </form> |
其实,在文件上传的多数情况下,input 的 id 并不一定需要,如在网页_1中:
1 2 3 4 | <form name="demoForm" id="demoForm" method="post" enctype="multipart/form-data" action="javascript: uploadAndSubmit();"> <p><center>[wp_ad_camp_4]</center></p><p>Upload File: <input type="file" name="file" /></p> <p><input type="submit" value="Submit" /></p> </form> |
前端Javascript部分
网页_2中Javascript部分上载函数功能比较简单,经稍许修改即可正常工作:
1 2 3 4 5 6 7 8 9 10 11 | function uploadFile() { var fd = new FormData(); fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]); var xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", uploadProgress, false); xhr.addEventListener("load", uploadComplete, false); xhr.addEventListener("error", uploadFailed, false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "http://localhost:8900/post/file/"); //修改成自己的接口 xhr.send(fd); } |
事件处理使用单独定义的函数实现,后面我们会提供清单。这里面需要根据自己情况修改的有三处:
- 第三行程序中的第一个"fileToUpload",到服务器端,对应为接收对象的键(key),服务器端要用该键来获取收到的内容(详见2.4节),这里保持了原有的命名;
- 第三行程序中的第二个"fileToUpload",是对应网页input元素中的id,Javascript程序根据此id获取输入的文件对象,这里也保持了原有的命名;
- xhr.open()一行中的链接:http://localhost:8900/post/file/对应服务器相应服务的url,这里根据后台aiohttp程序的设置进行了修改,如果后台是jsp或PHP程序,对应的则是相应的接收程序及其所需的参数,例如:upload.jsp?fileName=" + file.name
要使网页_1中的程序正常工作,修改相对较多一些,但经过修改,仍然是可以配合服务器端正常工作的。
服务器端aiohttp部分
解决了CORS问题之后,后台程序所费周折相对较少, 网页_3、网页_4两个参考资料的介绍也基本一致,经过调整,我们采用的处理函数代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | async def post_file(request): try: reader = await request.multipart() file = await reader.next() filename = file.filename if file.filename else 'undefined' size = 0 with open(filename, 'wb') as f: while True: chunk = await file.read_chunk() # 默认是8192个字节。 if not chunk: break size += len(chunk) f.write(chunk) return web.Response(text="成功读取文件") except Exception as e: print(e) return web.Response(text="读取文件失败") |
上述函数在aiohttp和aiohttp_cors中的注册代码的片段如下:
1 2 3 4 5 6 7 8 9 10 | app = web.Application() cors = aiohttp_cors.setup(app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*", ) }) resource = cors.add(app.router.add_resource("/post/file/")) cors.add(resource.add_route("POST", post_file)) |
上面的处理代码是采用块状传输的方式,如果是小文件,可以采用直接接收POST数据的方式:
1 2 3 4 5 6 7 8 9 | async def post_file(request): try: data = await request.post() f = data['fileToUpload'].file content = f.read() print(content) except Exception as e: return web.Response(text="这是服务器返回的出错信息") return web.Response(text="这是服务器返回的正常信息") |
这段程序仅用于测试,它并未将上载的文件内容写入服务器文件,只是将其打印出来,这也演示了一般的POST操作。下面的完整程序中,我们将这一段程序的函数体部分以注释的方式给出。
完整程序代码
说明
为方便读者测试,这里给出的完整的程序代码,它包括两个部分:
- 前端代码:包括Javascript程序的完整HTML文件;
- 服务端代码:用aiohttp和aiohttp_cors实现的完整python服务端程序。
这两个程序均不再需要依赖额外的程序代码就可以运行,但python环境中必须包含所有import语句中的模块。如果没有,可以用pip install下载。前后端程序只要改动服务器url,就可以在不同的环境下运行了。
前端完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>upload files</title> <script> function fileSelected() { var file = document.getElementById('fileToUpload').files[0]; if (file) { var fileSize = 0; if (file.size > 1024 * 1024) fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; else fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; document.getElementById('fileName').innerHTML = 'Name: ' + file.name; document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize; document.getElementById('fileType').innerHTML = 'Type: ' + file.type; } } function uploadFile() { var fd = new FormData(); fd.append("fileToUpload", document.getElementById('fileToUpload').files[0]); var xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", uploadProgress, false); xhr.addEventListener("load", uploadComplete, false); xhr.addEventListener("error", uploadFailed, false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "http://localhost:8900/post/file/"); //修改成自己的接口 xhr.send(fd); } function uploadProgress(evt) { if (evt.lengthComputable) { var percentComplete = Math.round(evt.loaded * 100 / evt.total); document.getElementById('progressNumber').innerHTML = percentComplete.toString() + '%'; } else { document.getElementById('progressNumber').innerHTML = 'unable to compute'; } } function uploadComplete(evt) { /* 服务器端返回响应时候触发event事件*/ alert(evt.target.responseText); } function uploadFailed(evt) { alert("There was an error attempting to upload the file."); } function uploadCanceled(evt) { alert("The upload has been canceled by the user or the browser dropped the connection."); } </script> </head> <body> <form id="form1" enctype="multipart/form-data" method="post" action="javascript: fileSelected();"> <div class="row"> <label for="fileToUpload">Select a File to Upload</label><br> <!-- <input type="file" name="fileToUpload" id="fileToUpload" οnchange="fileSelected();"> --> <input type="file" name="fileToUpload" id="fileToUpload"> </div> <div id="fileName"></div> <div id="fileSize"></div> <div id="fileType"></div> <div class="row"> <input type="button" onclick="uploadFile()" value="Upload"> </div> <div id="progressNumber"></div> </form> </body> </html> |
服务端完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | from aiohttp import web import aiohttp_cors import asyncio import json import os ''' 这段注释掉的程序就是2.4节中描述的小文件传输的实验程序(只含函数体) try: data = await request.post() f = data['fileToUpload'].file content = f.read() print(content) except Exception as e: return web.Response(text="这是服务器返回的出错信息") return web.Response(text="这是服务器返回的正常信息") ''' async def post_file(request): try: reader = await request.multipart() file = await reader.next() filename = file.filename if file.filename else 'undefined' print('here: ', filename, os.getcwd()) size = 0 with open(filename, 'wb') as f: while True: chunk = await file.read_chunk() # 默认是8192个字节。 if not chunk: break size += len(chunk) f.write(chunk) text = {'res': '上传成功'} return web.Response(text=json.dumps(text, ensure_ascii=False)) except Exception as e: print(e) return web.Response(text="读取文件数据失败") async def init(): app = web.Application() cors = aiohttp_cors.setup(app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*", # allow_methods="*", ) }) resource = cors.add(app.router.add_resource("/post/file/")) cors.add(resource.add_route("POST", post_file)) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '0.0.0.0', 8900) await site.start() print('------- Http server started at localhost:8900 --------') # def main(): if __name__ == '__main__': loop = asyncio.new_event_loop() # get_event_loop() loop.run_until_complete(init()) loop.run_forever() |